mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Update Settings for Accounts to work with the latest SwiftUI
This commit is contained in:
16
iOS/Model Extensions/Account-Extensions.swift
Normal file
16
iOS/Model Extensions/Account-Extensions.swift
Normal file
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// Account-Extensions.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 9/7/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Account
|
||||
|
||||
extension Account: Identifiable {
|
||||
public var id: String {
|
||||
return accountID
|
||||
}
|
||||
}
|
||||
@@ -14,19 +14,13 @@ struct SettingsAccountLabelView : View {
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
HStack {
|
||||
Image(accountImage)
|
||||
.resizable()
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.frame(height: 32)
|
||||
Text(verbatim: accountLabel).font(.title)
|
||||
|
||||
}
|
||||
.layoutPriority(1)
|
||||
Spacer()
|
||||
Image(accountImage)
|
||||
.resizable()
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.frame(height: 32)
|
||||
Text(verbatim: accountLabel).font(.title)
|
||||
}
|
||||
.foregroundColor(.primary)
|
||||
.foregroundColor(.primary).padding(4.0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,17 +10,38 @@ import SwiftUI
|
||||
import Account
|
||||
|
||||
struct SettingsAddAccountView : View {
|
||||
|
||||
@State private var isAddPresented = false
|
||||
@State private var selectedAccountType: AccountType = nil
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
PresentationButton(destination: SettingsLocalAccountView(name: "")) {
|
||||
|
||||
Button(action: {
|
||||
self.selectedAccountType = AccountType.onMyMac
|
||||
self.isAddPresented.toggle()
|
||||
}) {
|
||||
SettingsAccountLabelView(accountImage: "accountLocal", accountLabel: Account.defaultLocalAccountName)
|
||||
}
|
||||
.padding(4)
|
||||
PresentationButton(destination: SettingsFeedbinAccountView(viewModel: SettingsFeedbinAccountView.ViewModel())) {
|
||||
|
||||
Button(action: {
|
||||
self.selectedAccountType = AccountType.feedbin
|
||||
self.isAddPresented.toggle()
|
||||
}) {
|
||||
SettingsAccountLabelView(accountImage: "accountFeedbin", accountLabel: "Feedbin")
|
||||
}
|
||||
.padding(4)
|
||||
|
||||
|
||||
}
|
||||
.sheet(isPresented: $isAddPresented) {
|
||||
if self.selectedAccountType == .onMyMac {
|
||||
SettingsLocalAccountView(name: "")
|
||||
}
|
||||
if self.selectedAccountType == .feedbin {
|
||||
SettingsFeedbinAccountView(viewModel: SettingsFeedbinAccountView.ViewModel())
|
||||
}
|
||||
}
|
||||
|
||||
.navigationBarTitle(Text("Add Account"), displayMode: .inline)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,17 +12,15 @@ import Account
|
||||
import RSWeb
|
||||
|
||||
struct SettingsDetailAccountView : View {
|
||||
@ObjectBinding var viewModel: ViewModel
|
||||
@State private var verifyDelete = false
|
||||
@State private var showFeedbinCredentials = false
|
||||
|
||||
@ObservedObject var viewModel: ViewModel
|
||||
@State private var isFeedbinCredentialsPresented = false
|
||||
@State private var isDeleteAlertPresented = false
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
HStack {
|
||||
Text("Name")
|
||||
Divider()
|
||||
TextField($viewModel.name, placeholder: Text("(Optional)"))
|
||||
TextField("Name", text: $viewModel.name)
|
||||
}
|
||||
Toggle(isOn: $viewModel.isActive) {
|
||||
Text("Active")
|
||||
@@ -33,33 +31,33 @@ struct SettingsDetailAccountView : View {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(action: {
|
||||
self.showFeedbinCredentials = true
|
||||
self.isFeedbinCredentialsPresented.toggle()
|
||||
}) {
|
||||
Text("Credentials")
|
||||
}
|
||||
.presentation(showFeedbinCredentials ? feedbinCredentialsModal : nil)
|
||||
.onDisappear() { self.showFeedbinCredentials = false }
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $isFeedbinCredentialsPresented) {
|
||||
self.settingsFeedbinAccountView
|
||||
}
|
||||
}
|
||||
if viewModel.isDeletable {
|
||||
Section {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(action: {
|
||||
self.verifyDelete = true
|
||||
self.isDeleteAlertPresented.toggle()
|
||||
}) {
|
||||
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())
|
||||
Text("Delete Account").foregroundColor(.red)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.alert(isPresented: $isDeleteAlertPresented) {
|
||||
Alert(title: Text("Are you sure you want to delete \"\(viewModel.nameForDisplay)\"?"),
|
||||
primaryButton: Alert.Button.default(Text("Delete"), action: { self.viewModel.delete() }),
|
||||
secondaryButton: Alert.Button.cancel())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,13 +65,15 @@ struct SettingsDetailAccountView : View {
|
||||
|
||||
}
|
||||
|
||||
var feedbinCredentialsModal: Modal {
|
||||
var settingsFeedbinAccountView: SettingsFeedbinAccountView {
|
||||
let feedbinViewModel = SettingsFeedbinAccountView.ViewModel(account: viewModel.account)
|
||||
return Modal(SettingsFeedbinAccountView(viewModel: feedbinViewModel))
|
||||
return SettingsFeedbinAccountView(viewModel: feedbinViewModel)
|
||||
}
|
||||
|
||||
class ViewModel: BindableObject {
|
||||
let didChange = PassthroughSubject<ViewModel, Never>()
|
||||
class ViewModel: ObservableObject {
|
||||
|
||||
let objectWillChange = ObservableObjectPublisher()
|
||||
|
||||
let account: Account
|
||||
|
||||
init(_ account: Account) {
|
||||
@@ -89,8 +89,8 @@ struct SettingsDetailAccountView : View {
|
||||
account.name ?? ""
|
||||
}
|
||||
set {
|
||||
objectWillChange.send()
|
||||
account.name = newValue.isEmpty ? nil : newValue
|
||||
didChange.send(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,8 +99,8 @@ struct SettingsDetailAccountView : View {
|
||||
account.isActive
|
||||
}
|
||||
set {
|
||||
objectWillChange.send()
|
||||
account.isActive = newValue
|
||||
didChange.send(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ struct SettingsDetailAccountView : View {
|
||||
|
||||
func delete() {
|
||||
AccountManager.shared.deleteAccount(account)
|
||||
ActivityManager.shared.cleanUp(account)
|
||||
// ActivityManager.shared.cleanUp(account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,10 @@ import Account
|
||||
import RSWeb
|
||||
|
||||
struct SettingsFeedbinAccountView : View {
|
||||
@Environment(\.isPresented) private var isPresented
|
||||
@ObjectBinding var viewModel: ViewModel
|
||||
@Environment(\.presentationMode) var presentation
|
||||
@ObservedObject var viewModel: ViewModel
|
||||
@State var busy: Bool = false
|
||||
@State var error: Text = Text("")
|
||||
@State var error: String = ""
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
@@ -23,22 +23,13 @@ struct SettingsFeedbinAccountView : View {
|
||||
Section(header:
|
||||
SettingsAccountLabelView(accountImage: "accountFeedbin", accountLabel: "Feedbin").padding()
|
||||
) {
|
||||
HStack {
|
||||
Text("Email:")
|
||||
Divider()
|
||||
TextField($viewModel.email)
|
||||
.textContentType(.username)
|
||||
}
|
||||
HStack {
|
||||
Text("Password:")
|
||||
Divider()
|
||||
SecureField($viewModel.password)
|
||||
}
|
||||
TextField("Email", text: $viewModel.email).textContentType(.emailAddress)
|
||||
SecureField("Password", text: $viewModel.password)
|
||||
}
|
||||
Section(footer:
|
||||
HStack {
|
||||
Spacer()
|
||||
error.color(.red)
|
||||
Text(verbatim: error).foregroundColor(.red)
|
||||
Spacer()
|
||||
}
|
||||
) {
|
||||
@@ -67,7 +58,7 @@ struct SettingsFeedbinAccountView : View {
|
||||
private func addAccount() {
|
||||
|
||||
busy = true
|
||||
error = Text("")
|
||||
error = ""
|
||||
|
||||
let emailAddress = viewModel.email.trimmingCharacters(in: .whitespaces)
|
||||
let credentials = Credentials.basic(username: emailAddress, password: viewModel.password)
|
||||
@@ -104,15 +95,15 @@ struct SettingsFeedbinAccountView : View {
|
||||
self.dismiss()
|
||||
|
||||
} catch {
|
||||
self.error = Text("Keychain error while storing credentials.")
|
||||
self.error = "Keychain error while storing credentials."
|
||||
}
|
||||
|
||||
} else {
|
||||
self.error = Text("Invalid email/password combination.")
|
||||
self.error = "Invalid email/password combination."
|
||||
}
|
||||
|
||||
case .failure:
|
||||
self.error = Text("Network error. Try again later.")
|
||||
self.error = "Network error. Try again later."
|
||||
}
|
||||
|
||||
}
|
||||
@@ -120,11 +111,12 @@ struct SettingsFeedbinAccountView : View {
|
||||
}
|
||||
|
||||
private func dismiss() {
|
||||
isPresented?.value = false
|
||||
presentation.wrappedValue.dismiss()
|
||||
}
|
||||
|
||||
class ViewModel: BindableObject {
|
||||
let didChange = PassthroughSubject<ViewModel, Never>()
|
||||
class ViewModel: ObservableObject {
|
||||
|
||||
let objectWillChange = ObservableObjectPublisher()
|
||||
var account: Account? = nil
|
||||
|
||||
init() {
|
||||
@@ -139,13 +131,14 @@ struct SettingsFeedbinAccountView : View {
|
||||
}
|
||||
|
||||
var email: String = "" {
|
||||
didSet {
|
||||
didChange.send(self)
|
||||
willSet {
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
var password: String = "" {
|
||||
didSet {
|
||||
didChange.send(self)
|
||||
willSet {
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
import Account
|
||||
|
||||
struct SettingsLocalAccountView : View {
|
||||
@Environment(\.isPresented) private var isPresented
|
||||
@Environment(\.presentationMode) var presentation
|
||||
@State var name: String
|
||||
|
||||
var body: some View {
|
||||
@@ -20,9 +20,7 @@ struct SettingsLocalAccountView : View {
|
||||
SettingsAccountLabelView(accountImage: "accountLocal", accountLabel: Account.defaultLocalAccountName).padding()
|
||||
) {
|
||||
HStack {
|
||||
Text("Name")
|
||||
Divider()
|
||||
TextField($name, placeholder: Text("(Optional)"))
|
||||
TextField("Name", text: $name)
|
||||
}
|
||||
}
|
||||
Section {
|
||||
@@ -47,7 +45,7 @@ struct SettingsLocalAccountView : View {
|
||||
}
|
||||
|
||||
private func dismiss() {
|
||||
isPresented?.value = false
|
||||
presentation.wrappedValue.dismiss()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -29,108 +29,117 @@ struct SettingsView : View {
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Form {
|
||||
|
||||
// Section(header: Text("ACCOUNTS")) {
|
||||
// ForEach(viewModel.accounts.identified(by: \.self)) { account in
|
||||
// NavigationLink(destination: SettingsDetailAccountView(viewModel: SettingsDetailAccountView.ViewModel(account)), isDetail: false) {
|
||||
// Text(verbatim: account.nameForDisplay)
|
||||
// }
|
||||
// }
|
||||
// NavigationLink(destination: SettingsAddAccountView(), isDetail: false) {
|
||||
// Text("Add Account")
|
||||
// }
|
||||
// }
|
||||
|
||||
Section(header: Text("TIMELINE")) {
|
||||
Toggle(isOn: $viewModel.sortOldestToNewest) {
|
||||
Text("Sort Oldest to Newest")
|
||||
}
|
||||
Stepper(value: $viewModel.timelineNumberOfLines, in: 2...6) {
|
||||
Text("Number of Text Lines: \(viewModel.timelineNumberOfLines)")
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("DATABASE")) {
|
||||
Picker(selection: $viewModel.refreshInterval, label: Text("Refresh Interval")) {
|
||||
ForEach(RefreshInterval.allCases) { interval in
|
||||
Text(interval.description()).tag(interval)
|
||||
}
|
||||
}
|
||||
|
||||
VStack {
|
||||
Button("Import Subscriptions...") {
|
||||
self.isOPMLImportPresented = true
|
||||
}
|
||||
}.actionSheet(isPresented: $isOPMLImportPresented) {
|
||||
createSubscriptionsImportAccounts
|
||||
}.sheet(isPresented: $isOPMLImportDocPickerPresented) {
|
||||
SettingsSubscriptionsImportDocumentPickerView(account: self.opmlAccount!)
|
||||
}
|
||||
|
||||
VStack {
|
||||
Button("Export Subscriptions...") {
|
||||
self.isOPMLExportPresented = true
|
||||
}
|
||||
}.actionSheet(isPresented: $isOPMLExportPresented) {
|
||||
createSubscriptionsExportAccounts
|
||||
}.sheet(isPresented: $isOPMLExportDocPickerPresented) {
|
||||
SettingsSubscriptionsExportDocumentPickerView(account: self.opmlAccount!)
|
||||
}
|
||||
}
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Section(header: Text("ABOUT"), footer: buildFooter) {
|
||||
Text("About NetNewsWire")
|
||||
|
||||
Button(action: {
|
||||
self.isWebsitePresented.toggle()
|
||||
self.website = "https://ranchero.com/netnewswire/"
|
||||
}) {
|
||||
Text("Website")
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
self.isWebsitePresented.toggle()
|
||||
self.website = "https://github.com/brentsimmons/NetNewsWire"
|
||||
}) {
|
||||
Text("Github Repository")
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
self.isWebsitePresented.toggle()
|
||||
self.website = "https://github.com/brentsimmons/NetNewsWire/issues"
|
||||
}) {
|
||||
Text("Bug Tracker")
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
self.isWebsitePresented.toggle()
|
||||
self.website = "https://github.com/brentsimmons/NetNewsWire/tree/master/Technotes"
|
||||
}) {
|
||||
Text("Technotes")
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
self.isWebsitePresented.toggle()
|
||||
self.website = "https://github.com/brentsimmons/NetNewsWire/blob/master/Technotes/HowToSupportNetNewsWire.markdown"
|
||||
}) {
|
||||
Text("How To Support NetNewsWire")
|
||||
}
|
||||
|
||||
Text("Add NetNewsWire News Feed")
|
||||
|
||||
}.sheet(isPresented: $isWebsitePresented) {
|
||||
SafariView(url: URL(string: self.website!)!)
|
||||
}
|
||||
.foregroundColor(.primary)
|
||||
|
||||
buildAccountsSection()
|
||||
buildTimelineSection()
|
||||
buildDatabaseSection()
|
||||
buildAboutSection()
|
||||
}
|
||||
.navigationBarTitle(Text("Settings"), displayMode: .inline)
|
||||
.navigationBarItems(leading: Button(action: { self.viewController?.dismiss(animated: true) }) { Text("Done") } )
|
||||
}
|
||||
}
|
||||
|
||||
var createSubscriptionsImportAccounts: ActionSheet {
|
||||
func buildAccountsSection() -> some View {
|
||||
Section(header: Text("ACCOUNTS")) {
|
||||
ForEach(viewModel.accounts) { account in
|
||||
NavigationLink(destination: SettingsDetailAccountView(viewModel: SettingsDetailAccountView.ViewModel(account))) {
|
||||
Text(verbatim: account.nameForDisplay)
|
||||
}
|
||||
}
|
||||
NavigationLink(destination: SettingsAddAccountView()) {
|
||||
Text("Add Account")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildTimelineSection() -> some View {
|
||||
Section(header: Text("TIMELINE")) {
|
||||
Toggle(isOn: $viewModel.sortOldestToNewest) {
|
||||
Text("Sort Oldest to Newest")
|
||||
}
|
||||
Stepper(value: $viewModel.timelineNumberOfLines, in: 2...6) {
|
||||
Text("Number of Text Lines: \(viewModel.timelineNumberOfLines)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildDatabaseSection() -> some View {
|
||||
Section(header: Text("DATABASE")) {
|
||||
Picker(selection: $viewModel.refreshInterval, label: Text("Refresh Interval")) {
|
||||
ForEach(RefreshInterval.allCases) { interval in
|
||||
Text(interval.description()).tag(interval)
|
||||
}
|
||||
}
|
||||
|
||||
VStack {
|
||||
Button("Import Subscriptions...") {
|
||||
self.isOPMLImportPresented = true
|
||||
}
|
||||
}.actionSheet(isPresented: $isOPMLImportPresented) {
|
||||
buildSubscriptionsImportAccounts()
|
||||
}.sheet(isPresented: $isOPMLImportDocPickerPresented) {
|
||||
SettingsSubscriptionsImportDocumentPickerView(account: self.opmlAccount!)
|
||||
}.foregroundColor(.primary)
|
||||
|
||||
VStack {
|
||||
Button("Export Subscriptions...") {
|
||||
self.isOPMLExportPresented = true
|
||||
}
|
||||
}.actionSheet(isPresented: $isOPMLExportPresented) {
|
||||
buildSubscriptionsExportAccounts()
|
||||
}.sheet(isPresented: $isOPMLExportDocPickerPresented) {
|
||||
SettingsSubscriptionsExportDocumentPickerView(account: self.opmlAccount!)
|
||||
}.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
|
||||
func buildAboutSection() -> some View {
|
||||
Section(header: Text("ABOUT"), footer: buildFooter()) {
|
||||
Text("About NetNewsWire")
|
||||
|
||||
Button(action: {
|
||||
self.isWebsitePresented.toggle()
|
||||
self.website = "https://ranchero.com/netnewswire/"
|
||||
}) {
|
||||
Text("Website")
|
||||
}.foregroundColor(.primary)
|
||||
|
||||
Button(action: {
|
||||
self.isWebsitePresented.toggle()
|
||||
self.website = "https://github.com/brentsimmons/NetNewsWire"
|
||||
}) {
|
||||
Text("Github Repository")
|
||||
}.foregroundColor(.primary)
|
||||
|
||||
Button(action: {
|
||||
self.isWebsitePresented.toggle()
|
||||
self.website = "https://github.com/brentsimmons/NetNewsWire/issues"
|
||||
}) {
|
||||
Text("Bug Tracker")
|
||||
}.foregroundColor(.primary)
|
||||
|
||||
Button(action: {
|
||||
self.isWebsitePresented.toggle()
|
||||
self.website = "https://github.com/brentsimmons/NetNewsWire/tree/master/Technotes"
|
||||
}) {
|
||||
Text("Technotes")
|
||||
}.foregroundColor(.primary)
|
||||
|
||||
Button(action: {
|
||||
self.isWebsitePresented.toggle()
|
||||
self.website = "https://github.com/brentsimmons/NetNewsWire/blob/master/Technotes/HowToSupportNetNewsWire.markdown"
|
||||
}) {
|
||||
Text("How To Support NetNewsWire")
|
||||
}.foregroundColor(.primary)
|
||||
|
||||
Text("Add NetNewsWire News Feed")
|
||||
|
||||
}.sheet(isPresented: $isWebsitePresented) {
|
||||
SafariView(url: URL(string: self.website!)!)
|
||||
}
|
||||
}
|
||||
|
||||
func buildSubscriptionsImportAccounts() -> ActionSheet {
|
||||
var buttons = [ActionSheet.Button]()
|
||||
|
||||
for account in viewModel.activeAccounts {
|
||||
@@ -150,7 +159,7 @@ struct SettingsView : View {
|
||||
return ActionSheet(title: Text("Import Subscriptions..."), message: Text("Select the account to import your OPML file into."), buttons: buttons)
|
||||
}
|
||||
|
||||
var createSubscriptionsExportAccounts: ActionSheet {
|
||||
func buildSubscriptionsExportAccounts() -> ActionSheet {
|
||||
var buttons = [ActionSheet.Button]()
|
||||
|
||||
for account in viewModel.accounts {
|
||||
@@ -165,7 +174,7 @@ struct SettingsView : View {
|
||||
return ActionSheet(title: Text("Export Subscriptions..."), message: Text("Select the account to export out of."), buttons: buttons)
|
||||
}
|
||||
|
||||
var buildFooter: some View {
|
||||
func buildFooter() -> some View {
|
||||
return Text(verbatim: "\(Bundle.main.appName) v \(Bundle.main.versionNumber) (Build \(Bundle.main.buildNumber))")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Reference in New Issue
Block a user