diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 1410f5c0a..244803bd7 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -41,7 +41,6 @@ 176814652564BD7F00D98635 /* WidgetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176813B62564B9F800D98635 /* WidgetData.swift */; }; 1768146C2564BD8100D98635 /* WidgetDeepLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176813D82564BA8700D98635 /* WidgetDeepLinks.swift */; }; 1768147B2564BE5400D98635 /* widget-sample.json in Resources */ = {isa = PBXBuildFile; fileRef = 1768147A2564BE5400D98635 /* widget-sample.json */; }; - 177A0C2D25454AAB00D7EAF6 /* ReaderAPIAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 177A0C2C25454AAB00D7EAF6 /* ReaderAPIAccountViewController.swift */; }; 178A9F9D2549449F00AB7E9D /* AddAccountsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 178A9F9C2549449F00AB7E9D /* AddAccountsView.swift */; }; 178A9F9E2549449F00AB7E9D /* AddAccountsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 178A9F9C2549449F00AB7E9D /* AddAccountsView.swift */; }; 179C39EA26F76B0500D4E741 /* Zip in Frameworks */ = {isa = PBXBuildFile; productRef = 179C39E926F76B0500D4E741 /* Zip */; }; @@ -133,7 +132,6 @@ 512AF9C2236ED52C0066F8BE /* ImageHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 512AF9C1236ED52C0066F8BE /* ImageHeaderView.swift */; }; 512AF9DD236F05230066F8BE /* InteractiveLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 512AF9DC236F05230066F8BE /* InteractiveLabel.swift */; }; 512D554423C804DE0023FFFA /* OpenInSafariActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 512D554323C804DE0023FFFA /* OpenInSafariActivity.swift */; }; - 512DD4C92430086400C17B1F /* CloudKitAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 512DD4C82430086400C17B1F /* CloudKitAccountViewController.swift */; }; 512E08E62268800D00BDCFDD /* FolderTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97A11ED9F180007D329B /* FolderTreeControllerDelegate.swift */; }; 512E08E72268801200BDCFDD /* WebFeedTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97611ED9EB96007D329B /* WebFeedTreeControllerDelegate.swift */; }; 512E09012268907400BDCFDD /* MasterFeedTableViewSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 512E08F722688F7C00BDCFDD /* MasterFeedTableViewSectionHeader.swift */; }; @@ -237,7 +235,6 @@ 51627A6923861DED007B3B4B /* MasterFeedViewController+Drop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */; }; 51627A93238A3836007B3B4B /* CroppingPreviewParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A92238A3836007B3B4B /* CroppingPreviewParameters.swift */; }; 516A093B2360A4A000EAE89B /* SettingsTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 516A093A2360A4A000EAE89B /* SettingsTableViewCell.xib */; }; - 516A09402361240900EAE89B /* Account.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 516A093F2361240900EAE89B /* Account.storyboard */; }; 516AE9B32371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516AE9B22371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift */; }; 516AE9DF2372269A007DEEAA /* IconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516AE9DE2372269A007DEEAA /* IconImage.swift */; }; 516AE9E02372269A007DEEAA /* IconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516AE9DE2372269A007DEEAA /* IconImage.swift */; }; @@ -288,7 +285,6 @@ 51A052CE244FB9D7006C2024 /* AddFeedWIndowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A052CD244FB9D6006C2024 /* AddFeedWIndowController.swift */; }; 51A052CF244FB9D7006C2024 /* AddFeedWIndowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A052CD244FB9D6006C2024 /* AddFeedWIndowController.swift */; }; 51A1699A235E10D700EB091F /* Settings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 51A16990235E10D600EB091F /* Settings.storyboard */; }; - 51A169A0235E10D700EB091F /* FeedbinAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16996235E10D700EB091F /* FeedbinAccountViewController.swift */; }; 51A66685238075AE00CB272D /* AddWebFeedDefaultContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A66684238075AE00CB272D /* AddWebFeedDefaultContainer.swift */; }; 51A737AE24DB19730015FA66 /* RSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 51A737AD24DB19730015FA66 /* RSCore */; }; 51A737AF24DB19730015FA66 /* RSCore in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 51A737AD24DB19730015FA66 /* RSCore */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; @@ -660,7 +656,6 @@ 65ED4098235DEF770081F399 /* netnewswire-subscribe-to-feed.js in Resources */ = {isa = PBXBuildFile; fileRef = 6581C73F20CED60100F4AD34 /* netnewswire-subscribe-to-feed.js */; }; 65ED40A0235DEFF00081F399 /* container-migration.plist in Resources */ = {isa = PBXBuildFile; fileRef = 65ED409F235DEFF00081F399 /* container-migration.plist */; }; 65ED40A1235DEFF00081F399 /* container-migration.plist in Resources */ = {isa = PBXBuildFile; fileRef = 65ED409F235DEFF00081F399 /* container-migration.plist */; }; - 769F2ED513DA03EE75B993A8 /* NewsBlurAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 769F2D3643779DB02786278E /* NewsBlurAccountViewController.swift */; }; 8405DD8A2213E0E3008CE1BF /* DetailContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405DD892213E0E3008CE1BF /* DetailContainerView.swift */; }; 8405DD9922153B6B008CE1BF /* TimelineContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405DD9822153B6B008CE1BF /* TimelineContainerView.swift */; }; 8405DD9C22153BD7008CE1BF /* NSView-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405DD9B22153BD7008CE1BF /* NSView-Extensions.swift */; }; @@ -842,6 +837,9 @@ DDF9E1D728EDF2FC000BC355 /* notificationSoundBlip.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = DDF9E1D628EDF2FC000BC355 /* notificationSoundBlip.mp3 */; }; DDF9E1D828EDF2FC000BC355 /* notificationSoundBlip.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = DDF9E1D628EDF2FC000BC355 /* notificationSoundBlip.mp3 */; }; DDF9E1D928EDF2FC000BC355 /* notificationSoundBlip.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = DDF9E1D628EDF2FC000BC355 /* notificationSoundBlip.mp3 */; }; + DF28B44D294ED52700C4D8CA /* View+DismissOnExternalContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF28B44C294ED52700C4D8CA /* View+DismissOnExternalContext.swift */; }; + DF28B44F294ED92F00C4D8CA /* NewsBlurAddAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF28B44E294ED92F00C4D8CA /* NewsBlurAddAccountView.swift */; }; + DF28B451294EFC6C00C4D8CA /* View+DismissOnAccountAdd.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF28B450294EFC6C00C4D8CA /* View+DismissOnAccountAdd.swift */; }; 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 */; }; @@ -871,6 +869,8 @@ DFB3499A294C4F1D00BC81AD /* Errors.strings in Resources */ = {isa = PBXBuildFile; fileRef = DFB34998294C4F1D00BC81AD /* Errors.strings */; }; DFB3499E294C5D5000BC81AD /* CloudKitAddAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFB3499D294C5D5000BC81AD /* CloudKitAddAccountView.swift */; }; DFB349A0294E87B700BC81AD /* LocalAddAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFB3499F294E87B700BC81AD /* LocalAddAccountView.swift */; }; + DFB349A2294E90B500BC81AD /* FeedbinAddAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFB349A1294E90B500BC81AD /* FeedbinAddAccountView.swift */; }; + DFB349A4294E914D00BC81AD /* AccountSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFB349A3294E914D00BC81AD /* AccountSectionHeader.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 */; }; @@ -1180,7 +1180,6 @@ 176814562564BD0600D98635 /* ArticleItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleItemView.swift; sourceTree = ""; }; 1768147A2564BE5400D98635 /* widget-sample.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "widget-sample.json"; sourceTree = ""; }; 176814822564C02A00D98635 /* NetNewsWire_iOS_WidgetExtension.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = NetNewsWire_iOS_WidgetExtension.entitlements; sourceTree = ""; }; - 177A0C2C25454AAB00D7EAF6 /* ReaderAPIAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderAPIAccountViewController.swift; sourceTree = ""; }; 178A9F9C2549449F00AB7E9D /* AddAccountsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccountsView.swift; sourceTree = ""; }; 179D280C26F73D83003B2E0A /* ArticleThemePlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleThemePlist.swift; sourceTree = ""; }; 179DBBA2B22A659F81EED6F9 /* AccountsNewsBlurWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsNewsBlurWindowController.swift; sourceTree = ""; }; @@ -1224,7 +1223,6 @@ 512AF9C1236ED52C0066F8BE /* ImageHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageHeaderView.swift; sourceTree = ""; }; 512AF9DC236F05230066F8BE /* InteractiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveLabel.swift; sourceTree = ""; }; 512D554323C804DE0023FFFA /* OpenInSafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInSafariActivity.swift; sourceTree = ""; }; - 512DD4C82430086400C17B1F /* CloudKitAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitAccountViewController.swift; sourceTree = ""; }; 512E08F722688F7C00BDCFDD /* MasterFeedTableViewSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedTableViewSectionHeader.swift; sourceTree = ""; }; 51314617235A797400387FDC /* NetNewsWire_iOSintentextension_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOSintentextension_target.xcconfig; sourceTree = ""; }; 51314637235A7BBE00387FDC /* NetNewsWire iOS Intents Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "NetNewsWire iOS Intents Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1274,7 +1272,6 @@ 51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MasterFeedViewController+Drop.swift"; sourceTree = ""; }; 51627A92238A3836007B3B4B /* CroppingPreviewParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CroppingPreviewParameters.swift; sourceTree = ""; }; 516A093A2360A4A000EAE89B /* SettingsTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SettingsTableViewCell.xib; sourceTree = ""; }; - 516A093F2361240900EAE89B /* Account.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Account.storyboard; sourceTree = ""; }; 516AE5FF246AF34100731738 /* RedditAdd.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = RedditAdd.storyboard; sourceTree = ""; }; 516AE601246AF36100731738 /* RedditSelectTypeTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedditSelectTypeTableViewController.swift; sourceTree = ""; }; 516AE603246AF37B00731738 /* RedditSelectAccountTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedditSelectAccountTableViewController.swift; sourceTree = ""; }; @@ -1312,7 +1309,6 @@ 519ED47924482AEB007F8E94 /* EnableExtensionPointViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnableExtensionPointViewController.swift; sourceTree = ""; }; 51A052CD244FB9D6006C2024 /* AddFeedWIndowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AddFeedWIndowController.swift; path = AddFeed/AddFeedWIndowController.swift; sourceTree = ""; }; 51A16990235E10D600EB091F /* Settings.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Settings.storyboard; sourceTree = ""; }; - 51A16996235E10D700EB091F /* FeedbinAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedbinAccountViewController.swift; sourceTree = ""; }; 51A66684238075AE00CB272D /* AddWebFeedDefaultContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddWebFeedDefaultContainer.swift; sourceTree = ""; }; 51A9A5E32380C8870033AADF /* ShareFolderPickerAccountCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ShareFolderPickerAccountCell.xib; sourceTree = ""; }; 51A9A5E52380C8B20033AADF /* ShareFolderPickerFolderCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ShareFolderPickerFolderCell.xib; sourceTree = ""; }; @@ -1420,7 +1416,6 @@ 65ED409F235DEFF00081F399 /* container-migration.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "container-migration.plist"; sourceTree = ""; }; 65ED40F2235DF5E00081F399 /* NetNewsWire_macapp_target_macappstore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_macapp_target_macappstore.xcconfig; sourceTree = ""; }; 65ED4186235E045B0081F399 /* NetNewsWire_safariextension_target_macappstore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_safariextension_target_macappstore.xcconfig; sourceTree = ""; }; - 769F2D3643779DB02786278E /* NewsBlurAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurAccountViewController.swift; sourceTree = ""; }; 8405DD892213E0E3008CE1BF /* DetailContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailContainerView.swift; sourceTree = ""; }; 8405DD9822153B6B008CE1BF /* TimelineContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineContainerView.swift; sourceTree = ""; }; 8405DD9B22153BD7008CE1BF /* NSView-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSView-Extensions.swift"; sourceTree = ""; }; @@ -1609,6 +1604,9 @@ D5F4EDB820074D7C00B9E363 /* Folder+Scriptability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Folder+Scriptability.swift"; sourceTree = ""; }; DD82AB09231003F6002269DF /* SharingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharingTests.swift; sourceTree = ""; }; DDF9E1D628EDF2FC000BC355 /* notificationSoundBlip.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = notificationSoundBlip.mp3; sourceTree = ""; }; + DF28B44C294ED52700C4D8CA /* View+DismissOnExternalContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+DismissOnExternalContext.swift"; sourceTree = ""; }; + DF28B44E294ED92F00C4D8CA /* NewsBlurAddAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsBlurAddAccountView.swift; sourceTree = ""; }; + DF28B450294EFC6C00C4D8CA /* View+DismissOnAccountAdd.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+DismissOnAccountAdd.swift"; 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 = ""; }; @@ -1633,6 +1631,8 @@ DFB34998294C4F1D00BC81AD /* Errors.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Errors.strings; sourceTree = ""; }; DFB3499D294C5D5000BC81AD /* CloudKitAddAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitAddAccountView.swift; sourceTree = ""; }; DFB3499F294E87B700BC81AD /* LocalAddAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAddAccountView.swift; sourceTree = ""; }; + DFB349A1294E90B500BC81AD /* FeedbinAddAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAddAccountView.swift; sourceTree = ""; }; + DFB349A3294E914D00BC81AD /* AccountSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSectionHeader.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 = ""; }; @@ -1996,11 +1996,6 @@ children = ( DFB34992294C0B7400BC81AD /* Account.strings */, DFB3498F294C0B0D00BC81AD /* Views */, - 516A093F2361240900EAE89B /* Account.storyboard */, - 512DD4C82430086400C17B1F /* CloudKitAccountViewController.swift */, - 51A16996235E10D700EB091F /* FeedbinAccountViewController.swift */, - 769F2D3643779DB02786278E /* NewsBlurAccountViewController.swift */, - 177A0C2C25454AAB00D7EAF6 /* ReaderAPIAccountViewController.swift */, ); path = Account; sourceTree = ""; @@ -2998,6 +2993,9 @@ isa = PBXGroup; children = ( DFB34987294B447F00BC81AD /* InjectedNavigationView.swift */, + DFB349A3294E914D00BC81AD /* AccountSectionHeader.swift */, + DF28B44C294ED52700C4D8CA /* View+DismissOnExternalContext.swift */, + DF28B450294EFC6C00C4D8CA /* View+DismissOnAccountAdd.swift */, ); path = "SwiftUI Extensions"; sourceTree = ""; @@ -3007,6 +3005,8 @@ children = ( DFB3499F294E87B700BC81AD /* LocalAddAccountView.swift */, DFB3499D294C5D5000BC81AD /* CloudKitAddAccountView.swift */, + DFB349A1294E90B500BC81AD /* FeedbinAddAccountView.swift */, + DF28B44E294ED92F00C4D8CA /* NewsBlurAddAccountView.swift */, DFB34990294C0B2200BC81AD /* ReaderAPIAddAccountView.swift */, ); path = Views; @@ -3622,7 +3622,6 @@ 84C9FCA42262A1B800D921D6 /* LaunchScreenPhone.storyboard in Resources */, 516A093B2360A4A000EAE89B /* SettingsTableViewCell.xib in Resources */, 511D43D1231FA62800FB1562 /* SidebarKeyboardShortcuts.plist in Resources */, - 516A09402361240900EAE89B /* Account.storyboard in Resources */, 51C452AB22650DC600C03939 /* template.html in Resources */, 84A3EE61223B667F00557320 /* DefaultFeeds.opml in Resources */, B27EEBFB244D15F3000932E6 /* stylesheet.css in Resources */, @@ -4197,7 +4196,6 @@ buildActionMask = 2147483647; files = ( 51E36E71239D6610006F47A5 /* AddFeedSelectFolderTableViewCell.swift in Sources */, - 512DD4C92430086400C17B1F /* CloudKitAccountViewController.swift in Sources */, 840D617F2029031C009BC708 /* AppDelegate.swift in Sources */, 512E08E72268801200BDCFDD /* WebFeedTreeControllerDelegate.swift in Sources */, 51C452A422650A2D00C03939 /* ArticleUtilities.swift in Sources */, @@ -4208,6 +4206,7 @@ 5195C1DA2720205F00888867 /* ShadowTableChanges.swift in Sources */, 5183CCDA226E31A50010922C /* NonIntrinsicImageView.swift in Sources */, B2B80778239C4C7000F191E0 /* RSImage-AppIcons.swift in Sources */, + DFB349A2294E90B500BC81AD /* FeedbinAddAccountView.swift in Sources */, 518ED21D23D0F26000E0A862 /* UIViewController-Extensions.swift in Sources */, DFD406FF291FDC0C00C02962 /* DisplayAndBehaviorsView.swift in Sources */, 51A9A5F52380F6A60033AADF /* ModalNavigationController.swift in Sources */, @@ -4265,7 +4264,6 @@ 51BB7C272335A8E5008E8144 /* ArticleActivityItemSource.swift in Sources */, 51F85BF52273625800C787DC /* Bundle-Extensions.swift in Sources */, DF790D6228E990A900455FC7 /* AboutData.swift in Sources */, - 177A0C2D25454AAB00D7EAF6 /* ReaderAPIAccountViewController.swift in Sources */, 51C452A622650A3500C03939 /* Node-Extensions.swift in Sources */, DF394F0029357A180081EB6E /* NewArticleNotificationsView.swift in Sources */, 51C45294226509C800C03939 /* SearchFeedDelegate.swift in Sources */, @@ -4289,6 +4287,7 @@ 51C4529D22650A1000C03939 /* FaviconURLFinder.swift in Sources */, 5142192A23522B5500E07E2C /* ImageViewController.swift in Sources */, 51C45258226508CF00C03939 /* AppAssets.swift in Sources */, + DF28B451294EFC6C00C4D8CA /* View+DismissOnAccountAdd.swift in Sources */, 51FA73A82332BE880090D516 /* ExtractedArticle.swift in Sources */, 51C4527C2265091600C03939 /* MasterTimelineDefaultCellLayout.swift in Sources */, 51E4398023805EBC00015C31 /* AddComboTableViewCell.swift in Sources */, @@ -4343,7 +4342,6 @@ DF3630EF293618A900326FB8 /* SettingsViewModel.swift in Sources */, 51C452882265093600C03939 /* AddFeedViewController.swift in Sources */, 51B5C8C023F3866C00032075 /* ExtensionFeedAddRequestFile.swift in Sources */, - 51A169A0235E10D700EB091F /* FeedbinAccountViewController.swift in Sources */, 51934CCE2310792F006127BE /* ActivityManager.swift in Sources */, DFB34980294B085100BC81AD /* AccountInspectorView.swift in Sources */, 5108F6B72375E612001ABC45 /* CacheCleaner.swift in Sources */, @@ -4356,6 +4354,7 @@ 516AE9B32371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift in Sources */, 51DC370B2405BC9A0095D371 /* PreloadedWebView.swift in Sources */, D3555BF524664566005E48C3 /* ArticleSearchBar.swift in Sources */, + DF28B44D294ED52700C4D8CA /* View+DismissOnExternalContext.swift in Sources */, 8454C3F3263F2D8700E3F9C7 /* IconImageCache.swift in Sources */, DFD406F5291F79C900C02962 /* SettingsView.swift in Sources */, B24E9ADE245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */, @@ -4363,11 +4362,13 @@ 17071EF126F8137400F5E71D /* ArticleTheme+Notifications.swift in Sources */, 51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */, 84DEE56622C32CA4005FC42C /* SmartFeedDelegate.swift in Sources */, + DF28B44F294ED92F00C4D8CA /* NewsBlurAddAccountView.swift in Sources */, 512E09012268907400BDCFDD /* MasterFeedTableViewSectionHeader.swift in Sources */, 512392BF24E33A3C00F11704 /* RedditSelectSortTableViewController.swift in Sources */, 516AE9E02372269A007DEEAA /* IconImage.swift in Sources */, 519ED456244828C3007F8E94 /* AddExtensionPointViewController.swift in Sources */, 51C45268226508F600C03939 /* MasterFeedUnreadCountView.swift in Sources */, + DFB349A4294E914D00BC81AD /* AccountSectionHeader.swift in Sources */, D3A39865246505DF00F9A366 /* FindInArticleActivity.swift in Sources */, 5183CCD0226E1E880010922C /* NonIntrinsicLabel.swift in Sources */, 5137C2EA26F63AE6009EFEDB /* ArticleThemeImporter.swift in Sources */, @@ -4387,7 +4388,6 @@ 511D4419231FC02D00FB1562 /* KeyboardManager.swift in Sources */, 51C45293226509C800C03939 /* StarredFeedDelegate.swift in Sources */, 51D6A5BC23199C85001C27D8 /* MasterTimelineDataSource.swift in Sources */, - 769F2ED513DA03EE75B993A8 /* NewsBlurAccountViewController.swift in Sources */, 51BC4B01247277E0000A6ED8 /* URL-Extensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Shared/Translations/Errors.strings b/Shared/Translations/Errors.strings index 4844e4b15..9a0312549 100644 --- a/Shared/Translations/Errors.strings +++ b/Shared/Translations/Errors.strings @@ -9,4 +9,6 @@ "ERROR_TITLE" = "Error"; "DUPLICATE_ACCOUNT_ERROR" = "There is already an account of that type with that username created."; "CLOUDKIT_NOT_ENABLED_ERROR" = "Unable to add iCloud Account. Please make sure you have iCloud and iCloud Drive enabled in System Settings."; - +"USERNAME_AND_PASSWORD_REQUIRED" = "Username and password required."; +"INVALID_USERNAME_PASSWORD" = "Invalid email or password combination."; +"KEYCHAIN_ERROR" = "Keychain error while storing credentials."; diff --git a/Shared/Translations/LocalizedNetNewsWireError.swift b/Shared/Translations/LocalizedNetNewsWireError.swift index b16ddf099..c67974f50 100644 --- a/Shared/Translations/LocalizedNetNewsWireError.swift +++ b/Shared/Translations/LocalizedNetNewsWireError.swift @@ -16,8 +16,14 @@ public enum LocalizedNetNewsWireError: LocalizedError { /// Displayed when the user attempts to add a /// iCloud account but iCloud and/or iCloud Drive - /// are not enabled/ + /// are not enabled. case iCloudDriveMissing + + case userNameAndPasswordRequired + + case invalidUsernameOrPassword + + case keychainError public var errorDescription: String? { switch self { @@ -25,6 +31,12 @@ public enum LocalizedNetNewsWireError: LocalizedError { return Bundle.main.localizedString(forKey: "DUPLICATE_ACCOUNT_ERROR", value: nil, table: "Errors") case .iCloudDriveMissing: return Bundle.main.localizedString(forKey: "CLOUDKIT_NOT_ENABLED_ERROR", value: nil, table: "Errors") + case .userNameAndPasswordRequired: + return Bundle.main.localizedString(forKey: "USERNAME_AND_PASSWORD_REQUIRED", value: nil, table: "Errors") + case .invalidUsernameOrPassword: + return Bundle.main.localizedString(forKey: "INVALID_USERNAME_PASSWORD", value: nil, table: "Errors") + case .keychainError: + return Bundle.main.localizedString(forKey: "KEYCHAIN_ERROR", value: nil, table: "Errors") } } } diff --git a/Shared/Widget/WidgetDataEncoder.swift b/Shared/Widget/WidgetDataEncoder.swift index 1b610a8b5..ac4ba2768 100644 --- a/Shared/Widget/WidgetDataEncoder.swift +++ b/Shared/Widget/WidgetDataEncoder.swift @@ -143,9 +143,9 @@ public final class WidgetDataEncoder { let latestData = WidgetData(currentUnreadCount: SmartFeedsController.shared.unreadFeed.unreadCount, currentTodayCount: SmartFeedsController.shared.todayFeed.unreadCount, currentStarredCount: (try? AccountManager.shared.fetchCountForStarredArticles()) ?? 0, - unreadArticles: unread, - starredArticles: starred, - todayArticles:today, + unreadArticles: unread.sorted(by: { $0.pubDate > $1.pubDate }), + starredArticles: starred.sorted(by: { $0.pubDate > $1.pubDate }), + todayArticles:today.sorted(by: { $0.pubDate > $1.pubDate }), lastUpdateTime: Date()) completion(latestData) } diff --git a/iOS/Account/Account.storyboard b/iOS/Account/Account.storyboard deleted file mode 100644 index f1fb29edf..000000000 --- a/iOS/Account/Account.storyboard +++ /dev/null @@ -1,810 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/iOS/Account/Account.strings b/iOS/Account/Account.strings index 818dbd7d8..7d9b15040 100644 --- a/iOS/Account/Account.strings +++ b/iOS/Account/Account.strings @@ -6,11 +6,14 @@ Copyright © 2022 Ranchero Software. All rights reserved. */ -/* Account Names */ +/* Account Section */ "ACCOUNT_NAME" = "Name"; "CLOUDKIT" = "iCloud"; "LOCAL_ACCOUNT_NAME_PHONE" = "On My iPhone"; "LOCAL_ACCOUNT_NAME_PAD" = "On My iPad"; +"ACCOUNT_EMAIL_ADDRESS_PROMPT" = "Email Address"; +"ACCOUNT_USERNAME_PROMPT" = "Username"; +"ACCOUNT_PASSWORD_PROMPT" = "Password"; /* Explainers */ @@ -20,5 +23,7 @@ "OLDREADER_FOOTER_EXPLAINER" = "Sign in to your The Old Reader account and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDon’t have a The Old Reader account? [Sign Up Here](https://theoldreader.com)"; "FRESHRSS_FOOTER_EXPLAINER" = "Sign in to your FreshRSS instance and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDon’t have an FreshRSS instance? [Sign Up Here](https://freshrss.org)"; "CLOUDKIT_FOOTER_EXPLAINER" = "NetNewsWire will use your iCloud account to sync your subscriptions across your Mac and iOS devices."; +"FEEDBIN_FOOTER_EXPLAINER" = "Sign in to your Feedbin account and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDon’t have a Feedbin account? [Sign Up Here](https://feedbin.com/signup)"; +"NEWSBLUR_FOOTER_EXPLAINER" = "Sign in to your NewsBlur account and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDon’t have a NewsBlur account? [Sign Up Here](https://newsblur.com)"; diff --git a/iOS/Account/CloudKitAccountViewController.swift b/iOS/Account/CloudKitAccountViewController.swift deleted file mode 100644 index 08d10d010..000000000 --- a/iOS/Account/CloudKitAccountViewController.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// CloudKitAccountViewController.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 3/28/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import UIKit -import SafariServices -import Account - -enum CloudKitAccountViewControllerError: LocalizedError { - case iCloudDriveMissing - - var errorDescription: String? { - return NSLocalizedString("Unable to add iCloud Account. Please make sure you have iCloud and iCloud Drive enabled in System Preferences.", comment: "Unable to add iCloud Account.") - } -} - -class CloudKitAccountViewController: UITableViewController { - - //weak var delegate: AddAccountDismissDelegate? - @IBOutlet weak var footerLabel: UILabel! - - override func viewDidLoad() { - super.viewDidLoad() - setupFooter() - - tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") - } - - private func setupFooter() { - footerLabel.text = NSLocalizedString("NetNewsWire will use your iCloud account to sync your subscriptions across your Mac and iOS devices.", comment: "iCloud") - } - - @IBAction func cancel(_ sender: Any) { - dismiss(animated: true, completion: nil) - //delegate?.dismiss() - } - - @IBAction func add(_ sender: Any) { - guard FileManager.default.ubiquityIdentityToken != nil else { - presentError(CloudKitAccountViewControllerError.iCloudDriveMissing) - return - } - - let _ = AccountManager.shared.createAccount(type: .cloudKit) - dismiss(animated: true, completion: nil) - //delegate?.dismiss() - } - - override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section) - } - - override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - if section == 0 { - let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView - headerView.imageView.image = AppAssets.image(for: .cloudKit) - return headerView - } else { - return super.tableView(tableView, viewForHeaderInSection: section) - } - } - - @IBAction func openLimitationsAndSolutions(_ sender: Any) { - let vc = SFSafariViewController(url: CloudKitWebDocumentation.limitationsAndSolutionsURL) - vc.modalPresentationStyle = .pageSheet - present(vc, animated: true) - } - -} diff --git a/iOS/Account/FeedbinAccountViewController.swift b/iOS/Account/FeedbinAccountViewController.swift deleted file mode 100644 index 33abc33c1..000000000 --- a/iOS/Account/FeedbinAccountViewController.swift +++ /dev/null @@ -1,194 +0,0 @@ -// -// FeedbinAccountViewController.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 5/19/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import UIKit -import Account -import Secrets -import RSWeb -import SafariServices -import RSCore - -class FeedbinAccountViewController: UITableViewController, Logging { - - @IBOutlet weak var activityIndicator: UIActivityIndicatorView! - @IBOutlet weak var cancelBarButtonItem: UIBarButtonItem! - @IBOutlet weak var emailTextField: UITextField! - @IBOutlet weak var passwordTextField: UITextField! - @IBOutlet weak var showHideButton: UIButton! - @IBOutlet weak var actionButton: UIButton! - @IBOutlet weak var footerLabel: UILabel! - - weak var account: Account? - //weak var delegate: AddAccountDismissDelegate? - - override func viewDidLoad() { - super.viewDidLoad() - setupFooter() - - activityIndicator.isHidden = true - emailTextField.delegate = self - passwordTextField.delegate = self - - if let account = account, let credentials = try? account.retrieveCredentials(type: .basic) { - actionButton.setTitle(NSLocalizedString("Update Credentials", comment: "Update Credentials"), for: .normal) - actionButton.isEnabled = true - emailTextField.text = credentials.username - passwordTextField.text = credentials.secret - } else { - actionButton.setTitle(NSLocalizedString("Add Account", comment: "Add Account"), for: .normal) - } - - NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: emailTextField) - NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: passwordTextField) - - tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") - - } - - private func setupFooter() { - footerLabel.text = NSLocalizedString("Sign in to your Feedbin account and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDon’t have a Feedbin account?", comment: "Feedbin") - } - - override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section) - } - - override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - if section == 0 { - let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView - headerView.imageView.image = AppAssets.image(for: .feedbin) - return headerView - } else { - return super.tableView(tableView, viewForHeaderInSection: section) - } - } - - @IBAction func cancel(_ sender: Any) { - dismiss(animated: true, completion: nil) - } - - - @IBAction func showHidePassword(_ sender: Any) { - if passwordTextField.isSecureTextEntry { - passwordTextField.isSecureTextEntry = false - showHideButton.setTitle("Hide", for: .normal) - } else { - passwordTextField.isSecureTextEntry = true - showHideButton.setTitle("Show", for: .normal) - } - } - - @IBAction func action(_ sender: Any) { - guard let email = emailTextField.text, let password = passwordTextField.text else { - showError(NSLocalizedString("Username & password required.", comment: "Credentials Error")) - return - } - - // When you fill in the email address via auto-complete it adds extra whitespace - let trimmedEmail = email.trimmingCharacters(in: .whitespaces) - - guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: .feedbin, username: trimmedEmail) else { - showError(NSLocalizedString("There is already a Feedbin account with that username created.", comment: "Duplicate Error")) - return - } - - resignFirstResponder() - toggleActivityIndicatorAnimation(visible: true) - setNavigationEnabled(to: false) - - let credentials = Credentials(type: .basic, username: trimmedEmail, secret: password) - Account.validateCredentials(type: .feedbin, credentials: credentials) { result in - self.toggleActivityIndicatorAnimation(visible: false) - self.setNavigationEnabled(to: true) - - switch result { - case .success(let credentials): - if let credentials = credentials { - if self.account == nil { - self.account = AccountManager.shared.createAccount(type: .feedbin) - } - - do { - - do { - try self.account?.removeCredentials(type: .basic) - } catch { - self.logger.error("Error removing credentials: \(error.localizedDescription, privacy: .public).") - } - try self.account?.storeCredentials(credentials) - - self.account?.refreshAll() { result in - switch result { - case .success: - break - case .failure(let error): - self.presentError(error) - } - } - - self.dismiss(animated: true, completion: nil) - //self.delegate?.dismiss() - } catch { - self.showError(NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error")) - self.logger.error("Keychain error while storing credentials: \(error.localizedDescription, privacy: .public).") - } - } else { - self.showError(NSLocalizedString("Invalid email/password combination.", comment: "Credentials Error")) - } - case .failure: - self.showError(NSLocalizedString("Network error. Try again later.", comment: "Credentials Error")) - } - - } - } - - @IBAction func signUpWithProvider(_ sender: Any) { - let url = URL(string: "https://feedbin.com/signup")! - let safari = SFSafariViewController(url: url) - safari.modalPresentationStyle = .currentContext - self.present(safari, animated: true, completion: nil) - } - - - @objc func textDidChange(_ note: Notification) { - actionButton.isEnabled = !(emailTextField.text?.isEmpty ?? false) && !(passwordTextField.text?.isEmpty ?? false) - } - - private func showError(_ message: String) { - presentError(title: NSLocalizedString("Error", comment: "Credentials Error"), message: message) - } - - private func setNavigationEnabled(to value:Bool){ - cancelBarButtonItem.isEnabled = value - actionButton.isEnabled = value - } - - private func toggleActivityIndicatorAnimation(visible value: Bool){ - activityIndicator.isHidden = !value - if value { - activityIndicator.startAnimating() - } else { - activityIndicator.stopAnimating() - } - } - -} - -extension FeedbinAccountViewController: UITextFieldDelegate { - - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if textField == emailTextField { - passwordTextField.becomeFirstResponder() - } else { - textField.resignFirstResponder() - action(self) - } - return true - } - -} diff --git a/iOS/Account/NewsBlurAccountViewController.swift b/iOS/Account/NewsBlurAccountViewController.swift deleted file mode 100644 index 845e6f429..000000000 --- a/iOS/Account/NewsBlurAccountViewController.swift +++ /dev/null @@ -1,197 +0,0 @@ -// -// NewsBlurAccountViewController.swift -// NetNewsWire -// -// Created by Anh-Quang Do on 3/9/20. -// Copyright (c) 2020 Ranchero Software. All rights reserved. -// - -import UIKit -import Account -import Secrets -import RSWeb -import RSCore -import SafariServices - -class NewsBlurAccountViewController: UITableViewController, Logging { - - @IBOutlet weak var activityIndicator: UIActivityIndicatorView! - @IBOutlet weak var cancelBarButtonItem: UIBarButtonItem! - @IBOutlet weak var usernameTextField: UITextField! - @IBOutlet weak var passwordTextField: UITextField! - @IBOutlet weak var showHideButton: UIButton! - @IBOutlet weak var actionButton: UIButton! - @IBOutlet weak var footerLabel: UILabel! - - weak var account: Account? - //weak var delegate: AddAccountDismissDelegate? - - override func viewDidLoad() { - super.viewDidLoad() - setupFooter() - activityIndicator.isHidden = true - usernameTextField.delegate = self - passwordTextField.delegate = self - - if let account = account, let credentials = try? account.retrieveCredentials(type: .newsBlurBasic) { - actionButton.setTitle(NSLocalizedString("Update Credentials", comment: "Update Credentials"), for: .normal) - actionButton.isEnabled = true - usernameTextField.text = credentials.username - passwordTextField.text = credentials.secret - } else { - actionButton.setTitle(NSLocalizedString("Add Account", comment: "Add Account"), for: .normal) - } - - NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: usernameTextField) - NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: passwordTextField) - - tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") - } - - private func setupFooter() { - footerLabel.text = NSLocalizedString("Sign in to your NewsBlur account and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDon’t have a NewsBlur account?", comment: "NewsBlur") - } - - override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section) - } - - override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - if section == 0 { - let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView - headerView.imageView.image = AppAssets.image(for: .newsBlur) - return headerView - } else { - return super.tableView(tableView, viewForHeaderInSection: section) - } - } - - @IBAction func cancel(_ sender: Any) { - dismiss(animated: true, completion: nil) - } - - @IBAction func showHidePassword(_ sender: Any) { - if passwordTextField.isSecureTextEntry { - passwordTextField.isSecureTextEntry = false - showHideButton.setTitle("Hide", for: .normal) - } else { - passwordTextField.isSecureTextEntry = true - showHideButton.setTitle("Show", for: .normal) - } - } - - @IBAction func action(_ sender: Any) { - - guard let username = usernameTextField.text else { - showError(NSLocalizedString("Username required.", comment: "Credentials Error")) - return - } - - // When you fill in the email address via auto-complete it adds extra whitespace - let trimmedUsername = username.trimmingCharacters(in: .whitespaces) - - guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: .newsBlur, username: trimmedUsername) else { - showError(NSLocalizedString("There is already a NewsBlur account with that username created.", comment: "Duplicate Error")) - return - } - - let password = passwordTextField.text ?? "" - - startAnimatingActivityIndicator() - disableNavigation() - - let basicCredentials = Credentials(type: .newsBlurBasic, username: trimmedUsername, secret: password) - Account.validateCredentials(type: .newsBlur, credentials: basicCredentials) { result in - - self.stopAnimatingActivityIndicator() - self.enableNavigation() - - switch result { - case .success(let sessionCredentials): - if let sessionCredentials = sessionCredentials { - - if self.account == nil { - self.account = AccountManager.shared.createAccount(type: .newsBlur) - } - - do { - - do { - try self.account?.removeCredentials(type: .newsBlurBasic) - try self.account?.removeCredentials(type: .newsBlurSessionId) - } catch { - self.logger.error("Error removing credentials: \(error.localizedDescription, privacy: .public).") - } - try self.account?.storeCredentials(basicCredentials) - try self.account?.storeCredentials(sessionCredentials) - - self.account?.refreshAll() { result in - switch result { - case .success: - break - case .failure(let error): - self.presentError(error) - } - } - - self.dismiss(animated: true, completion: nil) - //self.delegate?.dismiss() - } catch { - self.showError(NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error")) - self.logger.error("Keychain error while storing credentials: \(error.localizedDescription, privacy: .public).") - } - } else { - self.showError(NSLocalizedString("Invalid username/password combination.", comment: "Credentials Error")) - } - case .failure(let error): - self.showError(error.localizedDescription) - } - - } - } - - @IBAction func signUpWithProvider(_ sender: Any) { - let url = URL(string: "https://newsblur.com")! - let safari = SFSafariViewController(url: url) - safari.modalPresentationStyle = .currentContext - self.present(safari, animated: true, completion: nil) - } - - @objc func textDidChange(_ note: Notification) { - actionButton.isEnabled = !(usernameTextField.text?.isEmpty ?? false) - } - - private func showError(_ message: String) { - presentError(title: "Error", message: message) - } - - private func enableNavigation() { - self.cancelBarButtonItem.isEnabled = true - self.actionButton.isEnabled = true - } - - private func disableNavigation() { - cancelBarButtonItem.isEnabled = false - actionButton.isEnabled = false - } - - private func startAnimatingActivityIndicator() { - activityIndicator.isHidden = false - activityIndicator.startAnimating() - } - - private func stopAnimatingActivityIndicator() { - self.activityIndicator.isHidden = true - self.activityIndicator.stopAnimating() - } - -} - -extension NewsBlurAccountViewController: UITextFieldDelegate { - - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - textField.resignFirstResponder() - return true - } - -} diff --git a/iOS/Account/ReaderAPIAccountViewController.swift b/iOS/Account/ReaderAPIAccountViewController.swift deleted file mode 100644 index 11705b63d..000000000 --- a/iOS/Account/ReaderAPIAccountViewController.swift +++ /dev/null @@ -1,322 +0,0 @@ -// -// ReaderAPIAccountViewController.swift -// NetNewsWire-iOS -// -// Created by Stuart Breckenridge on 25/10/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import UIKit -import Account -import Secrets -import RSWeb -import SafariServices -import RSCore - -class ReaderAPIAccountViewController: UITableViewController, Logging { - - @IBOutlet weak var activityIndicator: UIActivityIndicatorView! - @IBOutlet weak var cancelBarButtonItem: UIBarButtonItem! - @IBOutlet weak var usernameTextField: UITextField! - @IBOutlet weak var passwordTextField: UITextField! - @IBOutlet weak var apiURLTextField: UITextField! - @IBOutlet weak var showHideButton: UIButton! - @IBOutlet weak var actionButton: UIButton! - @IBOutlet weak var footerLabel: UILabel! - @IBOutlet weak var signUpButton: UIButton! - - weak var account: Account? - var accountType: AccountType? - //weak var delegate: AddAccountDismissDelegate? - - override func viewDidLoad() { - super.viewDidLoad() - setupFooter() - - activityIndicator.isHidden = true - usernameTextField.delegate = self - passwordTextField.delegate = self - - if let unwrappedAcount = account, - let credentials = try? retrieveCredentialsForAccount(for: unwrappedAcount) { - actionButton.setTitle(NSLocalizedString("Update Credentials", comment: "Update Credentials"), for: .normal) - actionButton.isEnabled = true - usernameTextField.text = credentials.username - passwordTextField.text = credentials.secret - } else { - actionButton.setTitle(NSLocalizedString("Add Account", comment: "Add Account"), for: .normal) - } - - if let unwrappedAccountType = accountType { - switch unwrappedAccountType { - case .freshRSS: - title = NSLocalizedString("FreshRSS", comment: "FreshRSS") - apiURLTextField.placeholder = NSLocalizedString("API URL: fresh.rss.net/api/greader.php", comment: "FreshRSS API Helper") - case .inoreader: - title = NSLocalizedString("InoReader", comment: "InoReader") - case .bazQux: - title = NSLocalizedString("BazQux", comment: "BazQux") - case .theOldReader: - title = NSLocalizedString("The Old Reader", comment: "The Old Reader") - default: - title = "" - } - } - - NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: usernameTextField) - NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: passwordTextField) - - tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") - - } - - private func setupFooter() { - switch accountType { - case .bazQux: - footerLabel.text = NSLocalizedString("Sign in to your BazQux account and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDon’t have a BazQux account?", comment: "BazQux") - signUpButton.setTitle(NSLocalizedString("Sign Up Here", comment: "BazQux SignUp"), for: .normal) - case .inoreader: - footerLabel.text = NSLocalizedString("Sign in to your InoReader account and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDon’t have an InoReader account?", comment: "InoReader") - signUpButton.setTitle(NSLocalizedString("Sign Up Here", comment: "InoReader SignUp"), for: .normal) - case .theOldReader: - footerLabel.text = NSLocalizedString("Sign in to your The Old Reader account and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDon’t have a The Old Reader account?", comment: "TOR") - signUpButton.setTitle(NSLocalizedString("Sign Up Here", comment: "TOR SignUp"), for: .normal) - case .freshRSS: - footerLabel.text = NSLocalizedString("Sign in to your FreshRSS instance and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDon’t have an FreshRSS instance?", comment: "FreshRSS") - signUpButton.setTitle(NSLocalizedString("Find Out More", comment: "FreshRSS SignUp"), for: .normal) - default: - return - } - } - - override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section) - } - - override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - if section == 0 { - let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView - headerView.imageView.image = headerViewImage() - return headerView - } else { - return super.tableView(tableView, viewForHeaderInSection: section) - } - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - switch section { - case 0: - switch accountType { - case .freshRSS: - return 3 - default: - return 2 - } - default: - return 1 - } - } - - - @IBAction func cancel(_ sender: Any) { - dismiss(animated: true, completion: nil) - } - - @IBAction func showHidePassword(_ sender: Any) { - if passwordTextField.isSecureTextEntry { - passwordTextField.isSecureTextEntry = false - showHideButton.setTitle("Hide", for: .normal) - } else { - passwordTextField.isSecureTextEntry = true - showHideButton.setTitle("Show", for: .normal) - } - } - - @IBAction func action(_ sender: Any) { - guard validateDataEntry(), let type = accountType else { - return - } - - let username = usernameTextField.text! - let password = passwordTextField.text! - let url = apiURL()! - - // When you fill in the email address via auto-complete it adds extra whitespace - let trimmedUsername = username.trimmingCharacters(in: .whitespaces) - - guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: type, username: trimmedUsername) else { - showError(NSLocalizedString("There is already an account of that type with that username created.", comment: "Duplicate Error")) - return - } - - startAnimatingActivityIndicator() - disableNavigation() - - let credentials = Credentials(type: .readerBasic, username: trimmedUsername, secret: password) - Account.validateCredentials(type: type, credentials: credentials, endpoint: url) { result in - - self.stopAnimatingActivityIndicator() - self.enableNavigation() - - switch result { - case .success(let validatedCredentials): - if let validatedCredentials = validatedCredentials { - - if self.account == nil { - self.account = AccountManager.shared.createAccount(type: type) - } - - do { - self.account?.endpointURL = url - - try? self.account?.removeCredentials(type: .readerBasic) - try? self.account?.removeCredentials(type: .readerAPIKey) - try self.account?.storeCredentials(credentials) - try self.account?.storeCredentials(validatedCredentials) - - self.dismiss(animated: true, completion: nil) - - self.account?.refreshAll() { result in - switch result { - case .success: - break - case .failure(let error): - self.showError(NSLocalizedString(error.localizedDescription, comment: "Accoount Refresh Error")) - } - } - - //self.delegate?.dismiss() - } catch { - self.showError(NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error")) - self.logger.error("Keychain error while storing credentials: \(error.localizedDescription, privacy: .public).") - } - } else { - self.showError(NSLocalizedString("Invalid username/password combination.", comment: "Credentials Error")) - } - case .failure(let error): - self.showError(error.localizedDescription) - } - - } - } - - private func retrieveCredentialsForAccount(for account: Account) throws -> Credentials? { - switch accountType { - case .bazQux, .inoreader, .theOldReader, .freshRSS: - return try account.retrieveCredentials(type: .readerBasic) - default: - return nil - } - } - - private func headerViewImage() -> UIImage? { - if let accountType = accountType { - switch accountType { - case .bazQux: - return AppAssets.accountBazQuxImage - case .inoreader: - return AppAssets.accountInoreaderImage - case .theOldReader: - return AppAssets.accountTheOldReaderImage - case .freshRSS: - return AppAssets.accountFreshRSSImage - default: - return nil - } - } - return nil - } - - private func validateDataEntry() -> Bool { - switch accountType { - case .freshRSS: - if !usernameTextField.hasText || !passwordTextField.hasText || !apiURLTextField.hasText { - showError(NSLocalizedString("Username, password, and API URL are required.", comment: "Credentials Error")) - return false - } - guard let _ = URL(string: apiURLTextField.text!) else { - showError(NSLocalizedString("Invalid API URL.", comment: "Invalid API URL")) - return false - } - default: - if !usernameTextField.hasText || !passwordTextField.hasText { - showError(NSLocalizedString("Username and password are required.", comment: "Credentials Error")) - return false - } - } - return true - } - - @IBAction func signUpWithProvider(_ sender: Any) { - var url: URL! - switch accountType { - case .bazQux: - url = URL(string: "https://bazqux.com")! - case .inoreader: - url = URL(string: "https://www.inoreader.com")! - case .theOldReader: - url = URL(string: "https://theoldreader.com")! - case .freshRSS: - url = URL(string: "https://freshrss.org")! - default: - return - } - let safari = SFSafariViewController(url: url) - safari.modalPresentationStyle = .currentContext - self.present(safari, animated: true, completion: nil) - } - - private func apiURL() -> URL? { - switch accountType { - case .freshRSS: - return URL(string: apiURLTextField.text!)! - case .inoreader: - return URL(string: ReaderAPIVariant.inoreader.host)! - case .bazQux: - return URL(string: ReaderAPIVariant.bazQux.host)! - case .theOldReader: - return URL(string: ReaderAPIVariant.theOldReader.host)! - default: - return nil - } - } - - - - @objc func textDidChange(_ note: Notification) { - actionButton.isEnabled = !(usernameTextField.text?.isEmpty ?? false) - } - - private func showError(_ message: String) { - presentError(title: "Error", message: message) - } - - private func enableNavigation() { - self.cancelBarButtonItem.isEnabled = true - self.actionButton.isEnabled = true - } - - private func disableNavigation() { - cancelBarButtonItem.isEnabled = false - actionButton.isEnabled = false - } - - private func startAnimatingActivityIndicator() { - activityIndicator.isHidden = false - activityIndicator.startAnimating() - } - - private func stopAnimatingActivityIndicator() { - self.activityIndicator.isHidden = true - self.activityIndicator.stopAnimating() - } -} - -extension ReaderAPIAccountViewController: UITextFieldDelegate { - - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - textField.resignFirstResponder() - return true - } - -} diff --git a/iOS/Account/Views/CloudKitAddAccountView.swift b/iOS/Account/Views/CloudKitAddAccountView.swift index 97ddd43bf..95bae4208 100644 --- a/iOS/Account/Views/CloudKitAddAccountView.swift +++ b/iOS/Account/Views/CloudKitAddAccountView.swift @@ -17,7 +17,7 @@ struct CloudKitAddAccountView: View { var body: some View { NavigationView { Form { - Section(header: cloudKitHeader) {} + AccountSectionHeader(accountType: .cloudKit) Section { createCloudKitAccount } Section(footer: cloudKitExplainer) {} } @@ -33,23 +33,11 @@ struct CloudKitAddAccountView: View { } message: { Text(addAccountError.0?.localizedDescription ?? "Unknown Error") } - .onReceive(NotificationCenter.default.publisher(for: .UserDidAddAccount)) { _ in - dismiss() - } + .dismissOnExternalContextLaunch() + .dismissOnAccountAdd() } } - var cloudKitHeader: some View { - HStack { - Spacer() - Image(uiImage: AppAssets.accountCloudKitImage) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 48, height: 48) - Spacer() - } - } - var createCloudKitAccount: some View { Button { guard FileManager.default.ubiquityIdentityToken != nil else { diff --git a/iOS/Account/Views/FeedbinAddAccountView.swift b/iOS/Account/Views/FeedbinAddAccountView.swift new file mode 100644 index 000000000..bf79dfc0b --- /dev/null +++ b/iOS/Account/Views/FeedbinAddAccountView.swift @@ -0,0 +1,167 @@ +// +// FeedbinAddAccountView.swift +// NetNewsWire-iOS +// +// Created by Stuart Breckenridge on 18/12/2022. +// Copyright © 2022 Ranchero Software. All rights reserved. +// + +import SwiftUI +import Account +import Secrets +import RSWeb +import SafariServices +import RSCore + + +struct FeedbinAddAccountView: View { + + @Environment(\.dismiss) private var dismiss + @State var account: Account? = nil + @State private var accountEmail: String = "" + @State private var accountPassword: String = "" + @State private var showProgressIndicator: Bool = false + @State private var accountError: (Error?, Bool) = (nil, false) + + var body: some View { + NavigationView { + Form { + AccountSectionHeader(accountType: .feedbin) + accountDetails + accountButton + Section(footer: feedbinAccountExplainer) {} + } + .task { + retrieveCredentials() + } + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button(action: { dismiss() }, label: { Text("CANCEL_BUTTON_TITLE", tableName: "Buttons") }) + .disabled(showProgressIndicator) + } + ToolbarItem(placement: .navigationBarTrailing) { + if showProgressIndicator { ProgressView() } + } + } + .alert(Text("ERROR_TITLE", tableName: "Errors"), isPresented: $accountError.1) { + Button(role: .cancel) { + // + } label: { + Text("DISMISS_BUTTON_TITLE", tableName: "Buttons") + } + } message: { + Text(accountError.0?.localizedDescription ?? "Error") + } + .navigationTitle(Text(account?.type.localizedAccountName() ?? "")) + .navigationBarTitleDisplayMode(.inline) + .dismissOnExternalContextLaunch() + .dismissOnAccountAdd() + } + } + + var accountDetails: some View { + Section { + TextField("Email", text: $accountEmail, prompt: Text("ACCOUNT_EMAIL_ADDRESS_PROMPT", tableName: "Account")) + .autocorrectionDisabled() + .autocapitalization(.none) + SecureField("Password", text: $accountPassword, prompt: Text("ACCOUNT_PASSWORD_PROMPT", tableName: "Account")) + } + } + + var accountButton: some View { + Section { + Button { + Task { + do { + try await executeAccountCredentials() + dismiss() + } catch { + accountError = (error, true) + } + } + } label: { + HStack{ + Spacer() + if account == nil { + Text("ADD_ACCOUNT_BUTTON_TITLE", tableName: "Buttons") + } else { + Text("UPDATE_CREDENTIALS_BUTTON_TITLE", tableName: "Buttons") + } + Spacer() + } + } + } + } + + var feedbinAccountExplainer: some View { + Text("FEEDBIN_FOOTER_EXPLAINER", tableName: "Account").multilineTextAlignment(.center) + } + + private func retrieveCredentials() { + if let account = account { + do { + if let creds = try account.retrieveCredentials(type: .basic) { + accountEmail = creds.username + accountPassword = creds.secret + } + } catch { + accountError = (error, true) + } + } + } + + private func executeAccountCredentials() async throws { + let trimmedEmailAddress = accountEmail.trimmingWhitespace + + guard (account != nil || !AccountManager.shared.duplicateServiceAccount(type: .feedbin, username: trimmedEmailAddress)) else { + throw LocalizedNetNewsWireError.duplicateAccount + } + showProgressIndicator = true + + let credentials = Credentials(type: .basic, username: trimmedEmailAddress, secret: accountPassword) + return try await withCheckedThrowingContinuation { continuation in + Account.validateCredentials(type: .feedbin, credentials: credentials) { result in + switch result { + case .success(let credentials): + if let validatedCredentials = credentials { + if self.account == nil { + self.account = AccountManager.shared.createAccount(type: .feedbin) + } + + do { + try? self.account?.removeCredentials(type: .basic) + try self.account?.storeCredentials(validatedCredentials) + self.account?.refreshAll(completion: { result in + switch result { + case .success(_): + showProgressIndicator = false + continuation.resume() + case .failure(let failure): + showProgressIndicator = false + continuation.resume(throwing: failure) + } + }) + } catch { + showProgressIndicator = false + continuation.resume(throwing: LocalizedNetNewsWireError.keychainError) + } + } else { + showProgressIndicator = false + continuation.resume(throwing: LocalizedNetNewsWireError.invalidUsernameOrPassword) + } + case .failure(let failure): + showProgressIndicator = false + continuation.resume(throwing: failure) + } + } + } + } + + +} + +struct FeedbinAddAccountView_Previews: PreviewProvider { + static var previews: some View { + FeedbinAddAccountView() + } +} diff --git a/iOS/Account/Views/LocalAddAccountView.swift b/iOS/Account/Views/LocalAddAccountView.swift index a842f531e..b40269a05 100644 --- a/iOS/Account/Views/LocalAddAccountView.swift +++ b/iOS/Account/Views/LocalAddAccountView.swift @@ -17,7 +17,7 @@ struct LocalAddAccountView: View { var body: some View { NavigationView { Form { - Section(header: accountHeaderView) {} + AccountSectionHeader(accountType: .onMyMac) Section { accountNameSection } Section { addAccountButton } Section(footer: accountFooterView) {} @@ -29,9 +29,8 @@ struct LocalAddAccountView: View { } .navigationTitle(deviceAccountName()) .navigationBarTitleDisplayMode(.inline) - .onReceive(NotificationCenter.default.publisher(for: .UserDidAddAccount)) { _ in - dismiss() - } + .dismissOnExternalContextLaunch() + .dismissOnAccountAdd() } } @@ -39,6 +38,8 @@ struct LocalAddAccountView: View { TextField("Name", text: $accountName, prompt: Text("ACCOUNT_NAME", tableName: "Account")) + .autocorrectionDisabled() + .autocapitalization(.none) } var addAccountButton: some View { @@ -54,17 +55,6 @@ struct LocalAddAccountView: View { } } - var accountHeaderView: some View { - HStack { - Spacer() - Image(uiImage: accountImage()) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 48, height: 48) - Spacer() - } - } - var accountFooterView: some View { HStack { Spacer() diff --git a/iOS/Account/Views/NewsBlurAddAccountView.swift b/iOS/Account/Views/NewsBlurAddAccountView.swift new file mode 100644 index 000000000..3a8612c23 --- /dev/null +++ b/iOS/Account/Views/NewsBlurAddAccountView.swift @@ -0,0 +1,173 @@ +// +// NewsBlurAddAccountView.swift +// NetNewsWire-iOS +// +// Created by Stuart Breckenridge on 18/12/2022. +// Copyright © 2022 Ranchero Software. All rights reserved. +// + +import SwiftUI +import Account +import Secrets +import RSWeb +import RSCore + +struct NewsBlurAddAccountView: View { + + @Environment(\.dismiss) private var dismiss + @State var account: Account? = nil + @State private var accountUserName: String = "" + @State private var accountPassword: String = "" + @State private var showProgressIndicator: Bool = false + @State private var accountError: (Error?, Bool) = (nil, false) + + var body: some View { + NavigationView { + Form { + AccountSectionHeader(accountType: .newsBlur) + accountDetails + accountButton + Section(footer: newsBlurAccountExplainer) {} + } + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button(action: { dismiss() }, label: { Text("CANCEL_BUTTON_TITLE", tableName: "Buttons") }) + .disabled(showProgressIndicator) + } + ToolbarItem(placement: .navigationBarTrailing) { + if showProgressIndicator { ProgressView() } + } + } + .navigationTitle(Text(AccountType.newsBlur.localizedAccountName())) + .navigationBarTitleDisplayMode(.inline) + .task { + retreiveCredentials() + } + .alert(Text("ERROR_TITLE", tableName: "Errors"), isPresented: $accountError.1) { + Button(role: .cancel) { + // + } label: { + Text("DISMISS_BUTTON_TITLE", tableName: "Buttons") + } + } message: { + Text(accountError.0?.localizedDescription ?? "") + } + .dismissOnExternalContextLaunch() + .dismissOnAccountAdd() + } + } + + func retreiveCredentials() { + if let account = account { + do { + let credentials = try account.retrieveCredentials(type: .newsBlurBasic) + if let credentials = credentials { + self.accountUserName = credentials.username + self.accountPassword = credentials.secret + } else { + print("No cred") + } + } catch { + print(error.localizedDescription) + } + } else { + print("No account") + } + + } + + var accountDetails: some View { + Section { + TextField("Email", text: $accountUserName, prompt: Text("ACCOUNT_USERNAME_PROMPT", tableName: "Account")) + .autocorrectionDisabled() + .autocapitalization(.none) + SecureField("Password", text: $accountPassword, prompt: Text("ACCOUNT_PASSWORD_PROMPT", tableName: "Account")) + } + } + + var accountButton: some View { + Section { + Button { + Task { + do { + try await executeAccountCredentials() + dismiss() + } catch { + accountError = (error, true) + } + } + } label: { + HStack{ + Spacer() + if account == nil { + Text("ADD_ACCOUNT_BUTTON_TITLE", tableName: "Buttons") + } else { + Text("UPDATE_CREDENTIALS_BUTTON_TITLE", tableName: "Buttons") + } + Spacer() + } + } + } + } + + var newsBlurAccountExplainer: some View { + Text("NEWSBLUR_FOOTER_EXPLAINER", tableName: "Account").multilineTextAlignment(.center) + } + + private func executeAccountCredentials() async throws { + let trimmedEmailAddress = accountUserName.trimmingWhitespace + + guard (account != nil || !AccountManager.shared.duplicateServiceAccount(type: .newsBlur, username: trimmedEmailAddress)) else { + throw LocalizedNetNewsWireError.duplicateAccount + } + showProgressIndicator = true + + let basicCredentials = Credentials(type: .newsBlurBasic, username: trimmedEmailAddress, secret: accountPassword) + + return try await withCheckedThrowingContinuation { continuation in + Account.validateCredentials(type: .newsBlur, credentials: basicCredentials) { result in + switch result { + case .success(let credentials): + if let sessionsCredentials = credentials { + if self.account == nil { + self.account = AccountManager.shared.createAccount(type: .newsBlur) + } + + do { + try self.account?.removeCredentials(type: .newsBlurBasic) + try self.account?.removeCredentials(type: .newsBlurSessionId) + try self.account?.storeCredentials(basicCredentials) + try self.account?.storeCredentials(sessionsCredentials) + + self.account?.refreshAll(completion: { result in + switch result { + case .success(_): + showProgressIndicator = false + continuation.resume() + case .failure(let failure): + showProgressIndicator = false + continuation.resume(throwing: failure) + } + }) + } catch { + showProgressIndicator = false + continuation.resume(throwing: LocalizedNetNewsWireError.keychainError) + } + } else { + showProgressIndicator = false + continuation.resume(throwing: LocalizedNetNewsWireError.invalidUsernameOrPassword) + } + case .failure(let failure): + showProgressIndicator = false + continuation.resume(throwing: failure) + } + } + } + } +} + +struct NewsBlurAddAccountView_Previews: PreviewProvider { + static var previews: some View { + NewsBlurAddAccountView() + } +} diff --git a/iOS/Account/Views/ReaderAPIAddAccountView.swift b/iOS/Account/Views/ReaderAPIAddAccountView.swift index 50c10209c..09344a347 100644 --- a/iOS/Account/Views/ReaderAPIAddAccountView.swift +++ b/iOS/Account/Views/ReaderAPIAddAccountView.swift @@ -29,14 +29,16 @@ struct ReaderAPIAddAccountView: View { var body: some View { NavigationView { Form { - Section(header: readerAccountImage) {} + if accountType != nil { + AccountSectionHeader(accountType: accountType!) + } accountDetailsSection Section(footer: readerAccountExplainer) {} } .navigationTitle(Text(accountType?.localizedAccountName() ?? "")) .navigationBarTitleDisplayMode(.inline) .task { - try? retrieveAccountCredentials() + retrieveAccountCredentials() } .toolbar { ToolbarItem(placement: .navigationBarLeading) { @@ -55,10 +57,8 @@ struct ReaderAPIAddAccountView: View { } message: { Text(accountSetupError.0?.localizedDescription ?? "") } - .onReceive(NotificationCenter.default.publisher(for: .UserDidAddAccount)) { _ in - dismiss() - } - .edgesIgnoringSafeArea(.bottom) // Fix to make sure view is not offset from the top when presented + .dismissOnExternalContextLaunch() + .dismissOnAccountAdd() } } @@ -78,47 +78,19 @@ struct ReaderAPIAddAccountView: View { } } - var readerAccountImage: some View { - HStack { - Spacer() - if accountType == nil { Text("") } - switch accountType! { - case .bazQux: - Image(uiImage: AppAssets.accountBazQuxImage) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 48, height: 48) - case .inoreader: - Image(uiImage: AppAssets.accountInoreaderImage) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 48, height: 48) - case .theOldReader: - Image(uiImage: AppAssets.accountTheOldReaderImage) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 48, height: 48) - case .freshRSS: - Image(uiImage: AppAssets.accountFreshRSSImage) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 48, height: 48) - default: - Text("") - } - Spacer() - } - } + var accountDetailsSection: some View { Group { Section { TextField("Username", text: $accountUserName) .autocorrectionDisabled() + .autocapitalization(.none) SecureField("Password", text: $accountSecret) if accountType == .freshRSS && accountCredentials == nil { TextField("FreshRSS URL", text: $accountAPIUrl, prompt: Text("fresh.rss.net/api/greader.php")) .autocorrectionDisabled() + .autocapitalization(.none) } } @@ -127,6 +99,7 @@ struct ReaderAPIAddAccountView: View { Task { do { try await executeAccountCredentials() + dismiss() } catch { accountSetupError = (error, true) } @@ -150,7 +123,7 @@ struct ReaderAPIAddAccountView: View { // MARK: - API - private func retrieveAccountCredentials() throws { + private func retrieveAccountCredentials() { if let account = account { do { if let creds = try account.retrieveCredentials(type: .readerBasic) { diff --git a/iOS/Inspector/Inspector.strings b/iOS/Inspector/Inspector.strings index 6a9e0ad64..94addd18a 100644 --- a/iOS/Inspector/Inspector.strings +++ b/iOS/Inspector/Inspector.strings @@ -9,7 +9,7 @@ /* Account Inspector */ "ACCOUNT_NAME_FIELD" = "Account Name"; "ACTIVE" = "Active"; -"CLOUDKIT_LIMITATIONS_TITLE" = "[iCloud Syncing Limitations & Solutions](https://netnewswire.com/help/iCloud)"; +"CLOUDKIT_LIMITATIONS_LINK" = "[iCloud Syncing Limitations & Solutions](https://netnewswire.com/help/iCloud)"; "REMOVE_ACCOUNT_TITLE" = "Remove Account"; "REMOVE_FEEDLY_MESSAGE" = "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_MESSAGE" = "Are you sure you want to remove this account? This cannot be undone."; diff --git a/iOS/Inspector/Views/AccountInspectorView.swift b/iOS/Inspector/Views/AccountInspectorView.swift index d1321879a..2dd2924ee 100644 --- a/iOS/Inspector/Views/AccountInspectorView.swift +++ b/iOS/Inspector/Views/AccountInspectorView.swift @@ -20,7 +20,7 @@ struct AccountInspectorView: View { var body: some View { Form { - Section(header: accountHeaderView){} + AccountSectionHeader(accountType: account.type) accountNameAndActiveSection if account.type != .onMyMac && @@ -46,13 +46,14 @@ struct AccountInspectorView: View { case .theOldReader, .bazQux, .inoreader, .freshRSS: ReaderAPIAddAccountView(accountType: account.type, account: account) case .feedbin: - Text("FEEDBIN") + FeedbinAddAccountView(account: account) case .newsBlur: - Text("NEWSBLUR") + NewsBlurAddAccountView(account: account) default: EmptyView() } } + .dismissOnExternalContextLaunch() } var accountHeaderView: some View { @@ -135,7 +136,7 @@ struct AccountInspectorView: View { var cloudKitLimitations: some View { HStack { Spacer() - Text("CLOUDKIT_LIMITATIONS_TITLE", tableName: "Inspector") + Text("CLOUDKIT_LIMITATIONS_LINK", tableName: "Inspector") Spacer() } } diff --git a/iOS/Inspector/Views/ExtensionInspectorView.swift b/iOS/Inspector/Views/ExtensionInspectorView.swift index a2a42ee14..129041539 100644 --- a/iOS/Inspector/Views/ExtensionInspectorView.swift +++ b/iOS/Inspector/Views/ExtensionInspectorView.swift @@ -50,9 +50,9 @@ struct ExtensionInspectorView: View { } .navigationTitle(Text(extensionPoint?.title ?? "")) .edgesIgnoringSafeArea(.bottom) + .dismissOnExternalContextLaunch() } - var extensionHeader: some View { HStack { Spacer() diff --git a/iOS/Inspector/Views/WebFeedInspectorView.swift b/iOS/Inspector/Views/WebFeedInspectorView.swift index 679aee445..c35888b80 100644 --- a/iOS/Inspector/Views/WebFeedInspectorView.swift +++ b/iOS/Inspector/Views/WebFeedInspectorView.swift @@ -63,6 +63,7 @@ struct WebFeedInspectorView: View { .sheet(isPresented: $showHomePage, onDismiss: nil) { SafariView(url: URL(string: webFeed.homePageURL!)!) } + .dismissOnExternalContextLaunch() } var webFeedHeaderView: some View { diff --git a/iOS/Settings/Views/Account and Extensions/Accounts/AccountsManagementView.swift b/iOS/Settings/Views/Account and Extensions/Accounts/AccountsManagementView.swift index 5c2ee0fc3..924dd3777 100644 --- a/iOS/Settings/Views/Account and Extensions/Accounts/AccountsManagementView.swift +++ b/iOS/Settings/Views/Account and Extensions/Accounts/AccountsManagementView.swift @@ -66,7 +66,7 @@ struct AccountsManagementView: View { Button(role: .destructive) { AccountManager.shared.deleteAccount(accountToRemove!) } label: { - Text("REMOVE_BUTTON_TITLE", tableName: "Buttons") + Text("REMOVE_ACCOUNT_BUTTON_TITLE", tableName: "Buttons") } Button(role: .cancel) { diff --git a/iOS/Settings/Views/Account and Extensions/Accounts/AddAccountListView.swift b/iOS/Settings/Views/Account and Extensions/Accounts/AddAccountListView.swift index 6f29fce47..087a86d35 100644 --- a/iOS/Settings/Views/Account and Extensions/Accounts/AddAccountListView.swift +++ b/iOS/Settings/Views/Account and Extensions/Accounts/AddAccountListView.swift @@ -90,15 +90,15 @@ struct AddAccountListView: View { LocalAddAccountView() case .cloudKit: CloudKitAddAccountView() + case .newsBlur: + NewsBlurAddAccountView() case .freshRSS, .inoreader, .bazQux, .theOldReader: ReaderAPIAddAccountView(accountType: viewModel.showAddAccountSheet.accountType, account: nil) default: Text(viewModel.showAddAccountSheet.accountType.localizedAccountName()) } } - .onReceive(NotificationCenter.default.publisher(for: .UserDidAddAccount)) { _ in - dismiss() - } + .dismissOnAccountAdd() } } diff --git a/iOS/Settings/Views/General/SettingsView.swift b/iOS/Settings/Views/General/SettingsView.swift index 203712e0e..80e841f6c 100644 --- a/iOS/Settings/Views/General/SettingsView.swift +++ b/iOS/Settings/Views/General/SettingsView.swift @@ -108,9 +108,7 @@ struct SettingsView: View { } } } - .onReceive(NotificationCenter.default.publisher(for: .LaunchedFromExternalAction), perform: { _ in - dismiss() - }) + .dismissOnExternalContextLaunch() .fileImporter(isPresented: $viewModel.showImportView, allowedContentTypes: OPMLDocument.readableContentTypes) { result in switch result { case .success(let url): diff --git a/iOS/SwiftUI Extensions/AccountSectionHeader.swift b/iOS/SwiftUI Extensions/AccountSectionHeader.swift new file mode 100644 index 000000000..e0f3aed72 --- /dev/null +++ b/iOS/SwiftUI Extensions/AccountSectionHeader.swift @@ -0,0 +1,61 @@ +// +// AccountSectionHeader.swift +// NetNewsWire-iOS +// +// Created by Stuart Breckenridge on 18/12/2022. +// Copyright © 2022 Ranchero Software. All rights reserved. +// + +import SwiftUI +import Account + +struct AccountSectionHeader: View { + + var accountType: AccountType + + var body: some View { + Section(header: headerImage) {} + } + + var headerImage: some View { + HStack { + Spacer() + Image(uiImage: imageToUse()) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 48, height: 48) + Spacer() + } + } + + private func imageToUse() -> UIImage { + switch accountType { + case .onMyMac: + if UIDevice.current.userInterfaceIdiom == .pad { return AppAssets.accountLocalPadImage } + return AppAssets.accountLocalPhoneImage + case .cloudKit: + return AppAssets.accountCloudKitImage + case .feedly: + return AppAssets.accountFeedlyImage + case .feedbin: + return AppAssets.accountFeedbinImage + case .newsBlur: + return AppAssets.accountNewsBlurImage + case .freshRSS: + return AppAssets.accountFreshRSSImage + case .inoreader: + return AppAssets.accountInoreaderImage + case .bazQux: + return AppAssets.accountBazQuxImage + case .theOldReader: + return AppAssets.accountTheOldReaderImage + } + } + +} + +struct AccountHeader_Previews: PreviewProvider { + static var previews: some View { + AccountSectionHeader(accountType: .onMyMac) + } +} diff --git a/iOS/SwiftUI Extensions/View+DismissOnAccountAdd.swift b/iOS/SwiftUI Extensions/View+DismissOnAccountAdd.swift new file mode 100644 index 000000000..c049ab2d6 --- /dev/null +++ b/iOS/SwiftUI Extensions/View+DismissOnAccountAdd.swift @@ -0,0 +1,28 @@ +// +// View+DismissOnAccountAdd.swift +// NetNewsWire-iOS +// +// Created by Stuart Breckenridge on 18/12/2022. +// Copyright © 2022 Ranchero Software. All rights reserved. +// + +import SwiftUI + +struct DismissOnAccountAdd: ViewModifier { + + @Environment(\.dismiss) private var dismiss + + func body(content: Content) -> some View { + content + .onReceive(NotificationCenter.default.publisher(for: .UserDidAddAccount)) { _ in + dismiss() + } + } + +} + +extension View { + func dismissOnAccountAdd() -> some View { + modifier(DismissOnAccountAdd()) + } +} diff --git a/iOS/SwiftUI Extensions/View+DismissOnExternalContext.swift b/iOS/SwiftUI Extensions/View+DismissOnExternalContext.swift new file mode 100644 index 000000000..67ec0bdf4 --- /dev/null +++ b/iOS/SwiftUI Extensions/View+DismissOnExternalContext.swift @@ -0,0 +1,29 @@ +// +// View+DismissOnExternalContext.swift +// NetNewsWire-iOS +// +// Created by Stuart Breckenridge on 18/12/2022. +// Copyright © 2022 Ranchero Software. All rights reserved. +// + +import SwiftUI + + +struct DismissOnExternalContext: ViewModifier { + + @Environment(\.dismiss) private var dismiss + + func body(content: Content) -> some View { + content + .onReceive(NotificationCenter.default.publisher(for: .LaunchedFromExternalAction)) { _ in + dismiss() + } + } + +} + +extension View { + func dismissOnExternalContextLaunch() -> some View { + modifier(DismissOnExternalContext()) + } +} diff --git a/iOS/UIKit Extensions/UIViewController-Extensions.swift b/iOS/UIKit Extensions/UIViewController-Extensions.swift index 1ed607502..e82395f25 100644 --- a/iOS/UIKit Extensions/UIViewController-Extensions.swift +++ b/iOS/UIKit Extensions/UIViewController-Extensions.swift @@ -13,9 +13,7 @@ import Account extension UIViewController { func presentError(_ error: Error, dismiss: (() -> Void)? = nil) { - if let accountError = error as? AccountError, accountError.isCredentialsError { - presentAccountError(accountError, dismiss: dismiss) - } else if let decodingError = error as? DecodingError { + if let decodingError = error as? DecodingError { let errorTitle = NSLocalizedString("Error", comment: "Error") var informativeText: String = "" switch decodingError { @@ -53,38 +51,3 @@ extension UIViewController { } } - -private extension UIViewController { - - func presentAccountError(_ error: AccountError, dismiss: (() -> Void)? = nil) { - let title = NSLocalizedString("Account Error", comment: "Account Error") - let alertController = UIAlertController(title: title, message: error.localizedDescription, preferredStyle: .alert) - - if error.account?.type == .feedbin { - - let credentialsTitle = NSLocalizedString("Update Credentials", comment: "Update Credentials") - let credentialsAction = UIAlertAction(title: credentialsTitle, style: .default) { [weak self] _ in - dismiss?() - - let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "FeedbinAccountNavigationViewController") as! UINavigationController - navController.modalPresentationStyle = .formSheet - let addViewController = navController.topViewController as! FeedbinAccountViewController - addViewController.account = error.account - self?.present(navController, animated: true) - } - - alertController.addAction(credentialsAction) - alertController.preferredAction = credentialsAction - - } - - let dismissTitle = NSLocalizedString("OK", comment: "OK") - let dismissAction = UIAlertAction(title: dismissTitle, style: .default) { _ in - dismiss?() - } - alertController.addAction(dismissAction) - - self.present(alertController, animated: true, completion: nil) - } - -}