diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index 8d5564215..64a897a00 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -263,6 +263,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, switch credentials { case .basic(let username, _): self.username = username + default: + return } try CredentialsManager.storeCredentials(credentials, server: server) diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index eab5f9426..a6cd26935 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -139,7 +139,7 @@ 51EF0F902279C9500050506E /* AccountsAddViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F8F2279C9500050506E /* AccountsAddViewController.swift */; }; 51EF0F922279CA620050506E /* AccountsAddTableCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F912279CA620050506E /* AccountsAddTableCellView.swift */; }; 51F35D0922AFD4760003CE1B /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F35D0822AFD4760003CE1B /* SettingsView.swift */; }; - 51F35D1B22B001010003CE1B /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F35D1A22B001010003CE1B /* SettingsViewModel.swift */; }; + 51F772F622B279570087D9D1 /* SettingsDetailAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F772EC22B2789B0087D9D1 /* SettingsDetailAccountView.swift */; }; 51F85BE5227217D000C787DC /* RefreshIntervalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BDB2272162F00C787DC /* RefreshIntervalViewController.swift */; }; 51F85BE7227245FC00C787DC /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BE6227245FC00C787DC /* AboutViewController.swift */; }; 51F85BEB22724CB600C787DC /* About.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 51F85BEA22724CB600C787DC /* About.rtf */; }; @@ -730,7 +730,7 @@ 51EF0F8F2279C9500050506E /* AccountsAddViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsAddViewController.swift; sourceTree = ""; }; 51EF0F912279CA620050506E /* AccountsAddTableCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsAddTableCellView.swift; sourceTree = ""; }; 51F35D0822AFD4760003CE1B /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; - 51F35D1A22B001010003CE1B /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; + 51F772EC22B2789B0087D9D1 /* SettingsDetailAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDetailAccountView.swift; sourceTree = ""; }; 51F85BDB2272162F00C787DC /* RefreshIntervalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshIntervalViewController.swift; sourceTree = ""; }; 51F85BE6227245FC00C787DC /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; }; 51F85BEA22724CB600C787DC /* About.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = About.rtf; sourceTree = ""; }; @@ -1040,13 +1040,13 @@ 5183CCEB227117C70010922C /* Settings */ = { isa = PBXGroup; children = ( - 51F35CFD22AFD0350003CE1B /* UIKit */, - 51F35D0822AFD4760003CE1B /* SettingsView.swift */, - 51F35D1A22B001010003CE1B /* SettingsViewModel.swift */, 510D708122B041CC004E8F65 /* SettingsAccountLabelView.swift */, 510D707322B028E1004E8F65 /* SettingsAddAccountView.swift */, - 510D707D22B02A4B004E8F65 /* SettingsLocalAccountView.swift */, + 51F772EC22B2789B0087D9D1 /* SettingsDetailAccountView.swift */, 510D707F22B02A5F004E8F65 /* SettingsFeedbinAccountView.swift */, + 510D707D22B02A4B004E8F65 /* SettingsLocalAccountView.swift */, + 51F35D0822AFD4760003CE1B /* SettingsView.swift */, + 51F35CFD22AFD0350003CE1B /* UIKit */, ); path = Settings; sourceTree = ""; @@ -1935,12 +1935,12 @@ ORGANIZATIONNAME = "Ranchero Software"; TargetAttributes = { 6581C73220CED60000F4AD34 = { - DevelopmentTeam = M8L2WTLA8W; + DevelopmentTeam = SHJK2V3AJG; ProvisioningStyle = Manual; }; 840D617B2029031C009BC708 = { CreatedOnToolsVersion = 9.3; - DevelopmentTeam = M8L2WTLA8W; + DevelopmentTeam = SHJK2V3AJG; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.BackgroundModes = { @@ -1950,7 +1950,7 @@ }; 849C645F1ED37A5D003D8FC0 = { CreatedOnToolsVersion = 8.2.1; - DevelopmentTeam = M8L2WTLA8W; + DevelopmentTeam = SHJK2V3AJG; ProvisioningStyle = Manual; SystemCapabilities = { com.apple.HardenedRuntime = { @@ -1960,7 +1960,7 @@ }; 849C64701ED37A5D003D8FC0 = { CreatedOnToolsVersion = 8.2.1; - DevelopmentTeam = 9C84TZ7Q6Z; + DevelopmentTeam = SHJK2V3AJG; ProvisioningStyle = Automatic; TestTargetID = 849C645F1ED37A5D003D8FC0; }; @@ -2324,6 +2324,7 @@ 510D707422B028E1004E8F65 /* SettingsAddAccountView.swift in Sources */, 51C45294226509C800C03939 /* SearchFeedDelegate.swift in Sources */, 512E09352268B25900BDCFDD /* UISplitViewController-Extensions.swift in Sources */, + 51F772F622B279570087D9D1 /* SettingsDetailAccountView.swift in Sources */, 510D707E22B02A4B004E8F65 /* SettingsLocalAccountView.swift in Sources */, 51C452A022650A1900C03939 /* FeedIconDownloader.swift in Sources */, 51F85BE7227245FC00C787DC /* AboutViewController.swift in Sources */, @@ -2348,7 +2349,6 @@ 51C4529922650A0000C03939 /* ArticleStylesManager.swift in Sources */, 51EF0F802277A8330050506E /* MasterTimelineCellLayout.swift in Sources */, 51F85BF722749FA100C787DC /* UIFont-Extensions.swift in Sources */, - 51F35D1B22B001010003CE1B /* SettingsViewModel.swift in Sources */, 515436882291D75D005E1CDF /* AddLocalAccountViewController.swift in Sources */, 51C452AF2265108300C03939 /* ArticleArray.swift in Sources */, 51C4528E2265099C00C03939 /* SmartFeedsController.swift in Sources */, diff --git a/README.md b/README.md index 58d23fecf..09f8c3d68 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# NetNewsWire +# ![Icon](Technotes/Images/icon.png) NetNewsWire + +[![CircleCI](https://circleci.com/gh/brentsimmons/NetNewsWire.svg?style=svg)](https://circleci.com/gh/brentsimmons/NetNewsWire) It’s a free and open source feed reader for macOS. diff --git a/Technotes/BranchingStrategy.md b/Technotes/BranchingStrategy.md new file mode 100644 index 000000000..768e9ab48 --- /dev/null +++ b/Technotes/BranchingStrategy.md @@ -0,0 +1,32 @@ + +# NetNewsWire Branching Strategy + +The main repository for NetNewsWire utilizes a [Trunk Based Development](https://trunkbaseddevelopment.com) branching strategy. This branching strategy is a variant of [Three-Flow](https://www.nomachetejuggling.com/2017/04/09/a-different-branching-strategy/). + +## Three-Flow + +Three-Flow uses 3 branches to facilitate development, stabilize a release, and manage production hotfixes. Development happens on Master and moves to a branch called Candidate when it is ready to be stabilized. New feature development continues on Master and bug fixes to the release candidate happen on Candidate. When the product is released, it is pushed to the Release branch. Hotfixes can happen on the Release branch. Candidate is now free to be reused to stabilize the next release. All bugs found and fixed are back merged to Candidate and then Master respectively. + +![Branching](Images/Branching.png) + +All arrows going up are promotions (pushes) to the next environment. All arrows going down are back ports of bugfixes. + +That is Three-Flow applied to NetNewsWire. It would be that simple, but we have two products we are going to deliver from the same repository. The iOS and the macOS variants of NetNewsWire. To stabilize and manage both variants, each will need to be given their own Candidate and Release branches. + +![Branching Full](Images/Branching-Full.png) + +Today (6/12/2019) we have 2 branches, master and macOS Candidate, in the main repository which will eventually grow to be 5 branches. + +There will also be a number of repository forks that NetNewWire developers will create to do bug fixes and implement new features (not shown here). Typically contributers will fork the Master branch to thier own repository. They would then create a feature/bugfix branch on their repository. Once work on thier forked branch is complete, they will submit a pull request to be merged back into the main repository master. + +## Tagging + +Each release should be tagged using [Semantic Versioning](https://semver.org/). Candidates will continue to be tagged using the current convention which denotes the difference between developer, alpha and beta releases. Additionally, we will need to use a convention to avoid tag name collisions between iOS and macOS products. macOS will use even minor release numbers and iOS will use odd minor release numbers. (See the above diagram for examples.) + +## Submodules + +NetNewsWire uses Git submodules to manage project dependencies. All the submodules are under the same project umbrella as NetNewWire and there are no third party dependencies to manage. These submodules are mostly stable at this point. For simplicity sake, all development on the submodules will continue on their repository Master branch. These submodules won’t be managed as separate projects with separate releases/tags at this time. + +## Summary + +There are 3 types of branches: Master, Candidate, and Release. All feature development happens on Master. Stabilization happens on Candidate. Hotfixes happen on Release. Each product gets its own Candidate and Release branches. All candidates and releases get tagged. diff --git a/Technotes/ContinuousIntegration.md b/Technotes/ContinuousIntegration.md new file mode 100644 index 000000000..fb100ea0b --- /dev/null +++ b/Technotes/ContinuousIntegration.md @@ -0,0 +1,30 @@ +# NetNewsWire Continuous Integration + +CI for NetNewsWire is enabled through CircleCI, hosted at +. The CI configuration (hosted in +[`.circleci/config.yml`](https://github.com/brentsimmons/NetNewsWire/blob/master/.circleci/config.yml) +uses `xcodebuild` to build the project after syncing the repository and +the various submodules. + +As of June 2019, CircleCI offered Xcode 10.2.1, so IOS 13 and Catalina support are not available +via CI as yet. + +The build itself focuses on the scheme NetNewsWire and leverages the +`NetNewsWire.xcworkspace` configuration. + +Each submodule also has it's own CI configuration, which are set up and built from +their own repositories. The submodule CI systems are entirely independent so that +those libraries can grow and change, getting CI verification, indepdent of NetNewsWire. + +The submodule CI are typically set to run a build and any available tests. Refer to the +project repository for the current and complete list of submodules, but for quick reference: + +- [RSCore](https://github.com/brentsimmons/RSCore) [![CircleCI](https://circleci.com/gh/brentsimmons/RSCore.svg?style=svg)](https://circleci.com/gh/brentsimmons/RSCore) + +- [RSWeb](https://github.com/brentsimmons/RSWeb) [![CircleCI](https://circleci.com/gh/brentsimmons/RSWeb.svg?style=svg)](https://circleci.com/gh/brentsimmons/RSWeb) + +- [RSParser](https://github.com/brentsimmons/RSParser) [![CircleCI](https://circleci.com/gh/brentsimmons/RSParser.svg?style=svg)](https://circleci.com/gh/brentsimmons/RSParser) + +- [RSTree](https://github.com/brentsimmons/RSTree) [![CircleCI](https://circleci.com/gh/brentsimmons/RSTree.svg?style=svg)](https://circleci.com/gh/brentsimmons/RSTree) + +- [RSDatabase](https://github.com/brentsimmons/RSDatabase) [![CircleCI](https://circleci.com/gh/brentsimmons/RSDatabase.svg?style=svg)](https://circleci.com/gh/brentsimmons/RSDatabase) diff --git a/Technotes/Images/Branching-Full.png b/Technotes/Images/Branching-Full.png new file mode 100644 index 000000000..54de93b94 Binary files /dev/null and b/Technotes/Images/Branching-Full.png differ diff --git a/Technotes/Images/Branching.png b/Technotes/Images/Branching.png new file mode 100644 index 000000000..a64ec6756 Binary files /dev/null and b/Technotes/Images/Branching.png differ diff --git a/Technotes/Images/icon.png b/Technotes/Images/icon.png new file mode 100644 index 000000000..3596d8961 Binary files /dev/null and b/Technotes/Images/icon.png differ diff --git a/Technotes/README.md b/Technotes/README.md index a6af274ce..20a6392f7 100644 --- a/Technotes/README.md +++ b/Technotes/README.md @@ -16,4 +16,8 @@ ## Contributing +[Contributing](../CONTRIBUTING.md) + [Coding Guidelines](CodingGuidelines.md) + +[Branching Strategy](BranchingStrategy.md) diff --git a/Technotes/Reruns.md b/Technotes/Reruns.md new file mode 100644 index 000000000..11d043f80 --- /dev/null +++ b/Technotes/Reruns.md @@ -0,0 +1,39 @@ +# Why Reruns Happen + +Sometimes you might see a new article in a feed that you’d swear you’ve already read. And maybe you can even see, in NetNewsWire, what looks like another copy of that same exact article, with no changes. + +Here’s the thing to know: if the article really was the exact same in every respect, NetNewsWire would see that. It’s super-easy for a computer to tell that some data is the exact same as some other data. + +When it’s not really the exact same, that’s where the problem comes in. + +Here are some reasons this situation can happen: + +## A blog changes its blog engine + +If someone switches from (for instance) Ghost to WordPress, then the code that creates that feeds will be different. And that code will make a different choice for the unique ID for each article in the feed. + +Those unique IDs are critical: they’re how NetNewsWire identifies an article. If an article appears with a new unique ID, then NetNewsWire treats it like a new article. + +In this situation, you’ll often see that you get a bunch of reruns for a given feed all at once. You’ll get 10 or 20 or whatever. + +This is by far the most common cause of reruns. + +## A feed that lacks unique IDs does something weird + +This is quite a bit less common. There are some feeds that don’t have unique IDs, which means NetNewsWire has to use some combination of other article metadata to identify articles. + +That metadata could change just enough to throw NetNewsWire off. This is rare, but it can happen. + +## A feed just has terrible bugs + +We’ve seen feeds that create a different unique ID for each article every time you fetch the feed, which results in reruns every single time. We’ve seen feeds that use the same unique ID for every article in the feed, even — which goes against the very idea of unique IDs! + +Some feeds just have bugs, and weird, unpredictable things happen. + +NetNewsWire is designed to be resistant to that, and it does a good job — but we haven’t anticipated every odd case. + +However, this is the most rare cause of reruns. The most common cause is, by far, the first one: the feed is now being generated by different software. + +## Reporting Bugs + +If you have a feed that keeps showing reruns (as opposed to once, when a blog changes its blogging system), please do report a bug, either on our [Issues Tracker](https://github.com/brentsimmons/NetNewsWire/issues) or on the [Slack group](https://join.slack.com/t/netnewswire/shared_invite/enQtNjM4MDA1MjQzMDkzLTNlNjBhOWVhYzdhYjA4ZWFhMzQ1MTUxYjU0NTE5ZGY0YzYwZWJhNjYwNTNmNTg2NjIwYWY4YzhlYzk5NmU3ZTc). diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index f91f0ca6a..13eca1c11 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -394,7 +394,7 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn @IBAction func settings(_ sender: UIBarButtonItem) { - let settings = UIHostingController(rootView: SettingsView(viewModel: SettingsViewModel())) + let settings = UIHostingController(rootView: SettingsView(viewModel: SettingsView.ViewModel())) self.present(settings, animated: true) } diff --git a/iOS/Settings/SettingsAddAccountView.swift b/iOS/Settings/SettingsAddAccountView.swift index e7ec22319..fde5175f0 100644 --- a/iOS/Settings/SettingsAddAccountView.swift +++ b/iOS/Settings/SettingsAddAccountView.swift @@ -7,14 +7,15 @@ // import SwiftUI +import Account struct SettingsAddAccountView : View { var body: some View { List { - PresentationButton(SettingsAccountLabelView(accountImage: "accountLocal", accountLabel: "On My Device"), - destination: SettingsLocalAccountView(name: "")) + PresentationButton(SettingsAccountLabelView(accountImage: "accountLocal", accountLabel: Account.defaultLocalAccountName), + destination: SettingsLocalAccountView(name: "")).padding(.all, 4) PresentationButton(SettingsAccountLabelView(accountImage: "accountFeedbin", accountLabel: "Feedbin"), - destination: SettingsFeedbinAccountView(email: "", password: "")) + destination: SettingsFeedbinAccountView(viewModel: SettingsFeedbinAccountView.ViewModel())).padding(.all, 4) } .listStyle(.grouped) .navigationBarTitle(Text("Add Account"), displayMode: .inline) diff --git a/iOS/Settings/SettingsDetailAccountView.swift b/iOS/Settings/SettingsDetailAccountView.swift new file mode 100644 index 000000000..75f4fe1fe --- /dev/null +++ b/iOS/Settings/SettingsDetailAccountView.swift @@ -0,0 +1,114 @@ +// +// SettingsDetailAccountView.swift +// NetNewsWire +// +// Created by Maurice Parker on 6/13/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import SwiftUI +import Combine +import Account + +struct SettingsDetailAccountView : View { + @ObjectBinding var viewModel: ViewModel + @State private var verifyDelete = false + + var body: some View { + List { + Section { + HStack { + Text("Name") + Divider() + TextField($viewModel.name, placeholder: Text("(Optional)")) + } + Toggle(isOn: $viewModel.isActive) { + Text("Active") + } + } + Section { + HStack { + Spacer() + Button(action: { + + }) { + Text("Credentials") + } + Spacer() + } + } + if viewModel.isDeletable { + Section { + HStack { + Spacer() + Button(action: { + self.verifyDelete = true + }) { + Text("Delete Account") + .foregroundColor(.red) + } + .presentation($verifyDelete) { + Alert(title: Text("Are you sure you want to delete \"\(viewModel.nameForDisplay)\"?"), + primaryButton: Alert.Button.default(Text("Delete"), onTrigger: { self.viewModel.delete() }), + secondaryButton: Alert.Button.cancel()) + } + Spacer() + } + } + } + } + .listStyle(.grouped) + .navigationBarTitle(Text(verbatim: viewModel.nameForDisplay), displayMode: .inline) + + } + + class ViewModel: BindableObject { + let didChange = PassthroughSubject() + let account: Account + + init(_ account: Account) { + self.account = account + } + + var nameForDisplay: String { + account.nameForDisplay + } + + var name: String { + get { + account.name ?? "" + } + set { + account.name = newValue.isEmpty ? nil : newValue + didChange.send(self) + } + } + + var isActive: Bool { + get { + account.isActive + } + set { + account.isActive = newValue + didChange.send(self) + } + } + + var isDeletable: Bool { + return AccountManager.shared.defaultAccount != account + } + + func delete() { + AccountManager.shared.deleteAccount(account) + } + } +} + +#if DEBUG +struct SettingsDetailAccountView_Previews : PreviewProvider { + static var previews: some View { + let viewModel = SettingsDetailAccountView.ViewModel(AccountManager.shared.defaultAccount) + return SettingsDetailAccountView(viewModel: viewModel) + } +} +#endif diff --git a/iOS/Settings/SettingsFeedbinAccountView.swift b/iOS/Settings/SettingsFeedbinAccountView.swift index f62fce8c5..50d425bc7 100644 --- a/iOS/Settings/SettingsFeedbinAccountView.swift +++ b/iOS/Settings/SettingsFeedbinAccountView.swift @@ -7,11 +7,17 @@ // import SwiftUI +import Combine +import Account +import RSWeb struct SettingsFeedbinAccountView : View { - @State var email: String - @State var password: String - + @Environment(\.isPresented) private var isPresented + @ObjectBinding var viewModel: ViewModel + @State var busy: Bool = false + @State var error: Text = Text("") + var account: Account? = nil + var body: some View { NavigationView { List { @@ -20,27 +26,116 @@ struct SettingsFeedbinAccountView : View { ) { HStack { Spacer() - TextField($email, placeholder: Text("Email")) + TextField($viewModel.email, placeholder: Text("Email")) + .textContentType(.username) Spacer() } HStack { Spacer() - SecureField($password, placeholder: Text("Password")) + SecureField($viewModel.password, placeholder: Text("Password")) Spacer() } } - Section { + Section(footer: HStack { Spacer() - Button(action: {}) { + error.color(.red) + Spacer() + } + ) { + HStack { + Spacer() + Button(action: { self.addAccount() }) { Text("Add Account") } + .disabled(!viewModel.isValid) Spacer() } } } + .disabled(busy) .listStyle(.grouped) .navigationBarTitle(Text(""), displayMode: .inline) + .navigationBarItems(leading: + Button(action: { self.dismiss() }) { Text("Cancel") } + ) + } + } + + private func addAccount() { + + busy = true + + let emailAddress = viewModel.email.trimmingCharacters(in: .whitespaces) + let credentials = Credentials.basic(username: emailAddress, password: viewModel.password) + + Account.validateCredentials(type: .feedbin, credentials: credentials) { result in + + self.busy = false + + switch result { + case .success(let authenticated): + + if authenticated { + + var newAccount = false + let workAccount: Account + if self.account == nil { + workAccount = AccountManager.shared.createAccount(type: .feedbin) + newAccount = true + } else { + workAccount = self.account! + } + + do { + + do { + try workAccount.removeBasicCredentials() + } catch {} + try workAccount.storeCredentials(credentials) + + if newAccount { + workAccount.refreshAll() { result in } + } + + self.dismiss() + + } catch { + self.error = Text("Keychain error while storing credentials.") + } + + } else { + self.error = Text("Invalid email/password combination.") + } + + case .failure: + self.error = Text("Network error. Try again later.") + } + + } + + } + + private func dismiss() { + isPresented?.value = false + } + + class ViewModel: BindableObject { + let didChange = PassthroughSubject() + + var email: String = "" { + didSet { + didChange.send(self) + } + } + var password: String = "" { + didSet { + didChange.send(self) + } + } + + var isValid: Bool { + return !email.isEmpty && !password.isEmpty } } @@ -49,7 +144,7 @@ struct SettingsFeedbinAccountView : View { #if DEBUG struct SettingsFeedbinAccountView_Previews : PreviewProvider { static var previews: some View { - SettingsFeedbinAccountView(email: "", password: "") + SettingsFeedbinAccountView(viewModel: SettingsFeedbinAccountView.ViewModel()) } } #endif diff --git a/iOS/Settings/SettingsLocalAccountView.swift b/iOS/Settings/SettingsLocalAccountView.swift index 2e1db27f4..40f671661 100644 --- a/iOS/Settings/SettingsLocalAccountView.swift +++ b/iOS/Settings/SettingsLocalAccountView.swift @@ -10,13 +10,14 @@ import SwiftUI import Account struct SettingsLocalAccountView : View { + @Environment(\.isPresented) private var isPresented @State var name: String - + var body: some View { NavigationView { List { Section(header: - SettingsAccountLabelView(accountImage: "accountLocal", accountLabel: "On My Device").padding() + SettingsAccountLabelView(accountImage: "accountLocal", accountLabel: Account.defaultLocalAccountName).padding() ) { HStack { Spacer() @@ -36,13 +37,20 @@ struct SettingsLocalAccountView : View { } .listStyle(.grouped) .navigationBarTitle(Text(""), displayMode: .inline) + .navigationBarItems(leading: Button(action: { self.dismiss() }) { Text("Cancel") } ) } } - func addAccount() { + private func addAccount() { let account = AccountManager.shared.createAccount(type: .onMyMac) account.name = name + dismiss() } + + private func dismiss() { + isPresented?.value = false + } + } #if DEBUG diff --git a/iOS/Settings/SettingsView.swift b/iOS/Settings/SettingsView.swift index 629e78b25..d0ddae653 100644 --- a/iOS/Settings/SettingsView.swift +++ b/iOS/Settings/SettingsView.swift @@ -7,10 +7,11 @@ // import SwiftUI +import Combine import Account struct SettingsView : View { - @ObjectBinding var viewModel: SettingsViewModel + @ObjectBinding var viewModel: ViewModel var body: some View { NavigationView { @@ -18,7 +19,9 @@ struct SettingsView : View { Section(header: Text("ACCOUNTS")) { ForEach(viewModel.accounts.identified(by: \.self)) { account in - Text(verbatim: account.nameForDisplay) + NavigationButton(destination: SettingsDetailAccountView(viewModel: SettingsDetailAccountView.ViewModel(account)), isDetail: false) { + Text(verbatim: account.nameForDisplay) + } } NavigationButton(destination: SettingsAddAccountView(), isDetail: false) { Text("Add Account") @@ -83,12 +86,74 @@ struct SettingsView : View { } } + + class ViewModel: BindableObject { + + let didChange = PassthroughSubject() + + init() { + NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange(_:)), name: .AccountsDidChange, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil) + } + + var accounts: [Account] { + get { + return AccountManager.shared.sortedAccounts + } + set { + } + } + + var sortOldestToNewest: Bool { + get { + return AppDefaults.timelineSortDirection == .orderedDescending + } + set { + if newValue == true { + AppDefaults.timelineSortDirection = .orderedDescending + } else { + AppDefaults.timelineSortDirection = .orderedAscending + } + didChange.send(self) + } + } + + var timelineNumberOfLines: Int { + get { + return AppDefaults.timelineNumberOfLines + } + set { + AppDefaults.timelineNumberOfLines = newValue + didChange.send(self) + } + } + + var refreshInterval: RefreshInterval { + get { + return AppDefaults.refreshInterval + } + set { + AppDefaults.refreshInterval = newValue + didChange.send(self) + } + } + + @objc func accountsDidChange(_ notification: Notification) { + didChange.send(self) + } + + @objc func displayNameDidChange(_ notification: Notification) { + didChange.send(self) + } + + } + } #if DEBUG struct SettingsView_Previews : PreviewProvider { static var previews: some View { - SettingsView(viewModel: SettingsViewModel()) + SettingsView(viewModel: SettingsView.ViewModel()) } } #endif diff --git a/iOS/Settings/SettingsViewModel.swift b/iOS/Settings/SettingsViewModel.swift deleted file mode 100644 index d51735fa4..000000000 --- a/iOS/Settings/SettingsViewModel.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// SettingsViewModel.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 6/11/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import Foundation -import SwiftUI -import Combine -import Account - -class SettingsViewModel: BindableObject { - - let didChange = PassthroughSubject() - - init() { - NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange(_:)), name: .AccountsDidChange, object: nil) - } - - var accounts: [Account] { - get { - return AccountManager.shared.accounts - } - set { - } - } - - var sortOldestToNewest: Bool { - get { - return AppDefaults.timelineSortDirection == .orderedDescending - } - set { - if newValue == true { - AppDefaults.timelineSortDirection = .orderedDescending - } else { - AppDefaults.timelineSortDirection = .orderedAscending - } - didChange.send(self) - } - } - - var timelineNumberOfLines: Int { - get { - return AppDefaults.timelineNumberOfLines - } - set { - AppDefaults.timelineNumberOfLines = newValue - didChange.send(self) - } - } - - var refreshInterval: RefreshInterval { - get { - return AppDefaults.refreshInterval - } - set { - AppDefaults.refreshInterval = newValue - didChange.send(self) - } - } - - @objc func accountsDidChange(_ notification: Notification) { - didChange.send(self) - } - -} diff --git a/submodules/RSParser b/submodules/RSParser index 812594e90..032edf89b 160000 --- a/submodules/RSParser +++ b/submodules/RSParser @@ -1 +1 @@ -Subproject commit 812594e902d4d640b08fea9cc6184d6ee84424e2 +Subproject commit 032edf89b64ccbbfb6c05887b239a4bf81329b92 diff --git a/submodules/RSWeb b/submodules/RSWeb index 1686ee1b1..5d648e405 160000 --- a/submodules/RSWeb +++ b/submodules/RSWeb @@ -1 +1 @@ -Subproject commit 1686ee1b1c5a4aef6ebe1ffd72b17d6defabd9fd +Subproject commit 5d648e4050b700bb20fc7ae3303f087edcb3228f