diff --git a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/AccountsPreferencesModel.swift b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/AccountsPreferencesModel.swift similarity index 98% rename from Multiplatform/macOS/Preferences/Preference Panes/Accounts/AccountsPreferencesModel.swift rename to Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/AccountsPreferencesModel.swift index 74bb43a8b..c7683af0e 100644 --- a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/AccountsPreferencesModel.swift +++ b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/AccountsPreferencesModel.swift @@ -16,9 +16,10 @@ class AccountsPreferencesModel: ObservableObject { case add, credentials, none } + // Selected Account public private(set) var account: Account? - // Configured Accounts + // All Accounts @Published var sortedAccounts: [Account] = [] @Published var selectedConfiguredAccountID: String? = AccountManager.shared.defaultAccount.accountID { didSet { @@ -62,8 +63,6 @@ class AccountsPreferencesModel: ObservableObject { } @Published var showDeleteConfirmation: Bool = false - - // Subscriptions var notificationSubscriptions = Set() diff --git a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/AccountsPreferencesView.swift b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/AccountsPreferencesView.swift similarity index 96% rename from Multiplatform/macOS/Preferences/Preference Panes/Accounts/AccountsPreferencesView.swift rename to Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/AccountsPreferencesView.swift index a3b025039..20261fb98 100644 --- a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/AccountsPreferencesView.swift +++ b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/AccountsPreferencesView.swift @@ -20,7 +20,7 @@ struct AccountsPreferencesView: View { HStack(alignment: .top, spacing: 10) { listOfAccounts - EditAccountView(viewModel: viewModel) + AccountDetailView(viewModel: viewModel) .frame(height: 300, alignment: .leading) } Spacer() @@ -32,7 +32,7 @@ struct AccountsPreferencesView: View { case .add: AddAccountView(preferencesModel: viewModel) case .credentials: - EditAccountCredentials(viewModel: viewModel) + EditAccountCredentialsView(viewModel: viewModel) case .none: EmptyView() } @@ -48,7 +48,6 @@ struct AccountsPreferencesView: View { viewModel.showDeleteConfirmation = false })) }) - } var listOfAccounts: some View { diff --git a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Add Account/AddAccountModel.swift b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Add Account/AddAccountModel.swift similarity index 82% rename from Multiplatform/macOS/Preferences/Preference Panes/Accounts/Add Account/AddAccountModel.swift rename to Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Add Account/AddAccountModel.swift index b2a5a7a10..31f2976ba 100644 --- a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Add Account/AddAccountModel.swift +++ b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Add Account/AddAccountModel.swift @@ -13,35 +13,7 @@ import Secrets class AddAccountModel: ObservableObject { - enum AddAccountErrors: CustomStringConvertible { - case invalidUsernamePassword, invalidUsernamePasswordAPI, networkError, keyChainError, other(error: Error) , none - - var description: String { - switch self { - case .invalidUsernamePassword: - return NSLocalizedString("Invalid email or password combination.", comment: "Invalid email/password combination.") - case .invalidUsernamePasswordAPI: - return NSLocalizedString("Invalid email, password, or API URL combination.", comment: "Invalid email/password/API combination.") - case .networkError: - return NSLocalizedString("Network Error. Please try later.", comment: "Network Error. Please try later.") - case .keyChainError: - return NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error") - case .other(let error): - return NSLocalizedString(error.localizedDescription, comment: "Other add account error") - default: - return NSLocalizedString("N/A", comment: "N/A") - } - } - - static func ==(lhs: AddAccountErrors, rhs: AddAccountErrors) -> Bool { - switch (lhs, rhs) { - case (.other(let lhsError), .other(let rhsError)): - return lhsError.localizedDescription == rhsError.localizedDescription - default: - return false - } - } - } + #if DEBUG let addableAccountTypes: [AccountType] = [.onMyMac, .feedbin, .feedly, .feedWrangler, .freshRSS, .cloudKit, .newsBlur] @@ -56,7 +28,7 @@ class AddAccountModel: ObservableObject { @Published var apiUrl: String = "" @Published var newLocalAccountName: String = "" @Published var accountIsAuthenticating: Bool = false - @Published var addAccountError: AddAccountErrors = .none { + @Published var addAccountError: AccountUpdateErrors = .none { didSet { if addAccountError == .none { showError = false @@ -261,7 +233,7 @@ extension AddAccountModel { return } - let account = AccountManager.shared.createAccount(type: .newsBlur) + let account = AccountManager.shared.createAccount(type: .freshRSS) do { try account.removeCredentials(type: .readerBasic) diff --git a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Add Account/AddAccountPickerRow.swift b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Add Account/AddAccountPickerRow.swift similarity index 100% rename from Multiplatform/macOS/Preferences/Preference Panes/Accounts/Add Account/AddAccountPickerRow.swift rename to Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Add Account/AddAccountPickerRow.swift diff --git a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Add Account/AddAccountView.swift b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Add Account/AddAccountView.swift similarity index 95% rename from Multiplatform/macOS/Preferences/Preference Panes/Accounts/Add Account/AddAccountView.swift rename to Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Add Account/AddAccountView.swift index 8be44fee2..48e0dcd6d 100644 --- a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Add Account/AddAccountView.swift +++ b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Add Account/AddAccountView.swift @@ -50,7 +50,7 @@ struct AddAccountView: View { Spacer() HStack { if viewModel.accountIsAuthenticating { - ProgressView() + ProgressView("Adding Account") } Spacer() Button(action: { presentationMode.wrappedValue.dismiss() }, label: { @@ -121,12 +121,9 @@ struct AddAccountView: View { var userNamePasswordAndAPIUrlView: some View { Group { TextField("Email", text: $viewModel.userName) - .textFieldStyle(RoundedBorderTextFieldStyle()) SecureField("Password", text: $viewModel.password) - .textFieldStyle(RoundedBorderTextFieldStyle()) TextField("API URL", text: $viewModel.apiUrl) - .textFieldStyle(RoundedBorderTextFieldStyle()) - } + }.textFieldStyle(RoundedBorderTextFieldStyle()) } diff --git a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/ConfiguredAccountRow.swift b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/ConfiguredAccountRow.swift similarity index 100% rename from Multiplatform/macOS/Preferences/Preference Panes/Accounts/ConfiguredAccountRow.swift rename to Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/ConfiguredAccountRow.swift diff --git a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Edit Account/EditAccountView.swift b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Edit Account/AccountDetailView.swift similarity index 90% rename from Multiplatform/macOS/Preferences/Preference Panes/Accounts/Edit Account/EditAccountView.swift rename to Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Edit Account/AccountDetailView.swift index 87d4761b6..f979159ec 100644 --- a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Edit Account/EditAccountView.swift +++ b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Edit Account/AccountDetailView.swift @@ -1,5 +1,5 @@ // -// EditAccountView.swift +// AccountDetailView.swift // Multiplatform macOS // // Created by Stuart Breckenridge on 14/7/20. @@ -10,7 +10,7 @@ import SwiftUI import Account import Combine -struct EditAccountView: View { +struct AccountDetailView: View { @ObservedObject var viewModel: AccountsPreferencesModel @@ -76,8 +76,8 @@ struct EditAccountView: View { } -struct EditAccountView_Previews: PreviewProvider { +struct AccountDetailView_Previews: PreviewProvider { static var previews: some View { - EditAccountView(viewModel: AccountsPreferencesModel()) + AccountDetailView(viewModel: AccountsPreferencesModel()) } } diff --git a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Edit Account/EditAccountCredentialsModel.swift b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Edit Account/EditAccountCredentialsModel.swift new file mode 100644 index 000000000..55dd2e323 --- /dev/null +++ b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Edit Account/EditAccountCredentialsModel.swift @@ -0,0 +1,262 @@ +// +// EditAccountCredentialsModel.swift +// Multiplatform macOS +// +// Created by Stuart Breckenridge on 14/7/20. +// Copyright © 2020 Ranchero Software. All rights reserved. +// + +import Foundation +import Account +import Secrets + +class EditAccountCredentialsModel: ObservableObject { + + @Published var userName: String = "" + @Published var password: String = "" + @Published var apiUrl: String = "" + @Published var accountIsUpdatingCredentials: Bool = false + @Published var accountCredentialsWereUpdated: Bool = false + @Published var error: AccountUpdateErrors = .none { + didSet { + if error == .none { + showError = false + } else { + showError = true + } + } + } + @Published var showError: Bool = false + + func updateAccountCredentials(_ account: Account) { + switch account.type { + case .onMyMac: + return + case .feedbin: + updateFeedbin(account) + case .cloudKit: + return + case .feedWrangler: + updateFeedWrangler(account) + case .feedly: + updateFeedly(account) + case .freshRSS: + updateFreshRSS(account) + case .newsBlur: + updateNewsblur(account) + } + } + + func retrieveCredentials(_ account: Account) { + switch account.type { + case .feedbin: + let credentials = try? account.retrieveCredentials(type: .basic) + userName = credentials?.username ?? "" + password = credentials?.secret ?? "" + case .feedWrangler: + let credentials = try? account.retrieveCredentials(type: .feedWranglerBasic) + userName = credentials?.username ?? "" + password = credentials?.secret ?? "" + case .freshRSS: + let credentials = try? account.retrieveCredentials(type: .readerBasic) + userName = credentials?.username ?? "" + password = credentials?.secret ?? "" + case .newsBlur: + let credentials = try? account.retrieveCredentials(type: .newsBlurBasic) + userName = credentials?.username ?? "" + password = credentials?.secret ?? "" + default: + return + } + } + +} + +// MARK:- Update API +extension EditAccountCredentialsModel { + + func updateFeedbin(_ account: Account) { + accountIsUpdatingCredentials = true + let credentials = Credentials(type: .basic, username: userName, secret: password) + + Account.validateCredentials(type: .feedbin, credentials: credentials) { [weak self] result in + + guard let self = self else { return } + + self.accountIsUpdatingCredentials = false + + switch result { + case .success(let validatedCredentials): + + guard let validatedCredentials = validatedCredentials else { + self.error = .invalidUsernamePassword + return + } + + do { + try account.removeCredentials(type: .basic) + try account.storeCredentials(validatedCredentials) + self.accountCredentialsWereUpdated = true + account.refreshAll(completion: { result in + switch result { + case .success: + break + case .failure(let error): + self.error = .other(error: error) + } + }) + + } catch { + self.error = .keyChainError + } + + case .failure: + self.error = .networkError + } + } + } + + func updateFeedWrangler(_ account: Account) { + accountIsUpdatingCredentials = true + let credentials = Credentials(type: .feedWranglerBasic, username: userName, secret: password) + + Account.validateCredentials(type: .feedWrangler, credentials: credentials) { [weak self] result in + + guard let self = self else { return } + + self.accountIsUpdatingCredentials = false + + switch result { + case .success(let validatedCredentials): + + guard let validatedCredentials = validatedCredentials else { + self.error = .invalidUsernamePassword + return + } + + do { + try account.removeCredentials(type: .feedWranglerBasic) + try account.removeCredentials(type: .feedWranglerToken) + try account.storeCredentials(credentials) + try account.storeCredentials(validatedCredentials) + self.accountCredentialsWereUpdated = true + account.refreshAll(completion: { result in + switch result { + case .success: + break + case .failure(let error): + self.error = .other(error: error) + } + }) + + } catch { + self.error = .keyChainError + } + + case .failure: + self.error = .networkError + } + } + } + + func updateFeedly(_ account: Account) { + + } + + func updateFreshRSS(_ account: Account) { + accountIsUpdatingCredentials = true + let credentials = Credentials(type: .readerBasic, username: userName, secret: password) + + Account.validateCredentials(type: .freshRSS, credentials: credentials) { [weak self] result in + + guard let self = self else { return } + + self.accountIsUpdatingCredentials = false + + switch result { + case .success(let validatedCredentials): + + guard let validatedCredentials = validatedCredentials else { + self.error = .invalidUsernamePassword + return + } + + do { + try account.removeCredentials(type: .readerBasic) + try account.removeCredentials(type: .readerAPIKey) + try account.storeCredentials(credentials) + try account.storeCredentials(validatedCredentials) + self.accountCredentialsWereUpdated = true + account.refreshAll(completion: { result in + switch result { + case .success: + break + case .failure(let error): + self.error = .other(error: error) + } + }) + + } catch { + self.error = .keyChainError + } + + case .failure: + self.error = .networkError + } + } + } + + func updateNewsblur(_ account: Account) { + accountIsUpdatingCredentials = true + let credentials = Credentials(type: .newsBlurBasic, username: userName, secret: password) + + Account.validateCredentials(type: .newsBlur, credentials: credentials) { [weak self] result in + + guard let self = self else { return } + + self.accountIsUpdatingCredentials = false + + switch result { + case .success(let validatedCredentials): + + guard let validatedCredentials = validatedCredentials else { + self.error = .invalidUsernamePassword + return + } + + do { + try account.removeCredentials(type: .newsBlurBasic) + try account.removeCredentials(type: .newsBlurSessionId) + try account.storeCredentials(credentials) + try account.storeCredentials(validatedCredentials) + self.accountCredentialsWereUpdated = true + account.refreshAll(completion: { result in + switch result { + case .success: + break + case .failure(let error): + self.error = .other(error: error) + } + }) + + } catch { + self.error = .keyChainError + } + + case .failure: + self.error = .networkError + } + } + } + +} + +// MARK:- Retrieve Credentials +extension EditAccountCredentialsModel { + + + + + + +} diff --git a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Edit Account/EditAccountCredentialsView.swift b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Edit Account/EditAccountCredentialsView.swift new file mode 100644 index 000000000..8d2054960 --- /dev/null +++ b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Edit Account/EditAccountCredentialsView.swift @@ -0,0 +1,94 @@ +// +// EditAccountCredentialsView.swift +// Multiplatform macOS +// +// Created by Stuart Breckenridge on 14/7/20. +// Copyright © 2020 Ranchero Software. All rights reserved. +// + +import SwiftUI +import Secrets + +struct EditAccountCredentialsView: View { + + @Environment(\.presentationMode) var presentationMode + @StateObject private var editModel = EditAccountCredentialsModel() + @ObservedObject var viewModel: AccountsPreferencesModel + + var body: some View { + Form { + HStack { + Spacer() + Image(rsImage: viewModel.account!.smallIcon!.image) + .resizable() + .frame(width: 30, height: 30) + Text(viewModel.account?.nameForDisplay ?? "") + Spacer() + }.padding() + + HStack(alignment: .center) { + VStack(alignment: .trailing, spacing: 12) { + Text("Username: ") + Text("Password: ") + if viewModel.account?.type == .freshRSS { + Text("API URL: ") + } + }.frame(width: 75) + + VStack(alignment: .leading, spacing: 12) { + TextField("Username", text: $editModel.userName) + SecureField("Password", text: $editModel.password) + if viewModel.account?.type == .freshRSS { + TextField("API URL", text: $editModel.apiUrl) + } + } + }.textFieldStyle(RoundedBorderTextFieldStyle()) + + Spacer() + HStack{ + if editModel.accountIsUpdatingCredentials { + ProgressView("Updating") + } + Spacer() + Button("Cancel", action: { + presentationMode.wrappedValue.dismiss() + }) + if viewModel.account?.type != .freshRSS { + Button("Update", action: { + editModel.updateAccountCredentials(viewModel.account!) + }).disabled(editModel.userName.count == 0 || editModel.password.count == 0) + } else { + Button("Update", action: { + editModel.updateAccountCredentials(viewModel.account!) + }).disabled(editModel.userName.count == 0 || editModel.password.count == 0 || editModel.apiUrl.count == 0) + } + + } + }.onAppear { + editModel.retrieveCredentials(viewModel.account!) + } + .onChange(of: editModel.accountCredentialsWereUpdated) { value in + if value == true { + viewModel.sheetToShow = .none + } + } + .alert(isPresented: $editModel.showError) { + Alert(title: Text("Error Adding Account"), + message: Text(editModel.error.description), + dismissButton: .default(Text("Dismiss"), + action: { + editModel.error = .none + })) + } + .frame(idealWidth: 300, idealHeight: 200, alignment: .top) + .padding() + + } +} + +struct EditAccountCredentials_Previews: PreviewProvider { + static var previews: some View { + EditAccountCredentialsView(viewModel: AccountsPreferencesModel()) + } +} + diff --git a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/AccountManagement.swift b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/AccountManagement.swift new file mode 100644 index 000000000..d734c676c --- /dev/null +++ b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/AccountManagement.swift @@ -0,0 +1,18 @@ +// +// AccountManagement.swift +// Multiplatform macOS +// +// Created by Stuart Breckenridge on 14/7/20. +// Copyright © 2020 Ranchero Software. All rights reserved. +// + +import Foundation + +protocol AccountManagement { + + + + + + +} diff --git a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/AccountUpdateErrors.swift b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/AccountUpdateErrors.swift new file mode 100644 index 000000000..0ecec3500 --- /dev/null +++ b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/AccountUpdateErrors.swift @@ -0,0 +1,39 @@ +// +// AccountUpdateErrors.swift +// Multiplatform macOS +// +// Created by Stuart Breckenridge on 14/7/20. +// Copyright © 2020 Ranchero Software. All rights reserved. +// + +import Foundation + +enum AccountUpdateErrors: CustomStringConvertible { + case invalidUsernamePassword, invalidUsernamePasswordAPI, networkError, keyChainError, other(error: Error) , none + + var description: String { + switch self { + case .invalidUsernamePassword: + return NSLocalizedString("Invalid email or password combination.", comment: "Invalid email/password combination.") + case .invalidUsernamePasswordAPI: + return NSLocalizedString("Invalid email, password, or API URL combination.", comment: "Invalid email/password/API combination.") + case .networkError: + return NSLocalizedString("Network Error. Please try later.", comment: "Network Error. Please try later.") + case .keyChainError: + return NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error") + case .other(let error): + return NSLocalizedString(error.localizedDescription, comment: "Other add account error") + default: + return NSLocalizedString("N/A", comment: "N/A") + } + } + + static func ==(lhs: AccountUpdateErrors, rhs: AccountUpdateErrors) -> Bool { + switch (lhs, rhs) { + case (.other(let lhsError), .other(let rhsError)): + return lhsError.localizedDescription == rhsError.localizedDescription + default: + return false + } + } +} diff --git a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Edit Account/EditAccountCredentials.swift b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Edit Account/EditAccountCredentials.swift deleted file mode 100644 index fe868c12a..000000000 --- a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Edit Account/EditAccountCredentials.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// EditAccountCredentials.swift -// Multiplatform macOS -// -// Created by Stuart Breckenridge on 14/7/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Secrets - -struct EditAccountCredentials: View { - - @ObservedObject var viewModel: AccountsPreferencesModel - @Environment(\.presentationMode) var presentationMode - - @State private var userName: String = "" - @State private var password: String = "" - @State private var apiUrl: String? - - var body: some View { - Form { - HStack { - Spacer() - Image(rsImage: viewModel.account!.smallIcon!.image) - .resizable() - .frame(width: 30, height: 30) - Text(viewModel.account?.nameForDisplay ?? "") - Spacer() - }.padding() - - HStack(alignment: .center) { - VStack(alignment: .trailing, spacing: 12) { - Text("Username: ") - Text("Password: ") - }.frame(width: 75) - - VStack(alignment: .leading, spacing: 12) { - TextField("Username", text: $userName) - SecureField("Password", text: $password) - } - }.textFieldStyle(RoundedBorderTextFieldStyle()) - - Spacer() - HStack{ - Spacer() - Button("Dismiss", action: { - presentationMode.wrappedValue.dismiss() - }) - Button("Update", action: { - presentationMode.wrappedValue.dismiss() - }) - } - }.onAppear { - let credentials = try? viewModel.account?.retrieveCredentials(type: .basic) - userName = credentials?.username ?? "" - password = credentials?.secret ?? "" - } - .frame(idealWidth: 300, idealHeight: 200, alignment: .top) - .padding() - } -} - -struct EditAccountCredentials_Previews: PreviewProvider { - static var previews: some View { - EditAccountCredentials(viewModel: AccountsPreferencesModel()) - } -} - diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 6cd60e8e0..923e1ee06 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -23,8 +23,10 @@ 1769E32724BC5B6C000E1E8E /* AddAccountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1769E32624BC5B6C000E1E8E /* AddAccountModel.swift */; }; 1769E32924BCAFC7000E1E8E /* AddAccountPickerRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1769E32824BCAFC7000E1E8E /* AddAccountPickerRow.swift */; }; 1769E32B24BCB030000E1E8E /* ConfiguredAccountRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1769E32A24BCB030000E1E8E /* ConfiguredAccountRow.swift */; }; - 1769E32D24BD20A0000E1E8E /* EditAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1769E32C24BD20A0000E1E8E /* EditAccountView.swift */; }; - 1769E33024BD6271000E1E8E /* EditAccountCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1769E32F24BD6271000E1E8E /* EditAccountCredentials.swift */; }; + 1769E32D24BD20A0000E1E8E /* AccountDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1769E32C24BD20A0000E1E8E /* AccountDetailView.swift */; }; + 1769E33024BD6271000E1E8E /* EditAccountCredentialsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1769E32F24BD6271000E1E8E /* EditAccountCredentialsView.swift */; }; + 1769E33624BD9621000E1E8E /* EditAccountCredentialsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1769E33524BD9621000E1E8E /* EditAccountCredentialsModel.swift */; }; + 1769E33824BD97CB000E1E8E /* AccountUpdateErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1769E33724BD97CB000E1E8E /* AccountUpdateErrors.swift */; }; 1776E88E24AC5F8A00E78166 /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1776E88D24AC5F8A00E78166 /* AppDefaults.swift */; }; 1776E88F24AC5F8A00E78166 /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1776E88D24AC5F8A00E78166 /* AppDefaults.swift */; }; 17930ED424AF10EE00A9BA52 /* AddWebFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17930ED324AF10EE00A9BA52 /* AddWebFeedView.swift */; }; @@ -1800,8 +1802,10 @@ 1769E32624BC5B6C000E1E8E /* AddAccountModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccountModel.swift; sourceTree = ""; }; 1769E32824BCAFC7000E1E8E /* AddAccountPickerRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccountPickerRow.swift; sourceTree = ""; }; 1769E32A24BCB030000E1E8E /* ConfiguredAccountRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfiguredAccountRow.swift; sourceTree = ""; }; - 1769E32C24BD20A0000E1E8E /* EditAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccountView.swift; sourceTree = ""; }; - 1769E32F24BD6271000E1E8E /* EditAccountCredentials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccountCredentials.swift; sourceTree = ""; }; + 1769E32C24BD20A0000E1E8E /* AccountDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDetailView.swift; sourceTree = ""; }; + 1769E32F24BD6271000E1E8E /* EditAccountCredentialsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccountCredentialsView.swift; sourceTree = ""; }; + 1769E33524BD9621000E1E8E /* EditAccountCredentialsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccountCredentialsModel.swift; sourceTree = ""; }; + 1769E33724BD97CB000E1E8E /* AccountUpdateErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountUpdateErrors.swift; sourceTree = ""; }; 1776E88D24AC5F8A00E78166 /* AppDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDefaults.swift; sourceTree = ""; }; 17930ED324AF10EE00A9BA52 /* AddWebFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddWebFeedView.swift; sourceTree = ""; }; 179DBBA2B22A659F81EED6F9 /* AccountsNewsBlurWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsNewsBlurWindowController.swift; sourceTree = ""; }; @@ -2547,11 +2551,8 @@ 1769E31F24BC58A4000E1E8E /* Accounts */ = { isa = PBXGroup; children = ( - 1769E32124BC5925000E1E8E /* AccountsPreferencesModel.swift */, - 1729529024AA1CAA00D65E66 /* AccountsPreferencesView.swift */, - 1769E32A24BCB030000E1E8E /* ConfiguredAccountRow.swift */, - 1769E32E24BD5F22000E1E8E /* Edit Account */, - 1769E32324BC5A50000E1E8E /* Add Account */, + 1769E33724BD97CB000E1E8E /* AccountUpdateErrors.swift */, + 1769E33924BD97E5000E1E8E /* Account Preferences */, ); path = Accounts; sourceTree = ""; @@ -2577,12 +2578,25 @@ 1769E32E24BD5F22000E1E8E /* Edit Account */ = { isa = PBXGroup; children = ( - 1769E32C24BD20A0000E1E8E /* EditAccountView.swift */, - 1769E32F24BD6271000E1E8E /* EditAccountCredentials.swift */, + 1769E32C24BD20A0000E1E8E /* AccountDetailView.swift */, + 1769E32F24BD6271000E1E8E /* EditAccountCredentialsView.swift */, + 1769E33524BD9621000E1E8E /* EditAccountCredentialsModel.swift */, ); path = "Edit Account"; sourceTree = ""; }; + 1769E33924BD97E5000E1E8E /* Account Preferences */ = { + isa = PBXGroup; + children = ( + 1769E32124BC5925000E1E8E /* AccountsPreferencesModel.swift */, + 1729529024AA1CAA00D65E66 /* AccountsPreferencesView.swift */, + 1769E32A24BCB030000E1E8E /* ConfiguredAccountRow.swift */, + 1769E32324BC5A50000E1E8E /* Add Account */, + 1769E32E24BD5F22000E1E8E /* Edit Account */, + ); + path = "Account Preferences"; + sourceTree = ""; + }; 17930ED224AF10CD00A9BA52 /* Add */ = { isa = PBXGroup; children = ( @@ -5193,6 +5207,7 @@ 51E4996F24A8764C00B667CB /* ActivityType.swift in Sources */, 51E4994E24A8734C00B667CB /* SendToMarsEditCommand.swift in Sources */, 51919FB024AA8EFA00541E64 /* SidebarItemView.swift in Sources */, + 1769E33624BD9621000E1E8E /* EditAccountCredentialsModel.swift in Sources */, 51919FEF24AB85E400541E64 /* TimelineContainerView.swift in Sources */, 51E4996624A8760B00B667CB /* ArticleStyle.swift in Sources */, 5171B4D424B7BABA00FB8D3B /* MarkStatusCommand.swift in Sources */, @@ -5217,6 +5232,7 @@ 51E4994F24A8734C00B667CB /* TwitterFeedProvider-Extensions.swift in Sources */, 51E498CA24A8085D00B667CB /* SmartFeedDelegate.swift in Sources */, 5177470A24B2F87600EB0F74 /* SidebarListStyleModifier.swift in Sources */, + 1769E33824BD97CB000E1E8E /* AccountUpdateErrors.swift in Sources */, 51E4990524A808C300B667CB /* FeaturedImageDownloader.swift in Sources */, 5181C5AE24AF89B1002E0F70 /* PreferredColorSchemeModifier.swift in Sources */, 1769E32924BCAFC7000E1E8E /* AddAccountPickerRow.swift in Sources */, @@ -5233,7 +5249,7 @@ 1729529724AA1CD000D65E66 /* MacPreferencesView.swift in Sources */, 51E4994C24A8734C00B667CB /* RedditFeedProvider-Extensions.swift in Sources */, 1729529324AA1CAA00D65E66 /* AccountsPreferencesView.swift in Sources */, - 1769E32D24BD20A0000E1E8E /* EditAccountView.swift in Sources */, + 1769E32D24BD20A0000E1E8E /* AccountDetailView.swift in Sources */, 51919FAD24AA8CCA00541E64 /* UnreadCountView.swift in Sources */, 51E498C924A8085D00B667CB /* PseudoFeed.swift in Sources */, 51E498FC24A808BA00B667CB /* FaviconURLFinder.swift in Sources */, @@ -5263,7 +5279,7 @@ 51408B7F24A9EC6F0073CF4E /* SidebarItem.swift in Sources */, 514E6BDB24ACEA0400AC6F6E /* TimelineItemView.swift in Sources */, 51E4996E24A8764C00B667CB /* ActivityManager.swift in Sources */, - 1769E33024BD6271000E1E8E /* EditAccountCredentials.swift in Sources */, + 1769E33024BD6271000E1E8E /* EditAccountCredentialsView.swift in Sources */, 51E4995A24A873F900B667CB /* ErrorHandler.swift in Sources */, 51E4991F24A8094300B667CB /* RSImage-AppIcons.swift in Sources */, 51A5769724AE617200078888 /* ArticleContainerView.swift in Sources */,