Merge branch 'master' of https://github.com/brentsimmons/NetNewsWire
This commit is contained in:
commit
c092b91349
|
@ -263,6 +263,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||||
switch credentials {
|
switch credentials {
|
||||||
case .basic(let username, _):
|
case .basic(let username, _):
|
||||||
self.username = username
|
self.username = username
|
||||||
|
default:
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try CredentialsManager.storeCredentials(credentials, server: server)
|
try CredentialsManager.storeCredentials(credentials, server: server)
|
||||||
|
|
|
@ -139,7 +139,7 @@
|
||||||
51EF0F902279C9500050506E /* AccountsAddViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F8F2279C9500050506E /* AccountsAddViewController.swift */; };
|
51EF0F902279C9500050506E /* AccountsAddViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F8F2279C9500050506E /* AccountsAddViewController.swift */; };
|
||||||
51EF0F922279CA620050506E /* AccountsAddTableCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F912279CA620050506E /* AccountsAddTableCellView.swift */; };
|
51EF0F922279CA620050506E /* AccountsAddTableCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F912279CA620050506E /* AccountsAddTableCellView.swift */; };
|
||||||
51F35D0922AFD4760003CE1B /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F35D0822AFD4760003CE1B /* SettingsView.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 */; };
|
51F85BE5227217D000C787DC /* RefreshIntervalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BDB2272162F00C787DC /* RefreshIntervalViewController.swift */; };
|
||||||
51F85BE7227245FC00C787DC /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BE6227245FC00C787DC /* AboutViewController.swift */; };
|
51F85BE7227245FC00C787DC /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BE6227245FC00C787DC /* AboutViewController.swift */; };
|
||||||
51F85BEB22724CB600C787DC /* About.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 51F85BEA22724CB600C787DC /* About.rtf */; };
|
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 = "<group>"; };
|
51EF0F8F2279C9500050506E /* AccountsAddViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsAddViewController.swift; sourceTree = "<group>"; };
|
||||||
51EF0F912279CA620050506E /* AccountsAddTableCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsAddTableCellView.swift; sourceTree = "<group>"; };
|
51EF0F912279CA620050506E /* AccountsAddTableCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsAddTableCellView.swift; sourceTree = "<group>"; };
|
||||||
51F35D0822AFD4760003CE1B /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
51F35D0822AFD4760003CE1B /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||||
51F35D1A22B001010003CE1B /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
|
51F772EC22B2789B0087D9D1 /* SettingsDetailAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDetailAccountView.swift; sourceTree = "<group>"; };
|
||||||
51F85BDB2272162F00C787DC /* RefreshIntervalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshIntervalViewController.swift; sourceTree = "<group>"; };
|
51F85BDB2272162F00C787DC /* RefreshIntervalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshIntervalViewController.swift; sourceTree = "<group>"; };
|
||||||
51F85BE6227245FC00C787DC /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = "<group>"; };
|
51F85BE6227245FC00C787DC /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = "<group>"; };
|
||||||
51F85BEA22724CB600C787DC /* About.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = About.rtf; sourceTree = "<group>"; };
|
51F85BEA22724CB600C787DC /* About.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = About.rtf; sourceTree = "<group>"; };
|
||||||
|
@ -1040,13 +1040,13 @@
|
||||||
5183CCEB227117C70010922C /* Settings */ = {
|
5183CCEB227117C70010922C /* Settings */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
51F35CFD22AFD0350003CE1B /* UIKit */,
|
|
||||||
51F35D0822AFD4760003CE1B /* SettingsView.swift */,
|
|
||||||
51F35D1A22B001010003CE1B /* SettingsViewModel.swift */,
|
|
||||||
510D708122B041CC004E8F65 /* SettingsAccountLabelView.swift */,
|
510D708122B041CC004E8F65 /* SettingsAccountLabelView.swift */,
|
||||||
510D707322B028E1004E8F65 /* SettingsAddAccountView.swift */,
|
510D707322B028E1004E8F65 /* SettingsAddAccountView.swift */,
|
||||||
510D707D22B02A4B004E8F65 /* SettingsLocalAccountView.swift */,
|
51F772EC22B2789B0087D9D1 /* SettingsDetailAccountView.swift */,
|
||||||
510D707F22B02A5F004E8F65 /* SettingsFeedbinAccountView.swift */,
|
510D707F22B02A5F004E8F65 /* SettingsFeedbinAccountView.swift */,
|
||||||
|
510D707D22B02A4B004E8F65 /* SettingsLocalAccountView.swift */,
|
||||||
|
51F35D0822AFD4760003CE1B /* SettingsView.swift */,
|
||||||
|
51F35CFD22AFD0350003CE1B /* UIKit */,
|
||||||
);
|
);
|
||||||
path = Settings;
|
path = Settings;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1935,12 +1935,12 @@
|
||||||
ORGANIZATIONNAME = "Ranchero Software";
|
ORGANIZATIONNAME = "Ranchero Software";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
6581C73220CED60000F4AD34 = {
|
6581C73220CED60000F4AD34 = {
|
||||||
DevelopmentTeam = M8L2WTLA8W;
|
DevelopmentTeam = SHJK2V3AJG;
|
||||||
ProvisioningStyle = Manual;
|
ProvisioningStyle = Manual;
|
||||||
};
|
};
|
||||||
840D617B2029031C009BC708 = {
|
840D617B2029031C009BC708 = {
|
||||||
CreatedOnToolsVersion = 9.3;
|
CreatedOnToolsVersion = 9.3;
|
||||||
DevelopmentTeam = M8L2WTLA8W;
|
DevelopmentTeam = SHJK2V3AJG;
|
||||||
ProvisioningStyle = Automatic;
|
ProvisioningStyle = Automatic;
|
||||||
SystemCapabilities = {
|
SystemCapabilities = {
|
||||||
com.apple.BackgroundModes = {
|
com.apple.BackgroundModes = {
|
||||||
|
@ -1950,7 +1950,7 @@
|
||||||
};
|
};
|
||||||
849C645F1ED37A5D003D8FC0 = {
|
849C645F1ED37A5D003D8FC0 = {
|
||||||
CreatedOnToolsVersion = 8.2.1;
|
CreatedOnToolsVersion = 8.2.1;
|
||||||
DevelopmentTeam = M8L2WTLA8W;
|
DevelopmentTeam = SHJK2V3AJG;
|
||||||
ProvisioningStyle = Manual;
|
ProvisioningStyle = Manual;
|
||||||
SystemCapabilities = {
|
SystemCapabilities = {
|
||||||
com.apple.HardenedRuntime = {
|
com.apple.HardenedRuntime = {
|
||||||
|
@ -1960,7 +1960,7 @@
|
||||||
};
|
};
|
||||||
849C64701ED37A5D003D8FC0 = {
|
849C64701ED37A5D003D8FC0 = {
|
||||||
CreatedOnToolsVersion = 8.2.1;
|
CreatedOnToolsVersion = 8.2.1;
|
||||||
DevelopmentTeam = 9C84TZ7Q6Z;
|
DevelopmentTeam = SHJK2V3AJG;
|
||||||
ProvisioningStyle = Automatic;
|
ProvisioningStyle = Automatic;
|
||||||
TestTargetID = 849C645F1ED37A5D003D8FC0;
|
TestTargetID = 849C645F1ED37A5D003D8FC0;
|
||||||
};
|
};
|
||||||
|
@ -2324,6 +2324,7 @@
|
||||||
510D707422B028E1004E8F65 /* SettingsAddAccountView.swift in Sources */,
|
510D707422B028E1004E8F65 /* SettingsAddAccountView.swift in Sources */,
|
||||||
51C45294226509C800C03939 /* SearchFeedDelegate.swift in Sources */,
|
51C45294226509C800C03939 /* SearchFeedDelegate.swift in Sources */,
|
||||||
512E09352268B25900BDCFDD /* UISplitViewController-Extensions.swift in Sources */,
|
512E09352268B25900BDCFDD /* UISplitViewController-Extensions.swift in Sources */,
|
||||||
|
51F772F622B279570087D9D1 /* SettingsDetailAccountView.swift in Sources */,
|
||||||
510D707E22B02A4B004E8F65 /* SettingsLocalAccountView.swift in Sources */,
|
510D707E22B02A4B004E8F65 /* SettingsLocalAccountView.swift in Sources */,
|
||||||
51C452A022650A1900C03939 /* FeedIconDownloader.swift in Sources */,
|
51C452A022650A1900C03939 /* FeedIconDownloader.swift in Sources */,
|
||||||
51F85BE7227245FC00C787DC /* AboutViewController.swift in Sources */,
|
51F85BE7227245FC00C787DC /* AboutViewController.swift in Sources */,
|
||||||
|
@ -2348,7 +2349,6 @@
|
||||||
51C4529922650A0000C03939 /* ArticleStylesManager.swift in Sources */,
|
51C4529922650A0000C03939 /* ArticleStylesManager.swift in Sources */,
|
||||||
51EF0F802277A8330050506E /* MasterTimelineCellLayout.swift in Sources */,
|
51EF0F802277A8330050506E /* MasterTimelineCellLayout.swift in Sources */,
|
||||||
51F85BF722749FA100C787DC /* UIFont-Extensions.swift in Sources */,
|
51F85BF722749FA100C787DC /* UIFont-Extensions.swift in Sources */,
|
||||||
51F35D1B22B001010003CE1B /* SettingsViewModel.swift in Sources */,
|
|
||||||
515436882291D75D005E1CDF /* AddLocalAccountViewController.swift in Sources */,
|
515436882291D75D005E1CDF /* AddLocalAccountViewController.swift in Sources */,
|
||||||
51C452AF2265108300C03939 /* ArticleArray.swift in Sources */,
|
51C452AF2265108300C03939 /* ArticleArray.swift in Sources */,
|
||||||
51C4528E2265099C00C03939 /* SmartFeedsController.swift in Sources */,
|
51C4528E2265099C00C03939 /* SmartFeedsController.swift in Sources */,
|
||||||
|
|
|
@ -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.
|
It’s a free and open source feed reader for macOS.
|
||||||
|
|
||||||
|
|
|
@ -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.
|
|
@ -0,0 +1,30 @@
|
||||||
|
# NetNewsWire Continuous Integration
|
||||||
|
|
||||||
|
CI for NetNewsWire is enabled through CircleCI, hosted at
|
||||||
|
<https://circleci.com/gh/brentsimmons/NetNewsWire>. 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)
|
Binary file not shown.
After Width: | Height: | Size: 45 KiB |
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
|
@ -16,4 +16,8 @@
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
[Contributing](../CONTRIBUTING.md)
|
||||||
|
|
||||||
[Coding Guidelines](CodingGuidelines.md)
|
[Coding Guidelines](CodingGuidelines.md)
|
||||||
|
|
||||||
|
[Branching Strategy](BranchingStrategy.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).
|
|
@ -394,7 +394,7 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn
|
||||||
|
|
||||||
@IBAction func settings(_ sender: UIBarButtonItem) {
|
@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)
|
self.present(settings, animated: true)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,14 +7,15 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Account
|
||||||
|
|
||||||
struct SettingsAddAccountView : View {
|
struct SettingsAddAccountView : View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
PresentationButton(SettingsAccountLabelView(accountImage: "accountLocal", accountLabel: "On My Device"),
|
PresentationButton(SettingsAccountLabelView(accountImage: "accountLocal", accountLabel: Account.defaultLocalAccountName),
|
||||||
destination: SettingsLocalAccountView(name: ""))
|
destination: SettingsLocalAccountView(name: "")).padding(.all, 4)
|
||||||
PresentationButton(SettingsAccountLabelView(accountImage: "accountFeedbin", accountLabel: "Feedbin"),
|
PresentationButton(SettingsAccountLabelView(accountImage: "accountFeedbin", accountLabel: "Feedbin"),
|
||||||
destination: SettingsFeedbinAccountView(email: "", password: ""))
|
destination: SettingsFeedbinAccountView(viewModel: SettingsFeedbinAccountView.ViewModel())).padding(.all, 4)
|
||||||
}
|
}
|
||||||
.listStyle(.grouped)
|
.listStyle(.grouped)
|
||||||
.navigationBarTitle(Text("Add Account"), displayMode: .inline)
|
.navigationBarTitle(Text("Add Account"), displayMode: .inline)
|
||||||
|
|
|
@ -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<ViewModel, Never>()
|
||||||
|
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
|
|
@ -7,11 +7,17 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Combine
|
||||||
|
import Account
|
||||||
|
import RSWeb
|
||||||
|
|
||||||
struct SettingsFeedbinAccountView : View {
|
struct SettingsFeedbinAccountView : View {
|
||||||
@State var email: String
|
@Environment(\.isPresented) private var isPresented
|
||||||
@State var password: String
|
@ObjectBinding var viewModel: ViewModel
|
||||||
|
@State var busy: Bool = false
|
||||||
|
@State var error: Text = Text("")
|
||||||
|
var account: Account? = nil
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
List {
|
List {
|
||||||
|
@ -20,27 +26,116 @@ struct SettingsFeedbinAccountView : View {
|
||||||
) {
|
) {
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
TextField($email, placeholder: Text("Email"))
|
TextField($viewModel.email, placeholder: Text("Email"))
|
||||||
|
.textContentType(.username)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
SecureField($password, placeholder: Text("Password"))
|
SecureField($viewModel.password, placeholder: Text("Password"))
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Section {
|
Section(footer:
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
Button(action: {}) {
|
error.color(.red)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Button(action: { self.addAccount() }) {
|
||||||
Text("Add Account")
|
Text("Add Account")
|
||||||
}
|
}
|
||||||
|
.disabled(!viewModel.isValid)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.disabled(busy)
|
||||||
.listStyle(.grouped)
|
.listStyle(.grouped)
|
||||||
.navigationBarTitle(Text(""), displayMode: .inline)
|
.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<ViewModel, Never>()
|
||||||
|
|
||||||
|
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
|
#if DEBUG
|
||||||
struct SettingsFeedbinAccountView_Previews : PreviewProvider {
|
struct SettingsFeedbinAccountView_Previews : PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
SettingsFeedbinAccountView(email: "", password: "")
|
SettingsFeedbinAccountView(viewModel: SettingsFeedbinAccountView.ViewModel())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -10,13 +10,14 @@ import SwiftUI
|
||||||
import Account
|
import Account
|
||||||
|
|
||||||
struct SettingsLocalAccountView : View {
|
struct SettingsLocalAccountView : View {
|
||||||
|
@Environment(\.isPresented) private var isPresented
|
||||||
@State var name: String
|
@State var name: String
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
List {
|
List {
|
||||||
Section(header:
|
Section(header:
|
||||||
SettingsAccountLabelView(accountImage: "accountLocal", accountLabel: "On My Device").padding()
|
SettingsAccountLabelView(accountImage: "accountLocal", accountLabel: Account.defaultLocalAccountName).padding()
|
||||||
) {
|
) {
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
@ -36,13 +37,20 @@ struct SettingsLocalAccountView : View {
|
||||||
}
|
}
|
||||||
.listStyle(.grouped)
|
.listStyle(.grouped)
|
||||||
.navigationBarTitle(Text(""), displayMode: .inline)
|
.navigationBarTitle(Text(""), displayMode: .inline)
|
||||||
|
.navigationBarItems(leading: Button(action: { self.dismiss() }) { Text("Cancel") } )
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addAccount() {
|
private func addAccount() {
|
||||||
let account = AccountManager.shared.createAccount(type: .onMyMac)
|
let account = AccountManager.shared.createAccount(type: .onMyMac)
|
||||||
account.name = name
|
account.name = name
|
||||||
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func dismiss() {
|
||||||
|
isPresented?.value = false
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
|
|
@ -7,10 +7,11 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Combine
|
||||||
import Account
|
import Account
|
||||||
|
|
||||||
struct SettingsView : View {
|
struct SettingsView : View {
|
||||||
@ObjectBinding var viewModel: SettingsViewModel
|
@ObjectBinding var viewModel: ViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
|
@ -18,7 +19,9 @@ struct SettingsView : View {
|
||||||
|
|
||||||
Section(header: Text("ACCOUNTS")) {
|
Section(header: Text("ACCOUNTS")) {
|
||||||
ForEach(viewModel.accounts.identified(by: \.self)) { account in
|
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) {
|
NavigationButton(destination: SettingsAddAccountView(), isDetail: false) {
|
||||||
Text("Add Account")
|
Text("Add Account")
|
||||||
|
@ -83,12 +86,74 @@ struct SettingsView : View {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ViewModel: BindableObject {
|
||||||
|
|
||||||
|
let didChange = PassthroughSubject<ViewModel, Never>()
|
||||||
|
|
||||||
|
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
|
#if DEBUG
|
||||||
struct SettingsView_Previews : PreviewProvider {
|
struct SettingsView_Previews : PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
SettingsView(viewModel: SettingsViewModel())
|
SettingsView(viewModel: SettingsView.ViewModel())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -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<SettingsViewModel, Never>()
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 812594e902d4d640b08fea9cc6184d6ee84424e2
|
Subproject commit 032edf89b64ccbbfb6c05887b239a4bf81329b92
|
|
@ -1 +1 @@
|
||||||
Subproject commit 1686ee1b1c5a4aef6ebe1ffd72b17d6defabd9fd
|
Subproject commit 5d648e4050b700bb20fc7ae3303f087edcb3228f
|
Loading…
Reference in New Issue