From f42b6e5473a045247aa21ddd164cf31c227cb729 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Thu, 15 Dec 2022 09:30:28 +0800 Subject: [PATCH] This commit includes: - Localisation for new SwiftUI Settings Views - Inactive/Active Account sections in the Manage Account View - Early work to deprecate AddAccountViewController --- NetNewsWire.xcodeproj/project.pbxproj | 34 ++- iOS/Settings/AddAccountViewController.swift | 30 +-- iOS/Settings/Settings.strings | 102 +++++++++ .../AccountsManagementView.swift | 74 +++--- .../Accounts/AddAccountView.swift | 215 ++++++++++++++++++ .../AddAccountListView.swift | 33 --- .../AddExtensionListView.swift | 9 +- .../ExtensionsManagementView.swift | 22 +- .../Appearance/ColorPaletteSelectorView.swift | 6 +- .../Appearance/DisplayAndBehaviorsView.swift | 6 +- iOS/Settings/Views/General/SettingsRows.swift | 52 ++--- iOS/Settings/Views/General/SettingsView.swift | 50 ++-- iOS/Settings/Views/Help/AboutView.swift | 10 +- .../Views/Help/SettingsHelpSheets.swift | 4 +- .../NewArticleNotificationsView.swift | 3 +- 15 files changed, 461 insertions(+), 189 deletions(-) create mode 100644 iOS/Settings/Settings.strings rename iOS/Settings/Views/Account and Extensions/{ => Accounts}/AccountsManagementView.swift (58%) create mode 100644 iOS/Settings/Views/Account and Extensions/Accounts/AddAccountView.swift delete mode 100644 iOS/Settings/Views/Account and Extensions/AddAccountListView.swift rename iOS/Settings/Views/Account and Extensions/{ => Extensions}/AddExtensionListView.swift (82%) rename iOS/Settings/Views/Account and Extensions/{ => Extensions}/ExtensionsManagementView.swift (79%) diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index b35b41a75..6b3b7f8f4 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -851,18 +851,19 @@ DDF9E1D928EDF2FC000BC355 /* notificationSoundBlip.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = DDF9E1D628EDF2FC000BC355 /* notificationSoundBlip.mp3 */; }; DF32ABE829493193008E3A12 /* SettingsComboTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF32ABE629493192008E3A12 /* SettingsComboTableViewCell.swift */; }; DF32ABE929493193008E3A12 /* SettingsComboTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = DF32ABE729493193008E3A12 /* SettingsComboTableViewCell.xib */; }; + DF32ABEB29494CF1008E3A12 /* Settings.strings in Resources */ = {isa = PBXBuildFile; fileRef = DF32ABEA29494CF0008E3A12 /* Settings.strings */; }; DF3630EB2936183D00326FB8 /* OPMLDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF3630EA2936183D00326FB8 /* OPMLDocument.swift */; }; DF3630EC2936183D00326FB8 /* OPMLDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF3630EA2936183D00326FB8 /* OPMLDocument.swift */; }; DF3630ED2936183D00326FB8 /* OPMLDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF3630EA2936183D00326FB8 /* OPMLDocument.swift */; }; DF3630EF293618A900326FB8 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF3630EE293618A900326FB8 /* SettingsViewModel.swift */; }; DF394F0029357A180081EB6E /* NewArticleNotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF394EFF29357A180081EB6E /* NewArticleNotificationsView.swift */; }; - DF47CDB02947F77200FCD57E /* AddAccountListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF47CDAF2947F77200FCD57E /* AddAccountListView.swift */; }; DF47CDB2294803AB00FCD57E /* AddExtensionListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF47CDB1294803AB00FCD57E /* AddExtensionListView.swift */; }; DF59F072292085B800ACD33D /* ColorPaletteSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF59F071292085B800ACD33D /* ColorPaletteSelectorView.swift */; }; DF59F0742920DB5100ACD33D /* AccountsManagementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF59F0732920DB5100ACD33D /* AccountsManagementView.swift */; }; DF5AD10128D6922200CA3BF7 /* SmartFeedSummaryWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1768144D2564BCE000D98635 /* SmartFeedSummaryWidget.swift */; }; DF766FED29377FD9006FBBE2 /* ExtensionsManagementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF766FEC29377FD9006FBBE2 /* ExtensionsManagementView.swift */; }; DF790D6228E990A900455FC7 /* AboutData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF790D6128E990A900455FC7 /* AboutData.swift */; }; + DFB3497A294A962D00BC81AD /* AddAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFB34979294A962D00BC81AD /* AddAccountView.swift */; }; DFC14F0F28EA55BD00F6EE86 /* AboutWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFC14F0E28EA55BD00F6EE86 /* AboutWindowController.swift */; }; DFC14F1228EA5DC500F6EE86 /* AboutData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF790D6128E990A900455FC7 /* AboutData.swift */; }; DFC14F1328EA677C00F6EE86 /* Bundle-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BF42273625800C787DC /* Bundle-Extensions.swift */; }; @@ -1610,15 +1611,16 @@ DDF9E1D628EDF2FC000BC355 /* notificationSoundBlip.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = notificationSoundBlip.mp3; sourceTree = ""; }; DF32ABE629493192008E3A12 /* SettingsComboTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsComboTableViewCell.swift; sourceTree = ""; }; DF32ABE729493193008E3A12 /* SettingsComboTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SettingsComboTableViewCell.xib; sourceTree = ""; }; + DF32ABEA29494CF0008E3A12 /* Settings.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Settings.strings; sourceTree = ""; }; DF3630EA2936183D00326FB8 /* OPMLDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPMLDocument.swift; sourceTree = ""; }; DF3630EE293618A900326FB8 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; DF394EFF29357A180081EB6E /* NewArticleNotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewArticleNotificationsView.swift; sourceTree = ""; }; - DF47CDAF2947F77200FCD57E /* AddAccountListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccountListView.swift; sourceTree = ""; }; DF47CDB1294803AB00FCD57E /* AddExtensionListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddExtensionListView.swift; sourceTree = ""; }; DF59F071292085B800ACD33D /* ColorPaletteSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPaletteSelectorView.swift; sourceTree = ""; }; DF59F0732920DB5100ACD33D /* AccountsManagementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsManagementView.swift; sourceTree = ""; }; DF766FEC29377FD9006FBBE2 /* ExtensionsManagementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionsManagementView.swift; sourceTree = ""; }; DF790D6128E990A900455FC7 /* AboutData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutData.swift; sourceTree = ""; }; + DFB34979294A962D00BC81AD /* AddAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccountView.swift; sourceTree = ""; }; DFC14F0E28EA55BD00F6EE86 /* AboutWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutWindowController.swift; sourceTree = ""; }; DFC14F1428EB177000F6EE86 /* AboutNetNewsWireView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutNetNewsWireView.swift; sourceTree = ""; }; DFC14F1628EB17A800F6EE86 /* CreditsNetNewsWireView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditsNetNewsWireView.swift; sourceTree = ""; }; @@ -2018,6 +2020,7 @@ 5183CCEB227117C70010922C /* Settings */ = { isa = PBXGroup; children = ( + DF32ABEA29494CF0008E3A12 /* Settings.strings */, DFD406F8291FB5D500C02962 /* Views */, 51A16992235E10D600EB091F /* AddAccountViewController.swift */, 519ED455244828C3007F8E94 /* AddExtensionPointViewController.swift */, @@ -2909,10 +2912,8 @@ DF59F0752920E42000ACD33D /* Account and Extensions */ = { isa = PBXGroup; children = ( - DF59F0732920DB5100ACD33D /* AccountsManagementView.swift */, - DF47CDAF2947F77200FCD57E /* AddAccountListView.swift */, - DF766FEC29377FD9006FBBE2 /* ExtensionsManagementView.swift */, - DF47CDB1294803AB00FCD57E /* AddExtensionListView.swift */, + DFB3497B294AA95200BC81AD /* Accounts */, + DFB3497C294AA95A00BC81AD /* Extensions */, ); path = "Account and Extensions"; sourceTree = ""; @@ -2936,6 +2937,24 @@ path = General; sourceTree = ""; }; + DFB3497B294AA95200BC81AD /* Accounts */ = { + isa = PBXGroup; + children = ( + DFB34979294A962D00BC81AD /* AddAccountView.swift */, + DF59F0732920DB5100ACD33D /* AccountsManagementView.swift */, + ); + path = Accounts; + sourceTree = ""; + }; + DFB3497C294AA95A00BC81AD /* Extensions */ = { + isa = PBXGroup; + children = ( + DF766FEC29377FD9006FBBE2 /* ExtensionsManagementView.swift */, + DF47CDB1294803AB00FCD57E /* AddExtensionListView.swift */, + ); + path = Extensions; + sourceTree = ""; + }; DFC14F0928EA51AB00F6EE86 /* About */ = { isa = PBXGroup; children = ( @@ -3536,6 +3555,7 @@ 51077C5A27A86D16000C71DB /* Hyperlegible.nnwtheme in Resources */, 516A09422361248000EAE89B /* Inspector.storyboard in Resources */, DDF9E1D928EDF2FC000BC355 /* notificationSoundBlip.mp3 in Resources */, + DF32ABEB29494CF1008E3A12 /* Settings.strings in Resources */, 51DEE81A26FBFF84006DAA56 /* Promenade.nnwtheme in Resources */, 1768140B2564BB8300D98635 /* NetNewsWire_iOSwidgetextension_target.xcconfig in Resources */, 5103A9B424216A4200410853 /* blank.html in Resources */, @@ -4147,6 +4167,7 @@ 519ED47A24482AEB007F8E94 /* EnableExtensionPointViewController.swift in Sources */, 51C4528F226509BD00C03939 /* UnreadFeed.swift in Sources */, 51FD413B2342BD0500880194 /* MasterTimelineUnreadCountView.swift in Sources */, + DFB3497A294A962D00BC81AD /* AddAccountView.swift in Sources */, 513146B2235A81A400387FDC /* AddWebFeedIntentHandler.swift in Sources */, 51D87EE12311D34700E63F03 /* ActivityType.swift in Sources */, 51C452772265091600C03939 /* MultilineUILabelSizer.swift in Sources */, @@ -4259,7 +4280,6 @@ 5108F6D823763094001ABC45 /* TickMarkSlider.swift in Sources */, DF3630EF293618A900326FB8 /* SettingsViewModel.swift in Sources */, 51C452882265093600C03939 /* AddFeedViewController.swift in Sources */, - DF47CDB02947F77200FCD57E /* AddAccountListView.swift in Sources */, 51B5C8C023F3866C00032075 /* ExtensionFeedAddRequestFile.swift in Sources */, 51A169A0235E10D700EB091F /* FeedbinAccountViewController.swift in Sources */, 51934CCE2310792F006127BE /* ActivityManager.swift in Sources */, diff --git a/iOS/Settings/AddAccountViewController.swift b/iOS/Settings/AddAccountViewController.swift index f53953b8f..75a15b4ac 100644 --- a/iOS/Settings/AddAccountViewController.swift +++ b/iOS/Settings/AddAccountViewController.swift @@ -11,39 +11,11 @@ import UIKit import SwiftUI import RSCore -struct AddAccountViewControllerRepresentable: UIViewControllerRepresentable { - func makeUIViewController(context: Context) -> AddAccountViewController { - let storyboard = UIStoryboard(name: "Settings", bundle: .main) - let controller = storyboard.instantiateViewController(withIdentifier: "AddAccountViewController") as! AddAccountViewController - - context.coordinator.parentObserver = controller.observe(\.parent, changeHandler: { vc, _ in - vc.parent?.title = vc.title - vc.parent?.navigationItem.rightBarButtonItems = vc.navigationItem.rightBarButtonItems - }) - - - return controller - } - - func updateUIViewController(_ uiViewController: AddAccountViewController, context: Context) { - // - } - - typealias UIViewControllerType = AddAccountViewController - - class Coordinator { - var parentObserver: NSKeyValueObservation? - } - - func makeCoordinator() -> Self.Coordinator { Coordinator() } - -} - - protocol AddAccountDismissDelegate: UIViewController { func dismiss() } +@available(*, deprecated, message: "Use AddAccountView") class AddAccountViewController: UITableViewController, AddAccountDismissDelegate { private enum AddAccountSections: Int, CaseIterable { diff --git a/iOS/Settings/Settings.strings b/iOS/Settings/Settings.strings new file mode 100644 index 000000000..b87334880 --- /dev/null +++ b/iOS/Settings/Settings.strings @@ -0,0 +1,102 @@ +/* + Settings.strings + NetNewsWire + + Created by Stuart Breckenridge on 14/12/2022. + Copyright © 2022 Ranchero Software. All rights reserved. +*/ + +/* Settings */ + +"SETTINGS_TITLE" = "Settings"; +"DEVICE_PERMISSIONS_HEADER" = "Device Permissions"; +"DEVICE_PERMISSIONS_FOOTER" = "Configure NetNewsWire's access to Siri, background app refresh, mobile data, and more."; +"ACCOUNTS_EXTENSIONS_HEADER" = "Accounts & Extensions"; +"ACCOUNTS_EXTENSIONS_FOOTER" = "Add, delete, enable, or disable accounts and extensions."; +"APPEARANCE_HEADER" = "Appearance"; +"APPEARANCE_FOOTER" = "Manage the look, feel, and behavior of NetNewsWire."; +"DISPLAY_BEHAVIORS_HEADER" = "Display & Behaviors"; + +/* Settings Rows */ + +"OPEN_SYSTEM_SETTINGS" = "Open System Settings"; +"NEW_ARTICLE_NOTIFICATIONS" = "New Article Notifications"; +"MANAGE_ACCOUNTS" = "Manage Accounts"; +"MANAGE_EXTENSIONS" = "Manage Extensions"; +"IMPORT_SUBSCRIPTIONS" = "Import Subscriptions"; +"EXPORT_SUBSCRIPTIONS" = "Export Subscriptions"; +"ABOUT" = "About NetNewsWire"; +"NETNEWSWIRE_HELP" = "NetNewsWire Help Guide"; +"NETNEWSWIRE_WEBSITE" = "NewNewsWire Website"; + +/* Display & Behaviors */ + +"APPLICATION_HEADER" = "Application"; +"TIMELINE_HEADER" = "Timeline"; +"ARTICLE_HEADER" = "Article"; +"ALWAYS_DARK_MODE" = "Always Dark"; +"ALWAYS_LIGHT_MODE" = "Always Light"; +"USE_SYSTEM_DISPLAY_MODE" = "Use System"; +"SORT_OLDEST_NEWEST" = "Sort Oldest to Newest"; +"GROUP_BY_FEED" = "Group by Feed"; +"REFRESH_TO_CLEAR_READ_ARTICLES" = "Refresh to Clear Articles"; +"TIMELINE_LAYOUT" = "Timeline Layout"; +"ARTICLE_THEME" = "Article Theme"; +"CONFIRM_MARK_ALL_AS_READ" = "Confirm Mark All as Read"; +"OPEN_LINKS_IN_APP" = "Open Links in NetNewsWire"; + +/* Account Management */ + +"ADD_ACCOUNT" = "Add Account"; +"NO_INACTIVE_ACCOUNT_FOOTER" = "There are no inactive accounts."; +"ACCOUNT_REMOVE %@" = "Are you sure you want to remove “%@”?"; +"REMOVE_FEEDLY_CONFIRMATION" = "Are you sure you want to remove this account? NetNewsWire will no longer be able to access articles and feeds unless the account is added again."; +"REMOVE_ACCOUNT_CONFIRMATION" = "Are you sure you want to remove this account? This cannot be undone."; +"ACTIVE_ACCOUNTS_HEADER" = "Active Accounts"; +"INACTIVE_ACCOUNTS_HEADER" = "Inactive Accounts"; + +/* Extension Management */ + +"DEACTIVATE" = "Deactivate"; +"DEACTIVATE_EXTENSION_TITLE" = "Deactivate Extension"; +"DEACTIVATE_EXTENSION %@" = "Deactivate “%@”"; +"ACTIVE_EXTENSIONS" = "Active Extensions"; +"FEED_PROVIDER_HEADER" = "Feed Provider"; +"FEED_PROVIDER_FOOTER" = "Feed Providers allow you to subscribe to some pages as if they were RSS Feeds."; +"ADD_EXTENSIONS_TITLE" = "Add Extension"; + +/* New Article Notifications */ + +"NEW_ARTICLE_NOTIFICATIONS_TITLE" = "New Article Notifications"; + +/* About */ + +"ABOUT_TITLE" = "About"; +"PRIMARY_CONTRIBUTORS" = "Primary Contributors"; +"ADDITIONAL_CONTRIBUTORS" = "Additional Contributors"; +"THANKS" = "Thanks"; +"BYLINE" = "By Brent Simmons and the Ranchero Software team."; + +/* Add Account */ +"ADD_LOCAL_ACCOUNT_HEADER" = "Local"; +"ADD_LOCAL_ACCOUNT_FOOTER" = "Local accounts do not sync your feeds across devices"; +"ADD_CLOUDKIT_ACCOUNT_HEADER" = "iCloud"; +"ADD_CLOUDKIT_ACCOUNT_FOOTER" = "Your iCloud account syncs your feeds across your Mac and iOS devices"; +"ADD_WEB_ACCOUNT_HEADER" = "Web"; +"ADD_WEB_ACCOUNT_FOOTER" = "Web accounts sync your feeds across all your devices"; +"ADD_SELFHOSTED_ACCOUNT_HEADER" = "Self-hosted"; +"ADD_SELFHOSTED_ACCOUNT_FOOTER" = "Self-hosted accounts sync your feeds across all your devices"; + + +/* Alerts */ + +"IMPORT_OPML_CONFIRMATION" = "Choose an account to receive the imported feeds and folders"; +"IMPORT_OPML_SUCCESS_TITLE" = "Imported Successfully"; +"IMPORT_OPML_SUCCESS_MESSAGE %@" = "Subscriptions have been imported to your %@ account."; +"EXPORT_OPML_CONFIRMATION" = "Choose an account with the subscriptions to export"; +"EXPORT_OPML_SUCCESS_TITLE" = "Exported Successfully"; +"EXPORT_OPML_SUCCESS_MESSAGE" = "Your OPML file has been successfully exported."; +"ERROR_TITLE" = "Error"; +"REMOVE" = "Remove"; +"CANCEL" = "Cancel"; +"DONE" = "Done"; diff --git a/iOS/Settings/Views/Account and Extensions/AccountsManagementView.swift b/iOS/Settings/Views/Account and Extensions/Accounts/AccountsManagementView.swift similarity index 58% rename from iOS/Settings/Views/Account and Extensions/AccountsManagementView.swift rename to iOS/Settings/Views/Account and Extensions/Accounts/AccountsManagementView.swift index 190e79f19..497b9f7a3 100644 --- a/iOS/Settings/Views/Account and Extensions/AccountsManagementView.swift +++ b/iOS/Settings/Views/Account and Extensions/Accounts/AccountsManagementView.swift @@ -10,50 +10,30 @@ import SwiftUI import Account import Combine -struct AddAccountWrapper: UIViewControllerRepresentable { - func makeUIViewController(context: Context) -> AddAccountViewController { - let controller = UIStoryboard.settings.instantiateViewController(withIdentifier: "AddAccountViewController") as! AddAccountViewController - - - context.coordinator.parentObserver = controller.observe(\.parent, changeHandler: { vc, _ in - vc.parent?.title = vc.title - vc.parent?.navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .done, primaryAction: UIAction(title: NSLocalizedString("Done", comment: "Done"), image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { _ in - controller.dismiss(animated: true ) - }), menu: nil) - }) - - return controller - } - - func updateUIViewController(_ uiViewController: AddAccountViewController, context: Context) { - // - } - - class Coordinator { - var parentObserver: NSKeyValueObservation? - } - - func makeCoordinator() -> Self.Coordinator { Coordinator() } - - typealias UIViewControllerType = AddAccountViewController - -} - struct AccountsManagementView: View { @State private var showAddAccountSheet: Bool = false - var cancellables = Set() - @State private var sortedAccounts = [Account]() + @State private var sortedActiveAccounts = [Account]() + @State private var sortedInactiveAccounts = [Account]() @State private var accountToRemove: Account? @State private var showRemoveAccountAlert: Bool = false var body: some View { List { - ForEach(sortedAccounts, id: \.self) { account in - accountRow(account, showRemoveAccountAlert: $showRemoveAccountAlert, accountToRemove: $accountToRemove) + Section(header: Text("ACTIVE_ACCOUNTS_HEADER", tableName: "Settings")) { + ForEach(sortedActiveAccounts, id: \.self) { account in + accountRow(account, showRemoveAccountAlert: $showRemoveAccountAlert, accountToRemove: $accountToRemove) + } } + + Section(header: Text("INACTIVE_ACCOUNTS_HEADER", tableName: "Settings")) { + ForEach(sortedInactiveAccounts, id: \.self) { account in + accountRow(account, showRemoveAccountAlert: $showRemoveAccountAlert, accountToRemove: $accountToRemove) + } + } + } - .navigationTitle(Text("Manage Accounts")) + .navigationTitle(Text("MANAGE_ACCOUNTS", tableName: "Settings")) .tint(Color(uiColor: AppAssets.primaryAccentColor)) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { @@ -80,20 +60,20 @@ struct AccountsManagementView: View { refreshAccounts() } .sheet(isPresented: $showAddAccountSheet) { - AddAccountListView() + AddAccountView() } - .alert("Remove “\(accountToRemove?.nameForDisplay ?? "")”?", + .alert(Text("ACCOUNT_REMOVE \(accountToRemove?.nameForDisplay ?? "")", tableName: "Settings"), isPresented: $showRemoveAccountAlert) { Button(role: .destructive) { AccountManager.shared.deleteAccount(accountToRemove!) } label: { - Text("Remove") + Text("REMOVE", tableName: "Settings") } Button(role: .cancel) { accountToRemove = nil } label: { - Text("Cancel") + Text("CANCEL", tableName: "Settings") } } message: { switch accountToRemove { @@ -102,18 +82,17 @@ struct AccountsManagementView: View { case .some(let wrapped): switch wrapped.type { case .feedly: - Text("Are you sure you want to remove this account? NetNewsWire will no longer be able to access articles and feeds unless the account is added again.") + Text("REMOVE_FEEDLY_CONFIRMATION", tableName: "Settings") default: - Text("Are you sure you want to remove this account? This cannot be undone.") + Text("REMOVE_ACCOUNT_CONFIRMATION", tableName: "Settings") } } } - } func refreshAccounts() { - sortedAccounts = [] - sortedAccounts = AccountManager.shared.sortedAccounts + sortedActiveAccounts = AccountManager.shared.sortedActiveAccounts + sortedInactiveAccounts = AccountManager.shared.sortedAccounts.filter({ $0.isActive == false }) } func accountRow(_ account: Account, showRemoveAccountAlert: Binding, accountToRemove: Binding) -> some View { @@ -123,6 +102,7 @@ struct AccountsManagementView: View { } label: { Image(uiImage: account.smallIcon!.image) .resizable() + .aspectRatio(contentMode: .fit) .frame(width: 25, height: 25) Text(account.nameForDisplay) }.swipeActions(edge: .trailing, allowsFullSwipe: false) { @@ -131,7 +111,11 @@ struct AccountsManagementView: View { accountToRemove.wrappedValue = account showRemoveAccountAlert.wrappedValue = true } label: { - Label("Remove Account", systemImage: "trash") + Label { + Text("REMOVE_ACCOUNT", tableName: "Settings") + } icon: { + Image(systemName: "trash") + } }.tint(.red) } } @@ -139,7 +123,7 @@ struct AccountsManagementView: View { var inactiveFooterText: some View { if AccountManager.shared.sortedAccounts.filter({ $0.isActive == false }).count == 0 { - return Text("There are no inactive accounts.") + return Text("NO_INACTIVE_ACCOUNT_FOOTER", tableName: "Settings") } else { return Text("") } diff --git a/iOS/Settings/Views/Account and Extensions/Accounts/AddAccountView.swift b/iOS/Settings/Views/Account and Extensions/Accounts/AddAccountView.swift new file mode 100644 index 000000000..dcaa6384d --- /dev/null +++ b/iOS/Settings/Views/Account and Extensions/Accounts/AddAccountView.swift @@ -0,0 +1,215 @@ +// +// AddAccountView.swift +// NetNewsWire-iOS +// +// Created by Stuart Breckenridge on 15/12/2022. +// Copyright © 2022 Ranchero Software. All rights reserved. +// + +import SwiftUI +import Account +import RSCore + +public final class AddAcccountViewModel: ObservableObject, OAuthAccountAuthorizationOperationDelegate { + + @Published public var showAddAccountSheet: (Bool, accountType: AccountType) = (false, .onMyMac) + public var webAccountTypes: [AccountType] { + if AppDefaults.shared.isDeveloperBuild { + return [.bazQux, .feedbin, .feedly, .inoreader, .newsBlur, .theOldReader].filter({ $0.isDeveloperRestricted == false }) + } else { + return [.bazQux, .feedbin, .feedly, .inoreader, .newsBlur, .theOldReader] + } + } + + public var rootViewController: UIViewController? { + var currentKeyWindow: UIWindow? { + UIApplication.shared.connectedScenes + .filter { $0.activationState == .foregroundActive } + .map { $0 as? UIWindowScene } + .compactMap { $0 } + .first?.windows + .filter { $0.isKeyWindow } + .first + } + + var rootViewController: UIViewController? { + currentKeyWindow?.rootViewController + } + + return rootViewController + } + + public func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didCreate account: Account) { + account.refreshAll { [weak self] result in + switch result { + case .success: + break + case .failure(let error): + guard let viewController = self?.rootViewController else { + return + } + viewController.presentError(error) + } + } + } + + public func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didFailWith error: Error) { + //presentError(error) + } +} + +struct AddAccountView: View { + + @Environment(\.dismiss) var dismiss + @StateObject private var viewModel = AddAcccountViewModel() + + + var body: some View { + NavigationView { + List { + localAccountSection + cloudKitSection + webAccountSection + selfHostedSection + } + .navigationTitle(Text("ADD_ACCOUNT", tableName: "Settings")) + .navigationBarTitleDisplayMode(.inline) + .listItemTint(.primary) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button(role: .cancel) { + dismiss() + } label: { + Text("DONE", tableName: "Settings") + } + } + } + .sheet(isPresented: $viewModel.showAddAccountSheet.0) { + switch viewModel.showAddAccountSheet.accountType { + case .onMyMac: + Text("ON MY MAC") + case .cloudKit: + Text("CLOUDKIT") + case .freshRSS: + Text("SELF_HOSTED") + default: + Text(viewModel.showAddAccountSheet.accountType.localizedAccountName()) + } + } + .onReceive(NotificationCenter.default.publisher(for: .UserDidAddAccount)) { _ in + dismiss() + } + } + } + + var localAccountSection: some View { + Section { + Button { + viewModel.showAddAccountSheet = (true, .onMyMac) + } label: { + Label { + Text(AccountType.onMyMac.localizedAccountName()) + .foregroundColor(.primary) + } icon: { + Image(uiImage: AppAssets.image(for: .onMyMac)!) + .resizable() + .frame(width: 30, height: 30) + } + } + } header: { + Text("ADD_LOCAL_ACCOUNT_HEADER", tableName: "Settings") + } footer: { + Text("ADD_LOCAL_ACCOUNT_FOOTER", tableName: "Settings") + } + } + + var cloudKitSection: some View { + Section { + Button { + viewModel.showAddAccountSheet = (true, .cloudKit) + } label: { + Label { + Text(AccountType.cloudKit.localizedAccountName()) + .foregroundColor(interactionDisabled(for: .cloudKit) ? .secondary : .primary) + } icon: { + Image(uiImage: AppAssets.image(for: .cloudKit)!) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 30, height: 30) + } + } + .disabled(interactionDisabled(for: .cloudKit)) + } header: { + Text("ADD_CLOUDKIT_ACCOUNT_HEADER", tableName: "Settings") + } footer: { + Text("ADD_CLOUDKIT_ACCOUNT_FOOTER", tableName: "Settings") + } + } + + var webAccountSection: some View { + Section { + ForEach(viewModel.webAccountTypes, id: \.self) { webAccount in + Button { + if webAccount == .feedly { + let addAccount = OAuthAccountAuthorizationOperation(accountType: .feedly) + addAccount.delegate = viewModel + addAccount.presentationAnchor = viewModel.rootViewController?.view.window + MainThreadOperationQueue.shared.add(addAccount) + } else { + viewModel.showAddAccountSheet = (true, webAccount) + } + + } label: { + Label { + Text(webAccount.localizedAccountName()) + .foregroundColor(.primary) + } icon: { + Image(uiImage: AppAssets.image(for: webAccount)!) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 30, height: 30) + } + } + } + } header: { + Text("ADD_WEB_ACCOUNT_HEADER", tableName: "Settings") + } footer: { + Text("ADD_WEB_ACCOUNT_FOOTER", tableName: "Settings") + } + } + + var selfHostedSection: some View { + Section { + Button { + viewModel.showAddAccountSheet = (true, .freshRSS) + } label: { + Label { + Text(AccountType.freshRSS.localizedAccountName()) + .foregroundColor(.primary) + } icon: { + Image(uiImage: AppAssets.image(for: .freshRSS)!) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 30, height: 30) + } + } + } header: { + Text("ADD_SELFHOSTED_ACCOUNT_HEADER", tableName: "Settings") + } footer: { + Text("ADD_SELFHOSTED_ACCOUNT_FOOTER", tableName: "Settings") + } + } + + + private func interactionDisabled(for accountType: AccountType) -> Bool { + if accountType == .cloudKit { + if AccountManager.shared.accounts.contains(where: { $0.type == .cloudKit }) { + return true + } + return AppDefaults.shared.isDeveloperBuild + } + + return accountType.isDeveloperRestricted + } + +} diff --git a/iOS/Settings/Views/Account and Extensions/AddAccountListView.swift b/iOS/Settings/Views/Account and Extensions/AddAccountListView.swift deleted file mode 100644 index 6deb7af01..000000000 --- a/iOS/Settings/Views/Account and Extensions/AddAccountListView.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// AddAccountListView.swift -// NetNewsWire-iOS -// -// Created by Stuart Breckenridge on 13/12/2022. -// Copyright © 2022 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account - -struct AddAccountListView: View { - - @Environment(\.dismiss) var dismiss - - var body: some View { - NavigationView { - AddAccountWrapper() - .navigationTitle("Add Account") - .navigationBarTitleDisplayMode(.inline) - .edgesIgnoringSafeArea(.all) - } - .onReceive(NotificationCenter.default.publisher(for: .UserDidAddAccount)) { _ in - dismiss() - } - } -} - -struct AddAccountListView_Previews: PreviewProvider { - static var previews: some View { - AddAccountListView() - } -} diff --git a/iOS/Settings/Views/Account and Extensions/AddExtensionListView.swift b/iOS/Settings/Views/Account and Extensions/Extensions/AddExtensionListView.swift similarity index 82% rename from iOS/Settings/Views/Account and Extensions/AddExtensionListView.swift rename to iOS/Settings/Views/Account and Extensions/Extensions/AddExtensionListView.swift index 7bc9e7731..a67aa73dd 100644 --- a/iOS/Settings/Views/Account and Extensions/AddExtensionListView.swift +++ b/iOS/Settings/Views/Account and Extensions/Extensions/AddExtensionListView.swift @@ -17,7 +17,8 @@ struct AddExtensionListView: View { var body: some View { NavigationView { List { - Section(header: Text("Feed Provider"),footer: Text("Feed Providers allow you to subscribe to some pages as if they were RSS Feeds.")) { + Section(header: Text("FEED_PROVIDER_HEADER", tableName: "Settings"), + footer: Text("FEED_PROVIDER_FOOTER", tableName: "Settings")) { ForEach(0..( diff --git a/iOS/Settings/Views/General/SettingsRows.swift b/iOS/Settings/Views/General/SettingsRows.swift index a0f4c73f3..a6e0d2685 100644 --- a/iOS/Settings/Views/General/SettingsRows.swift +++ b/iOS/Settings/Views/General/SettingsRows.swift @@ -18,7 +18,7 @@ struct SettingsViewRows { /// This row, when tapped, will open iOS System Settings. static var openSystemSettings: some View { Label { - Text("Open System Settings") + Text("OPEN_SYSTEM_SETTINGS", tableName: "Settings") } icon: { Image("system.settings") .resizable() @@ -36,7 +36,7 @@ struct SettingsViewRows { static var configureNewArticleNotifications: some View { NavigationLink(destination: NewArticleNotificationsView()) { Label { - Text("New Article Notifications") + Text("NEW_ARTICLE_NOTIFICATIONS", tableName: "Settings") } icon: { Image("notifications.sounds") .resizable() @@ -51,7 +51,7 @@ struct SettingsViewRows { static var addAccount: some View { NavigationLink(destination: AccountsManagementView()) { Label { - Text("Manage Accounts") + Text("MANAGE_ACCOUNTS", tableName: "Settings") } icon: { Image("app.account") .resizable() @@ -66,7 +66,7 @@ struct SettingsViewRows { static var manageExtensions: some View { NavigationLink(destination: ExtensionsManagementView()) { Label { - Text("Manage Extensions") + Text("MANAGE_EXTENSIONS", tableName: "Settings") } icon: { Image("app.extension") .resizable() @@ -83,7 +83,7 @@ struct SettingsViewRows { showImportActionSheet.wrappedValue.toggle() } label: { Label { - Text("Import Subscriptions") + Text("IMPORT_SUBSCRIPTIONS", tableName: "Settings") .foregroundColor(.primary) } icon: { @@ -102,7 +102,7 @@ struct SettingsViewRows { showExportActionSheet.wrappedValue.toggle() } label: { Label { - Text("Export Subscriptions") + Text("EXPORT_SUBSCRIPTIONS", tableName: "Settings") .foregroundColor(.primary) } icon: { @@ -118,28 +118,34 @@ struct SettingsViewRows { /// - Parameter preference: `Binding` /// - Returns: `Toggle` static func sortOldestToNewest(_ preference: Binding) -> some View { - Toggle("Sort Oldest to Newest", isOn: preference) + Toggle(isOn: preference) { + Text("SORT_OLDEST_NEWEST", tableName: "Settings") + } } /// Returns a `Toggle` which triggers changes to the user's grouping preference. /// - Parameter preference: `Binding` /// - Returns: `Toggle` static func groupByFeed(_ preference: Binding) -> some View { - Toggle("Group by Feed", isOn: preference) + Toggle(isOn: preference) { + Text("GROUP_BY_FEED", tableName: "Settings") + } } /// Returns a `Toggle` which triggers changes to the user's refresh to clear preferences. /// - Parameter preference: `Binding` /// - Returns: `Toggle` static func refreshToClearReadArticles(_ preference: Binding) -> some View { - Toggle("Refresh To Clear Read Articles", isOn: preference) + Toggle(isOn: preference) { + Text("REFRESH_TO_CLEAR_READ_ARTICLES", tableName: "Settings") + } } /// This row, when tapped, will push the the Timeline Layout screen /// in to view. static var timelineLayout: some View { - NavigationLink(destination: TimelineCustomizerWrapper().edgesIgnoringSafeArea(.all).navigationTitle(Text("Timeline Layout"))) { - Text("Timeline Layout") + NavigationLink(destination: TimelineCustomizerWrapper().edgesIgnoringSafeArea(.all).navigationTitle(Text("TIMELINE_LAYOUT", tableName: "Settings"))) { + Text("TIMELINE_LAYOUT", tableName: "Settings") } } @@ -148,7 +154,7 @@ struct SettingsViewRows { static var themeSelection: some View { NavigationLink(destination: ArticleThemesWrapper().edgesIgnoringSafeArea(.all)) { HStack { - Text("Article Theme") + Text("ARTICLE_THEME", tableName: "Settings") Spacer() Text(ArticleThemesManager.shared.currentTheme.name) .font(.callout) @@ -158,31 +164,25 @@ struct SettingsViewRows { } static func confirmMarkAllAsRead(_ preference: Binding) -> some View { - Toggle("Confirm Mark All as Read", isOn: preference) + Toggle(isOn: preference) { + Text("CONFIRM_MARK_ALL_AS_READ", tableName: "Settings") + } } static func openLinksInNetNewsWire(_ preference: Binding) -> some View { - Toggle("Open Links in NetNewsWire", isOn: preference) + Toggle(isOn: preference) { + Text("OPEN_LINKS_IN_APP", tableName: "Settings") + } } // TODO: Add Reader Mode Defaults here. See #3684. - static func enableFullScreenArticles(_ preference: Binding) -> some View { - Toggle(isOn: preference) { - VStack(alignment: .leading, spacing: 4) { - Text("Enable Full Screen Articles") - Text("Tap the article top bar to enter Full Screen. Tap the top or bottom to exit.") - .font(.caption) - .foregroundColor(.gray) - } - } - } /// This row, when tapped, will push the New Article Notifications /// screen in to view. static func configureAppearance(_ isShown: Binding) -> some View { NavigationLink(destination: DisplayAndBehaviorsView(), isActive: isShown) { Label { - Text("Display & Behaviors") + Text("DISPLAY_BEHAVIORS_HEADER", tableName: "Settings") } icon: { Image("app.appearance") .resizable() @@ -220,7 +220,7 @@ struct SettingsViewRows { AboutView() } label: { Label { - Text("About NetNewsWire") + Text("ABOUT", tableName: "Settings") } icon: { Image(systemName: "info.circle") .resizable() diff --git a/iOS/Settings/Views/General/SettingsView.swift b/iOS/Settings/Views/General/SettingsView.swift index 9ba7a8e69..62807d874 100644 --- a/iOS/Settings/Views/General/SettingsView.swift +++ b/iOS/Settings/Views/General/SettingsView.swift @@ -22,18 +22,21 @@ struct SettingsView: View { var body: some View { NavigationView { List { - // Device Permissions - Section(header: Text("Device Permissions"), footer: Text("Configure NetNewsWire's access to Siri, background app refresh, mobile data, and more.")) { + Section(header: Text("DEVICE_PERMISSIONS_HEADER", tableName: "Settings"), + footer: Text("DEVICE_PERMISSIONS_FOOTER", tableName: "Settings")) { SettingsViewRows.openSystemSettings } // Account/Extensions/OPML Management - Section(header: Text("Accounts & Extensions"), footer: Text("Add, delete, enable, or disable accounts and extensions.")) { + Section(header: Text("ACCOUNTS_EXTENSIONS_HEADER", tableName: "Settings"), + footer: Text("ACCOUNTS_EXTENSIONS_FOOTER", tableName: "Settings")) { SettingsViewRows.addAccount SettingsViewRows.manageExtensions SettingsViewRows.importOPML(showImportActionSheet: $viewModel.showImportActionSheet) - .confirmationDialog("Choose an account to receive the imported feeds and folders", isPresented: $viewModel.showImportActionSheet, titleVisibility: .visible) { + .confirmationDialog(Text("IMPORT_OPML_CONFIRMATION", tableName: "Settings"), + isPresented: $viewModel.showImportActionSheet, + titleVisibility: .visible) { ForEach(AccountManager.shared.sortedActiveAccounts, id: \.self) { account in Button(account.nameForDisplay) { viewModel.importAccount = account @@ -41,8 +44,11 @@ struct SettingsView: View { } } } + SettingsViewRows.exportOPML(showExportActionSheet: $viewModel.showExportActionSheet) - .confirmationDialog("Choose an account with the subscriptions to export", isPresented: $viewModel.showExportActionSheet, titleVisibility: .visible) { + .confirmationDialog(Text("EXPORT_OPML_CONFIRMATION", tableName: "Settings"), + isPresented: $viewModel.showExportActionSheet, + titleVisibility: .visible) { ForEach(AccountManager.shared.sortedAccounts, id: \.self) { account in Button(account.nameForDisplay) { do { @@ -59,7 +65,8 @@ struct SettingsView: View { } // Appearance - Section(header: Text("Appearance"), footer: Text("Manage the look, feel, and behavior of NetNewsWire.")) { + Section(header: Text("APPEARANCE_HEADER", tableName: "Settings"), + footer: Text("APPEARANCE_FOOTER", tableName: "Settings")) { SettingsViewRows.configureAppearance($isConfigureAppearanceShown) if viewModel.notificationPermissions == .authorized { SettingsViewRows.configureNewArticleNotifications @@ -76,10 +83,10 @@ struct SettingsView: View { } .tint(Color(uiColor: AppAssets.primaryAccentColor)) .listStyle(.insetGrouped) - .navigationTitle(Text("Settings")) + .navigationTitle(Text("SETTINGS_TITLE", tableName: "Settings")) .navigationBarTitleDisplayMode(.inline) .sheet(isPresented: $viewModel.showAddAccountView) { - AddAccountViewControllerRepresentable().edgesIgnoringSafeArea(.all) + AddAccountView() } .sheet(isPresented: $viewModel.showHelpSheet) { SafariView(url: viewModel.helpSheet.url) @@ -130,21 +137,18 @@ struct SettingsView: View { viewModel.showImportExportError = true } }) - .alert("Imported Successfully", isPresented: $viewModel.showImportSuccess) { - Button("Dismiss") {} - } message: { - Text("Import to your \(viewModel.importAccount?.nameForDisplay ?? "") account has completed.") - } - .alert("Exported Successfully", isPresented: $viewModel.showExportSuccess) { - Button("Dismiss") {} - } message: { - Text("Your OPML file has been successfully exported.") - } - .alert("Error", isPresented: $viewModel.showImportExportError) { - Button("Dismiss") {} - } message: { - Text(viewModel.importExportError?.localizedDescription ?? "Import/Export Error") - } + .alert(Text("IMPORT_OPML_SUCCESS_TITLE", tableName: "Settings"), + isPresented: $viewModel.showImportSuccess, + actions: {}, + message: { Text("IMPORT_OPML_SUCCESS_MESSAGE \(viewModel.importAccount?.nameForDisplay ?? "")", tableName: "Settings") }) + .alert(Text("EXPORT_OPML_SUCCESS_TITLE", tableName: "Settings"), + isPresented: $viewModel.showExportSuccess, + actions: {}, + message: { Text("EXPORT_OPML_SUCCESS_MESSAGE", tableName: "Settings") }) + .alert(Text("ERROR_TITLE", tableName: "Settings"), + isPresented: $viewModel.showImportExportError, + actions: {}, + message: { Text(viewModel.importExportError?.localizedDescription ?? "Import/Export Error") } ) }.navigationViewStyle(.stack) } } diff --git a/iOS/Settings/Views/Help/AboutView.swift b/iOS/Settings/Views/Help/AboutView.swift index 70cd411e2..1190e5366 100644 --- a/iOS/Settings/Views/Help/AboutView.swift +++ b/iOS/Settings/Views/Help/AboutView.swift @@ -13,21 +13,21 @@ struct AboutView: View, LoadableAboutData { var body: some View { List { Section(header: aboutHeaderView) {} - Section(header: Text("Primary Contributors")) { + Section(header: Text("PRIMARY_CONTRIBUTORS", tableName: "Settings")) { ForEach(0..