mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Merge pull request #3778 from stuartbreckenridge/ios-ui-settings-localised
Settings/Inspectors/Account to SwiftUI
This commit is contained in:
@@ -11,7 +11,7 @@ import RSCore
|
||||
import RSWeb
|
||||
import Articles
|
||||
|
||||
public final class WebFeed: Feed, Renamable, Hashable {
|
||||
public final class WebFeed: Feed, Renamable, Hashable, ObservableObject {
|
||||
|
||||
public var defaultReadFilterType: ReadFilterType {
|
||||
return .none
|
||||
|
||||
@@ -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 */; };
|
||||
@@ -79,8 +78,6 @@
|
||||
51077C5A27A86D16000C71DB /* Hyperlegible.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 51077C5727A86D16000C71DB /* Hyperlegible.nnwtheme */; };
|
||||
5108F6B62375E612001ABC45 /* CacheCleaner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5108F6B52375E612001ABC45 /* CacheCleaner.swift */; };
|
||||
5108F6B72375E612001ABC45 /* CacheCleaner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5108F6B52375E612001ABC45 /* CacheCleaner.swift */; };
|
||||
5108F6D22375EED2001ABC45 /* TimelineCustomizerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5108F6D12375EED2001ABC45 /* TimelineCustomizerViewController.swift */; };
|
||||
5108F6D42375EEEF001ABC45 /* TimelinePreviewTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5108F6D32375EEEF001ABC45 /* TimelinePreviewTableViewController.swift */; };
|
||||
5108F6D823763094001ABC45 /* TickMarkSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5108F6D723763094001ABC45 /* TickMarkSlider.swift */; };
|
||||
510C416124E5CDE3008226FD /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510C416024E5CDE3008226FD /* ShareViewController.swift */; };
|
||||
510C416424E5CDE3008226FD /* ShareViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 510C416224E5CDE3008226FD /* ShareViewController.xib */; };
|
||||
@@ -99,11 +96,9 @@
|
||||
510C418624E5D1B4008226FD /* ExtensionFeedAddRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C87A23F2317700032075 /* ExtensionFeedAddRequest.swift */; };
|
||||
510C43F7243D035C009F70C3 /* ExtensionPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510C43F6243D035C009F70C3 /* ExtensionPoint.swift */; };
|
||||
510C43F8243D035C009F70C3 /* ExtensionPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510C43F6243D035C009F70C3 /* ExtensionPoint.swift */; };
|
||||
510FFAB326EEA22C00F32265 /* ArticleThemesTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510FFAB226EEA22C00F32265 /* ArticleThemesTableViewController.swift */; };
|
||||
51102165233A7D6C0007A5F7 /* ArticleExtractorButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51102164233A7D6C0007A5F7 /* ArticleExtractorButton.swift */; };
|
||||
51107746243BEE2500D97C8C /* ExtensionPointPreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51107745243BEE2500D97C8C /* ExtensionPointPreferencesViewController.swift */; };
|
||||
51107747243BEE2500D97C8C /* ExtensionPointPreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51107745243BEE2500D97C8C /* ExtensionPointPreferencesViewController.swift */; };
|
||||
5110C37D2373A8D100A9C04F /* InspectorIconHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5110C37C2373A8D100A9C04F /* InspectorIconHeaderView.swift */; };
|
||||
51126DA4225FDE2F00722696 /* RSImage-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */; };
|
||||
5117715524E1EA0F00A2A836 /* ArticleExtractorButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5117715424E1EA0F00A2A836 /* ArticleExtractorButton.swift */; };
|
||||
5117715624E1EA0F00A2A836 /* ArticleExtractorButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5117715424E1EA0F00A2A836 /* ArticleExtractorButton.swift */; };
|
||||
@@ -130,7 +125,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 */; };
|
||||
@@ -187,7 +181,6 @@
|
||||
513F32812593EF180003048F /* Account in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 516B695E24D2F33B00B5702F /* Account */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||
513F32882593EF8F0003048F /* RSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 513F32872593EF8F0003048F /* RSCore */; };
|
||||
513F32892593EF8F0003048F /* RSCore in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 513F32872593EF8F0003048F /* RSCore */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||
5141E7392373C18B0013FF27 /* WebFeedInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5141E7382373C18B0013FF27 /* WebFeedInspectorViewController.swift */; };
|
||||
514217062921C9DD00963F14 /* Bundle-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BF42273625800C787DC /* Bundle-Extensions.swift */; };
|
||||
5142192A23522B5500E07E2C /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5142192923522B5500E07E2C /* ImageViewController.swift */; };
|
||||
514219372352510100E07E2C /* ImageScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514219362352510100E07E2C /* ImageScrollView.swift */; };
|
||||
@@ -224,15 +217,9 @@
|
||||
515A5181243E90260089E588 /* ExtensionPointIdentifer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A5176243E90200089E588 /* ExtensionPointIdentifer.swift */; };
|
||||
515D4FCA23257CB500EE1167 /* Node-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97971ED9EFAA007D329B /* Node-Extensions.swift */; };
|
||||
515D4FCC2325815A00EE1167 /* SafariExt.js in Resources */ = {isa = PBXBuildFile; fileRef = 515D4FCB2325815A00EE1167 /* SafariExt.js */; };
|
||||
516244E3241E19F000B61C47 /* ColorPaletteTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516244E2241E19F000B61C47 /* ColorPaletteTableViewController.swift */; };
|
||||
51627A6723861DA3007B3B4B /* MasterFeedViewController+Drag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A6623861DA3007B3B4B /* MasterFeedViewController+Drag.swift */; };
|
||||
51627A6923861DED007B3B4B /* MasterFeedViewController+Drop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */; };
|
||||
51627A93238A3836007B3B4B /* CroppingPreviewParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A92238A3836007B3B4B /* CroppingPreviewParameters.swift */; };
|
||||
516A093723609A3600EAE89B /* SettingsComboTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 516A091D23609A3600EAE89B /* SettingsComboTableViewCell.xib */; };
|
||||
516A09392360A2AE00EAE89B /* SettingsComboTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516A09382360A2AE00EAE89B /* SettingsComboTableViewCell.swift */; };
|
||||
516A093B2360A4A000EAE89B /* SettingsTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 516A093A2360A4A000EAE89B /* SettingsTableViewCell.xib */; };
|
||||
516A09402361240900EAE89B /* Account.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 516A093F2361240900EAE89B /* Account.storyboard */; };
|
||||
516A09422361248000EAE89B /* Inspector.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 516A09412361248000EAE89B /* Inspector.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 */; };
|
||||
@@ -277,17 +264,8 @@
|
||||
519B8D332143397200FA689C /* SharingServiceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519B8D322143397200FA689C /* SharingServiceDelegate.swift */; };
|
||||
519CA8E525841DB700EB079A /* CrashReporter in Frameworks */ = {isa = PBXBuildFile; productRef = 519CA8E425841DB700EB079A /* CrashReporter */; };
|
||||
519E743D22C663F900A78E47 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519E743422C663F900A78E47 /* SceneDelegate.swift */; };
|
||||
519ED456244828C3007F8E94 /* AddExtensionPointViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519ED455244828C3007F8E94 /* AddExtensionPointViewController.swift */; };
|
||||
519ED47A24482AEB007F8E94 /* EnableExtensionPointViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519ED47924482AEB007F8E94 /* EnableExtensionPointViewController.swift */; };
|
||||
519ED47C24488C6F007F8E94 /* ExtensionInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519ED47B24488C6F007F8E94 /* ExtensionInspectorViewController.swift */; };
|
||||
51A052CE244FB9D7006C2024 /* AddFeedWIndowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A052CD244FB9D6006C2024 /* AddFeedWIndowController.swift */; };
|
||||
51A052CF244FB9D7006C2024 /* AddFeedWIndowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A052CD244FB9D6006C2024 /* AddFeedWIndowController.swift */; };
|
||||
51A16999235E10D700EB091F /* LocalAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A1698F235E10D600EB091F /* LocalAccountViewController.swift */; };
|
||||
51A1699A235E10D700EB091F /* Settings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 51A16990235E10D600EB091F /* Settings.storyboard */; };
|
||||
51A1699B235E10D700EB091F /* AccountInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16991235E10D600EB091F /* AccountInspectorViewController.swift */; };
|
||||
51A1699C235E10D700EB091F /* AddAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16992235E10D600EB091F /* AddAccountViewController.swift */; };
|
||||
51A1699D235E10D700EB091F /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16993235E10D600EB091F /* SettingsViewController.swift */; };
|
||||
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, ); }; };
|
||||
@@ -658,7 +636,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 */; };
|
||||
@@ -840,8 +817,40 @@
|
||||
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 */; };
|
||||
DF28B453294FE6C600C4D8CA /* EnableExtensionPointView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF28B452294FE6C600C4D8CA /* EnableExtensionPointView.swift */; };
|
||||
DF28B455294FE74A00C4D8CA /* ExtensionSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF28B454294FE74A00C4D8CA /* ExtensionSectionHeader.swift */; };
|
||||
DF28B4572950163F00C4D8CA /* EnableExtensionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF28B4562950163F00C4D8CA /* EnableExtensionViewModel.swift */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
DF84E563295122BA0045C334 /* TimelineCustomizerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF84E562295122BA0045C334 /* TimelineCustomizerView.swift */; };
|
||||
DFB3497A294A962D00BC81AD /* AddAccountListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFB34979294A962D00BC81AD /* AddAccountListView.swift */; };
|
||||
DFB34980294B085100BC81AD /* AccountInspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFB3497F294B085100BC81AD /* AccountInspectorView.swift */; };
|
||||
DFB34988294B447F00BC81AD /* InjectedNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFB34987294B447F00BC81AD /* InjectedNavigationView.swift */; };
|
||||
DFB3498A294B45AC00BC81AD /* ExtensionInspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFB34989294B45AC00BC81AD /* ExtensionInspectorView.swift */; };
|
||||
DFB3498C294B4CA700BC81AD /* WebFeedInspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFB3498B294B4CA700BC81AD /* WebFeedInspectorView.swift */; };
|
||||
DFB34994294C0E3900BC81AD /* ReaderAPIAddAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFB34990294C0B2200BC81AD /* ReaderAPIAddAccountView.swift */; };
|
||||
DFB34996294C4DCB00BC81AD /* LocalizedNetNewsWireError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFB34995294C4DCB00BC81AD /* LocalizedNetNewsWireError.swift */; };
|
||||
DFB34997294C4DCB00BC81AD /* LocalizedNetNewsWireError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFB34995294C4DCB00BC81AD /* LocalizedNetNewsWireError.swift */; };
|
||||
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 */; };
|
||||
DFBB4EAC2951BC0200639228 /* NNWThemeDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFBB4EAB2951BC0200639228 /* NNWThemeDocument.swift */; };
|
||||
DFBB4EAD2951BC0200639228 /* NNWThemeDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFBB4EAB2951BC0200639228 /* NNWThemeDocument.swift */; };
|
||||
DFBB4EAE2951BC0200639228 /* NNWThemeDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFBB4EAB2951BC0200639228 /* NNWThemeDocument.swift */; };
|
||||
DFBB4EB02951BCAC00639228 /* ArticleThemeManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFBB4EAF2951BCAC00639228 /* ArticleThemeManagerView.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 */; };
|
||||
@@ -851,9 +860,13 @@
|
||||
DFCE4F9228EF26F100405869 /* About.plist in Resources */ = {isa = PBXBuildFile; fileRef = DFCE4F9028EF26F000405869 /* About.plist */; };
|
||||
DFCE4F9428EF278300405869 /* Thanks.md in Resources */ = {isa = PBXBuildFile; fileRef = DFCE4F9328EF278300405869 /* Thanks.md */; };
|
||||
DFCE4F9528EF278300405869 /* Thanks.md in Resources */ = {isa = PBXBuildFile; fileRef = DFCE4F9328EF278300405869 /* Thanks.md */; };
|
||||
DFD406F5291F79C900C02962 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFD406F4291F79C900C02962 /* SettingsView.swift */; };
|
||||
DFD406F7291FB1A600C02962 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFD406F6291FB1A600C02962 /* SafariView.swift */; };
|
||||
DFD406FA291FB5E400C02962 /* SettingsRows.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFD406F9291FB5E400C02962 /* SettingsRows.swift */; };
|
||||
DFD406FC291FB63B00C02962 /* SettingsHelpSheets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFD406FB291FB63B00C02962 /* SettingsHelpSheets.swift */; };
|
||||
DFD406FF291FDC0C00C02962 /* DisplayAndBehaviorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFD406FE291FDC0C00C02962 /* DisplayAndBehaviorsView.swift */; };
|
||||
DFE522A32953DEF400376B77 /* CustomInsetGroupedRowStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFE522A22953DEF400376B77 /* CustomInsetGroupedRowStyle.swift */; };
|
||||
DFFB8FC2279B75E300AC21D7 /* Account in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 51BC2F4A24D343A500E90810 /* Account */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||
DFFC199827A0D0D7004B7AEF /* NotificationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFFC199727A0D0D7004B7AEF /* NotificationsViewController.swift */; };
|
||||
DFFC199A27A0D32A004B7AEF /* NotificationsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFFC199927A0D32A004B7AEF /* NotificationsTableViewCell.swift */; };
|
||||
DFFC4E7428E95C01006B82AF /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFFC4E7328E95C01006B82AF /* AboutView.swift */; };
|
||||
FF3ABF13232599810074C542 /* ArticleSorterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF09232599450074C542 /* ArticleSorterTests.swift */; };
|
||||
FF3ABF1523259DDB0074C542 /* ArticleSorter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */; };
|
||||
@@ -1148,7 +1161,6 @@
|
||||
176814562564BD0600D98635 /* ArticleItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleItemView.swift; sourceTree = "<group>"; };
|
||||
1768147A2564BE5400D98635 /* widget-sample.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "widget-sample.json"; sourceTree = "<group>"; };
|
||||
176814822564C02A00D98635 /* NetNewsWire_iOS_WidgetExtension.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = NetNewsWire_iOS_WidgetExtension.entitlements; sourceTree = "<group>"; };
|
||||
177A0C2C25454AAB00D7EAF6 /* ReaderAPIAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderAPIAccountViewController.swift; sourceTree = "<group>"; };
|
||||
178A9F9C2549449F00AB7E9D /* AddAccountsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccountsView.swift; sourceTree = "<group>"; };
|
||||
179D280C26F73D83003B2E0A /* ArticleThemePlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleThemePlist.swift; sourceTree = "<group>"; };
|
||||
179DBBA2B22A659F81EED6F9 /* AccountsNewsBlurWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsNewsBlurWindowController.swift; sourceTree = "<group>"; };
|
||||
@@ -1164,8 +1176,6 @@
|
||||
5103A9F624225E4C00410853 /* AccountsAddCloudKitWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsAddCloudKitWindowController.swift; sourceTree = "<group>"; };
|
||||
51077C5727A86D16000C71DB /* Hyperlegible.nnwtheme */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Hyperlegible.nnwtheme; sourceTree = "<group>"; };
|
||||
5108F6B52375E612001ABC45 /* CacheCleaner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheCleaner.swift; sourceTree = "<group>"; };
|
||||
5108F6D12375EED2001ABC45 /* TimelineCustomizerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineCustomizerViewController.swift; sourceTree = "<group>"; };
|
||||
5108F6D32375EEEF001ABC45 /* TimelinePreviewTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinePreviewTableViewController.swift; sourceTree = "<group>"; };
|
||||
5108F6D723763094001ABC45 /* TickMarkSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TickMarkSlider.swift; sourceTree = "<group>"; };
|
||||
510C415C24E5CDE3008226FD /* NetNewsWire Share Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "NetNewsWire Share Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
510C416024E5CDE3008226FD /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
||||
@@ -1174,10 +1184,8 @@
|
||||
510C416624E5CDE3008226FD /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = "<group>"; };
|
||||
510C418724E5D2E3008226FD /* NetNewsWire_shareextension_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_shareextension_target.xcconfig; sourceTree = "<group>"; };
|
||||
510C43F6243D035C009F70C3 /* ExtensionPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionPoint.swift; sourceTree = "<group>"; };
|
||||
510FFAB226EEA22C00F32265 /* ArticleThemesTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleThemesTableViewController.swift; sourceTree = "<group>"; };
|
||||
51102164233A7D6C0007A5F7 /* ArticleExtractorButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleExtractorButton.swift; sourceTree = "<group>"; };
|
||||
51107745243BEE2500D97C8C /* ExtensionPointPreferencesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionPointPreferencesViewController.swift; sourceTree = "<group>"; };
|
||||
5110C37C2373A8D100A9C04F /* InspectorIconHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorIconHeaderView.swift; sourceTree = "<group>"; };
|
||||
51121AA12265430A00BC0EC1 /* NetNewsWire_iOSapp_target.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOSapp_target.xcconfig; sourceTree = "<group>"; };
|
||||
51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RSImage-Extensions.swift"; sourceTree = "<group>"; };
|
||||
5117715424E1EA0F00A2A836 /* ArticleExtractorButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleExtractorButton.swift; sourceTree = "<group>"; };
|
||||
@@ -1190,7 +1198,6 @@
|
||||
512AF9C1236ED52C0066F8BE /* ImageHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageHeaderView.swift; sourceTree = "<group>"; };
|
||||
512AF9DC236F05230066F8BE /* InteractiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveLabel.swift; sourceTree = "<group>"; };
|
||||
512D554323C804DE0023FFFA /* OpenInSafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInSafariActivity.swift; sourceTree = "<group>"; };
|
||||
512DD4C82430086400C17B1F /* CloudKitAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitAccountViewController.swift; sourceTree = "<group>"; };
|
||||
512E08F722688F7C00BDCFDD /* MasterFeedTableViewSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedTableViewSectionHeader.swift; sourceTree = "<group>"; };
|
||||
51314617235A797400387FDC /* NetNewsWire_iOSintentextension_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOSintentextension_target.xcconfig; sourceTree = "<group>"; };
|
||||
51314637235A7BBE00387FDC /* NetNewsWire iOS Intents Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "NetNewsWire iOS Intents Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -1210,7 +1217,6 @@
|
||||
513C5CE8232571C2003D4054 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
||||
513C5CEB232571C2003D4054 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
|
||||
513C5CED232571C2003D4054 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
5141E7382373C18B0013FF27 /* WebFeedInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFeedInspectorViewController.swift; sourceTree = "<group>"; };
|
||||
5141E7552374A2890013FF27 /* DetailIconSchemeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailIconSchemeHandler.swift; sourceTree = "<group>"; };
|
||||
5142192923522B5500E07E2C /* ImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewController.swift; sourceTree = "<group>"; };
|
||||
514219362352510100E07E2C /* ImageScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageScrollView.swift; sourceTree = "<group>"; };
|
||||
@@ -1234,15 +1240,9 @@
|
||||
515D4FCB2325815A00EE1167 /* SafariExt.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = SafariExt.js; sourceTree = "<group>"; };
|
||||
515D4FCD2325909200EE1167 /* NetNewsWire_iOS_ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NetNewsWire_iOS_ShareExtension.entitlements; sourceTree = "<group>"; };
|
||||
515D4FCE2325B3D000EE1167 /* NetNewsWire_iOSshareextension_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOSshareextension_target.xcconfig; sourceTree = "<group>"; };
|
||||
516244E2241E19F000B61C47 /* ColorPaletteTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPaletteTableViewController.swift; sourceTree = "<group>"; };
|
||||
51627A6623861DA3007B3B4B /* MasterFeedViewController+Drag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MasterFeedViewController+Drag.swift"; sourceTree = "<group>"; };
|
||||
51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MasterFeedViewController+Drop.swift"; sourceTree = "<group>"; };
|
||||
51627A92238A3836007B3B4B /* CroppingPreviewParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CroppingPreviewParameters.swift; sourceTree = "<group>"; };
|
||||
516A091D23609A3600EAE89B /* SettingsComboTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SettingsComboTableViewCell.xib; sourceTree = "<group>"; };
|
||||
516A09382360A2AE00EAE89B /* SettingsComboTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsComboTableViewCell.swift; sourceTree = "<group>"; };
|
||||
516A093A2360A4A000EAE89B /* SettingsTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SettingsTableViewCell.xib; sourceTree = "<group>"; };
|
||||
516A093F2361240900EAE89B /* Account.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Account.storyboard; sourceTree = "<group>"; };
|
||||
516A09412361248000EAE89B /* Inspector.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Inspector.storyboard; sourceTree = "<group>"; };
|
||||
516AE5FF246AF34100731738 /* RedditAdd.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = RedditAdd.storyboard; sourceTree = "<group>"; };
|
||||
516AE601246AF36100731738 /* RedditSelectTypeTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedditSelectTypeTableViewController.swift; sourceTree = "<group>"; };
|
||||
516AE603246AF37B00731738 /* RedditSelectAccountTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedditSelectAccountTableViewController.swift; sourceTree = "<group>"; };
|
||||
@@ -1275,16 +1275,7 @@
|
||||
5195C1DB2720BD3000888867 /* MasterFeedRowIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedRowIdentifier.swift; sourceTree = "<group>"; };
|
||||
519B8D322143397200FA689C /* SharingServiceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingServiceDelegate.swift; sourceTree = "<group>"; };
|
||||
519E743422C663F900A78E47 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
519ED455244828C3007F8E94 /* AddExtensionPointViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddExtensionPointViewController.swift; sourceTree = "<group>"; };
|
||||
519ED47924482AEB007F8E94 /* EnableExtensionPointViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnableExtensionPointViewController.swift; sourceTree = "<group>"; };
|
||||
519ED47B24488C6F007F8E94 /* ExtensionInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionInspectorViewController.swift; sourceTree = "<group>"; };
|
||||
51A052CD244FB9D6006C2024 /* AddFeedWIndowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AddFeedWIndowController.swift; path = AddFeed/AddFeedWIndowController.swift; sourceTree = "<group>"; };
|
||||
51A1698F235E10D600EB091F /* LocalAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalAccountViewController.swift; sourceTree = "<group>"; };
|
||||
51A16990235E10D600EB091F /* Settings.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Settings.storyboard; sourceTree = "<group>"; };
|
||||
51A16991235E10D600EB091F /* AccountInspectorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountInspectorViewController.swift; sourceTree = "<group>"; };
|
||||
51A16992235E10D600EB091F /* AddAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddAccountViewController.swift; sourceTree = "<group>"; };
|
||||
51A16993235E10D600EB091F /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
|
||||
51A16996235E10D700EB091F /* FeedbinAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedbinAccountViewController.swift; sourceTree = "<group>"; };
|
||||
51A66684238075AE00CB272D /* AddWebFeedDefaultContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddWebFeedDefaultContainer.swift; sourceTree = "<group>"; };
|
||||
51A9A5E32380C8870033AADF /* ShareFolderPickerAccountCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ShareFolderPickerAccountCell.xib; sourceTree = "<group>"; };
|
||||
51A9A5E52380C8B20033AADF /* ShareFolderPickerFolderCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ShareFolderPickerFolderCell.xib; sourceTree = "<group>"; };
|
||||
@@ -1390,7 +1381,6 @@
|
||||
65ED409F235DEFF00081F399 /* container-migration.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "container-migration.plist"; sourceTree = "<group>"; };
|
||||
65ED40F2235DF5E00081F399 /* NetNewsWire_macapp_target_macappstore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_macapp_target_macappstore.xcconfig; sourceTree = "<group>"; };
|
||||
65ED4186235E045B0081F399 /* NetNewsWire_safariextension_target_macappstore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_safariextension_target_macappstore.xcconfig; sourceTree = "<group>"; };
|
||||
769F2D3643779DB02786278E /* NewsBlurAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurAccountViewController.swift; sourceTree = "<group>"; };
|
||||
8405DD892213E0E3008CE1BF /* DetailContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailContainerView.swift; sourceTree = "<group>"; };
|
||||
8405DD9822153B6B008CE1BF /* TimelineContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineContainerView.swift; sourceTree = "<group>"; };
|
||||
8405DD9B22153BD7008CE1BF /* NSView-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSView-Extensions.swift"; sourceTree = "<group>"; };
|
||||
@@ -1579,15 +1569,46 @@
|
||||
D5F4EDB820074D7C00B9E363 /* Folder+Scriptability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Folder+Scriptability.swift"; sourceTree = "<group>"; };
|
||||
DD82AB09231003F6002269DF /* SharingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharingTests.swift; sourceTree = "<group>"; };
|
||||
DDF9E1D628EDF2FC000BC355 /* notificationSoundBlip.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = notificationSoundBlip.mp3; sourceTree = "<group>"; };
|
||||
DF28B44C294ED52700C4D8CA /* View+DismissOnExternalContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+DismissOnExternalContext.swift"; sourceTree = "<group>"; };
|
||||
DF28B44E294ED92F00C4D8CA /* NewsBlurAddAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsBlurAddAccountView.swift; sourceTree = "<group>"; };
|
||||
DF28B450294EFC6C00C4D8CA /* View+DismissOnAccountAdd.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+DismissOnAccountAdd.swift"; sourceTree = "<group>"; };
|
||||
DF28B452294FE6C600C4D8CA /* EnableExtensionPointView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnableExtensionPointView.swift; sourceTree = "<group>"; };
|
||||
DF28B454294FE74A00C4D8CA /* ExtensionSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionSectionHeader.swift; sourceTree = "<group>"; };
|
||||
DF28B4562950163F00C4D8CA /* EnableExtensionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnableExtensionViewModel.swift; sourceTree = "<group>"; };
|
||||
DF3630EA2936183D00326FB8 /* OPMLDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPMLDocument.swift; sourceTree = "<group>"; };
|
||||
DF3630EE293618A900326FB8 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
|
||||
DF394EFF29357A180081EB6E /* NewArticleNotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewArticleNotificationsView.swift; sourceTree = "<group>"; };
|
||||
DF47CDB1294803AB00FCD57E /* AddExtensionListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddExtensionListView.swift; sourceTree = "<group>"; };
|
||||
DF59F071292085B800ACD33D /* ColorPaletteSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPaletteSelectorView.swift; sourceTree = "<group>"; };
|
||||
DF59F0732920DB5100ACD33D /* AccountsManagementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsManagementView.swift; sourceTree = "<group>"; };
|
||||
DF766FEC29377FD9006FBBE2 /* ExtensionsManagementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionsManagementView.swift; sourceTree = "<group>"; };
|
||||
DF790D6128E990A900455FC7 /* AboutData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutData.swift; sourceTree = "<group>"; };
|
||||
DF84E562295122BA0045C334 /* TimelineCustomizerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineCustomizerView.swift; sourceTree = "<group>"; };
|
||||
DFB34979294A962D00BC81AD /* AddAccountListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccountListView.swift; sourceTree = "<group>"; };
|
||||
DFB3497F294B085100BC81AD /* AccountInspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountInspectorView.swift; sourceTree = "<group>"; };
|
||||
DFB34987294B447F00BC81AD /* InjectedNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InjectedNavigationView.swift; sourceTree = "<group>"; };
|
||||
DFB34989294B45AC00BC81AD /* ExtensionInspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionInspectorView.swift; sourceTree = "<group>"; };
|
||||
DFB3498B294B4CA700BC81AD /* WebFeedInspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFeedInspectorView.swift; sourceTree = "<group>"; };
|
||||
DFB34990294C0B2200BC81AD /* ReaderAPIAddAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderAPIAddAccountView.swift; sourceTree = "<group>"; };
|
||||
DFB34995294C4DCB00BC81AD /* LocalizedNetNewsWireError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedNetNewsWireError.swift; sourceTree = "<group>"; };
|
||||
DFB3499D294C5D5000BC81AD /* CloudKitAddAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitAddAccountView.swift; sourceTree = "<group>"; };
|
||||
DFB3499F294E87B700BC81AD /* LocalAddAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAddAccountView.swift; sourceTree = "<group>"; };
|
||||
DFB349A1294E90B500BC81AD /* FeedbinAddAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAddAccountView.swift; sourceTree = "<group>"; };
|
||||
DFB349A3294E914D00BC81AD /* AccountSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSectionHeader.swift; sourceTree = "<group>"; };
|
||||
DFBB4EAB2951BC0200639228 /* NNWThemeDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NNWThemeDocument.swift; sourceTree = "<group>"; };
|
||||
DFBB4EAF2951BCAC00639228 /* ArticleThemeManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleThemeManagerView.swift; sourceTree = "<group>"; };
|
||||
DFC14F0E28EA55BD00F6EE86 /* AboutWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutWindowController.swift; sourceTree = "<group>"; };
|
||||
DFC14F1428EB177000F6EE86 /* AboutNetNewsWireView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutNetNewsWireView.swift; sourceTree = "<group>"; };
|
||||
DFC14F1628EB17A800F6EE86 /* CreditsNetNewsWireView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditsNetNewsWireView.swift; sourceTree = "<group>"; };
|
||||
DFCE4F9028EF26F000405869 /* About.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = About.plist; sourceTree = "<group>"; };
|
||||
DFCE4F9328EF278300405869 /* Thanks.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = Thanks.md; sourceTree = "<group>"; };
|
||||
DFD406F4291F79C900C02962 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
DFD406F6291FB1A600C02962 /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = "<group>"; };
|
||||
DFD406F9291FB5E400C02962 /* SettingsRows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsRows.swift; sourceTree = "<group>"; };
|
||||
DFD406FB291FB63B00C02962 /* SettingsHelpSheets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsHelpSheets.swift; sourceTree = "<group>"; };
|
||||
DFD406FE291FDC0C00C02962 /* DisplayAndBehaviorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayAndBehaviorsView.swift; sourceTree = "<group>"; };
|
||||
DFD6AACB27ADE80900463FAD /* NewsFax.nnwtheme */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = NewsFax.nnwtheme; sourceTree = "<group>"; };
|
||||
DFFC199727A0D0D7004B7AEF /* NotificationsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsViewController.swift; sourceTree = "<group>"; };
|
||||
DFFC199927A0D32A004B7AEF /* NotificationsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DFE522A22953DEF400376B77 /* CustomInsetGroupedRowStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomInsetGroupedRowStyle.swift; sourceTree = "<group>"; };
|
||||
DFFC4E7328E95C01006B82AF /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
|
||||
FF3ABF09232599450074C542 /* ArticleSorterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleSorterTests.swift; sourceTree = "<group>"; };
|
||||
FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleSorter.swift; sourceTree = "<group>"; };
|
||||
@@ -1855,11 +1876,9 @@
|
||||
5123DB95233EC69300282CC9 /* Inspector */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
516A09412361248000EAE89B /* Inspector.storyboard */,
|
||||
51A16991235E10D600EB091F /* AccountInspectorViewController.swift */,
|
||||
519ED47B24488C6F007F8E94 /* ExtensionInspectorViewController.swift */,
|
||||
5110C37C2373A8D100A9C04F /* InspectorIconHeaderView.swift */,
|
||||
5141E7382373C18B0013FF27 /* WebFeedInspectorViewController.swift */,
|
||||
DFB3497F294B085100BC81AD /* AccountInspectorView.swift */,
|
||||
DFB34989294B45AC00BC81AD /* ExtensionInspectorView.swift */,
|
||||
DFB3498B294B4CA700BC81AD /* WebFeedInspectorView.swift */,
|
||||
);
|
||||
path = Inspector;
|
||||
sourceTree = "<group>";
|
||||
@@ -1929,12 +1948,11 @@
|
||||
516A093E236123A800EAE89B /* Account */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
516A093F2361240900EAE89B /* Account.storyboard */,
|
||||
51A1698F235E10D600EB091F /* LocalAccountViewController.swift */,
|
||||
512DD4C82430086400C17B1F /* CloudKitAccountViewController.swift */,
|
||||
51A16996235E10D700EB091F /* FeedbinAccountViewController.swift */,
|
||||
769F2D3643779DB02786278E /* NewsBlurAccountViewController.swift */,
|
||||
177A0C2C25454AAB00D7EAF6 /* ReaderAPIAccountViewController.swift */,
|
||||
DFB3499F294E87B700BC81AD /* LocalAddAccountView.swift */,
|
||||
DFB3499D294C5D5000BC81AD /* CloudKitAddAccountView.swift */,
|
||||
DFB349A1294E90B500BC81AD /* FeedbinAddAccountView.swift */,
|
||||
DF28B44E294ED92F00C4D8CA /* NewsBlurAddAccountView.swift */,
|
||||
DFB34990294C0B2200BC81AD /* ReaderAPIAddAccountView.swift */,
|
||||
);
|
||||
path = Account;
|
||||
sourceTree = "<group>";
|
||||
@@ -1964,22 +1982,12 @@
|
||||
5183CCEB227117C70010922C /* Settings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DFFC4E7328E95C01006B82AF /* AboutView.swift */,
|
||||
51A16992235E10D600EB091F /* AddAccountViewController.swift */,
|
||||
519ED455244828C3007F8E94 /* AddExtensionPointViewController.swift */,
|
||||
DF766FEB2936344D006FBBE2 /* General */,
|
||||
DFD406FD291FDBD900C02962 /* Appearance */,
|
||||
DF59F0752920E42000ACD33D /* Account and Extensions */,
|
||||
DF3630E92936038400326FB8 /* New Article Notifications */,
|
||||
DF766FEA2936337A006FBBE2 /* Help */,
|
||||
5137C2E926F63AE6009EFEDB /* ArticleThemeImporter.swift */,
|
||||
510FFAB226EEA22C00F32265 /* ArticleThemesTableViewController.swift */,
|
||||
516244E2241E19F000B61C47 /* ColorPaletteTableViewController.swift */,
|
||||
519ED47924482AEB007F8E94 /* EnableExtensionPointViewController.swift */,
|
||||
51A16990235E10D600EB091F /* Settings.storyboard */,
|
||||
516A09382360A2AE00EAE89B /* SettingsComboTableViewCell.swift */,
|
||||
516A091D23609A3600EAE89B /* SettingsComboTableViewCell.xib */,
|
||||
516A093A2360A4A000EAE89B /* SettingsTableViewCell.xib */,
|
||||
51A16993235E10D600EB091F /* SettingsViewController.swift */,
|
||||
5108F6D12375EED2001ABC45 /* TimelineCustomizerViewController.swift */,
|
||||
5108F6D32375EEEF001ABC45 /* TimelinePreviewTableViewController.swift */,
|
||||
DFFC199727A0D0D7004B7AEF /* NotificationsViewController.swift */,
|
||||
DFFC199927A0D32A004B7AEF /* NotificationsTableViewCell.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
@@ -2030,6 +2038,7 @@
|
||||
5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */,
|
||||
5183CCCF226E1E880010922C /* NonIntrinsicLabel.swift */,
|
||||
51A9A6092382FD240033AADF /* PoppableGestureRecognizerDelegate.swift */,
|
||||
DFD406F6291FB1A600C02962 /* SafariView.swift */,
|
||||
51C45250226506F400C03939 /* String-Extensions.swift */,
|
||||
5108F6D723763094001ABC45 /* TickMarkSlider.swift */,
|
||||
C5A6ED6C23C9B0C800AB6BE2 /* UIActivityViewController-Extensions.swift */,
|
||||
@@ -2123,6 +2132,7 @@
|
||||
51C452802265093600C03939 /* Add */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DFB3497E294B07D900BC81AD /* Views */,
|
||||
51C452822265093600C03939 /* Add.storyboard */,
|
||||
51E4397F23805EBC00015C31 /* AddComboTableViewCell.swift */,
|
||||
51E43961238037C400015C31 /* AddFeedFolderViewController.swift */,
|
||||
@@ -2578,6 +2588,7 @@
|
||||
511D43CE231FA51100FB1562 /* Resources */,
|
||||
176813A22564B9D100D98635 /* Widget */,
|
||||
173A64162547BE0900267F6E /* AccountType+Helpers.swift */,
|
||||
DFB34985294B3B0800BC81AD /* Localizations */,
|
||||
);
|
||||
path = Shared;
|
||||
sourceTree = "<group>";
|
||||
@@ -2689,6 +2700,7 @@
|
||||
5123DB95233EC69300282CC9 /* Inspector */,
|
||||
513145F9235A55A700387FDC /* Intents */,
|
||||
5183CCEB227117C70010922C /* Settings */,
|
||||
DFB34986294B446300BC81AD /* SwiftUI Extensions */,
|
||||
51C45245226506C800C03939 /* UIKit Extensions */,
|
||||
513C5CE7232571C2003D4054 /* ShareExtension */,
|
||||
51314643235A7C2300387FDC /* IntentsExtension */,
|
||||
@@ -2726,6 +2738,8 @@
|
||||
children = (
|
||||
849A97591ED9EB0D007D329B /* DefaultFeedsImporter.swift */,
|
||||
84A3EE52223B667F00557320 /* DefaultFeeds.opml */,
|
||||
DF3630EA2936183D00326FB8 /* OPMLDocument.swift */,
|
||||
DFBB4EAB2951BC0200639228 /* NNWThemeDocument.swift */,
|
||||
);
|
||||
path = Importers;
|
||||
sourceTree = "<group>";
|
||||
@@ -2841,6 +2855,90 @@
|
||||
path = Scriptability;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DF3630E92936038400326FB8 /* New Article Notifications */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DF394EFF29357A180081EB6E /* NewArticleNotificationsView.swift */,
|
||||
);
|
||||
path = "New Article Notifications";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DF59F0752920E42000ACD33D /* Account and Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DFB3497B294AA95200BC81AD /* Accounts */,
|
||||
DFB3497C294AA95A00BC81AD /* Extensions */,
|
||||
);
|
||||
path = "Account and Extensions";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DF766FEA2936337A006FBBE2 /* Help */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DFFC4E7328E95C01006B82AF /* AboutView.swift */,
|
||||
DFD406FB291FB63B00C02962 /* SettingsHelpSheets.swift */,
|
||||
);
|
||||
path = Help;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DF766FEB2936344D006FBBE2 /* General */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DFD406F4291F79C900C02962 /* SettingsView.swift */,
|
||||
DF3630EE293618A900326FB8 /* SettingsViewModel.swift */,
|
||||
DFD406F9291FB5E400C02962 /* SettingsRows.swift */,
|
||||
);
|
||||
path = General;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DFB3497B294AA95200BC81AD /* Accounts */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DF59F0732920DB5100ACD33D /* AccountsManagementView.swift */,
|
||||
DFB34979294A962D00BC81AD /* AddAccountListView.swift */,
|
||||
);
|
||||
path = Accounts;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DFB3497C294AA95A00BC81AD /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DF766FEC29377FD9006FBBE2 /* ExtensionsManagementView.swift */,
|
||||
DF47CDB1294803AB00FCD57E /* AddExtensionListView.swift */,
|
||||
DF28B452294FE6C600C4D8CA /* EnableExtensionPointView.swift */,
|
||||
DF28B4562950163F00C4D8CA /* EnableExtensionViewModel.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DFB3497E294B07D900BC81AD /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DFB34985294B3B0800BC81AD /* Localizations */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DFB34995294C4DCB00BC81AD /* LocalizedNetNewsWireError.swift */,
|
||||
);
|
||||
path = Localizations;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DFB34986294B446300BC81AD /* SwiftUI Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DFB349A3294E914D00BC81AD /* AccountSectionHeader.swift */,
|
||||
DFE522A22953DEF400376B77 /* CustomInsetGroupedRowStyle.swift */,
|
||||
DF28B454294FE74A00C4D8CA /* ExtensionSectionHeader.swift */,
|
||||
DFB34987294B447F00BC81AD /* InjectedNavigationView.swift */,
|
||||
DF28B450294EFC6C00C4D8CA /* View+DismissOnAccountAdd.swift */,
|
||||
DF28B44C294ED52700C4D8CA /* View+DismissOnExternalContext.swift */,
|
||||
);
|
||||
path = "SwiftUI Extensions";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DFC14F0928EA51AB00F6EE86 /* About */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -2851,6 +2949,17 @@
|
||||
path = About;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DFD406FD291FDBD900C02962 /* Appearance */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DFD406FE291FDC0C00C02962 /* DisplayAndBehaviorsView.swift */,
|
||||
DF59F071292085B800ACD33D /* ColorPaletteSelectorView.swift */,
|
||||
DF84E562295122BA0045C334 /* TimelineCustomizerView.swift */,
|
||||
DFBB4EAF2951BCAC00639228 /* ArticleThemeManagerView.swift */,
|
||||
);
|
||||
path = Appearance;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@@ -3413,23 +3522,20 @@
|
||||
511D43D2231FA62C00FB1562 /* GlobalKeyboardShortcuts.plist in Resources */,
|
||||
84C9FCA12262A1B300D921D6 /* Main.storyboard in Resources */,
|
||||
51BB7C312335ACDE008E8144 /* page.html in Resources */,
|
||||
512392C324E3451400F11704 /* TwitterAdd.storyboard in Resources */,
|
||||
516A093723609A3600EAE89B /* SettingsComboTableViewCell.xib in Resources */,
|
||||
51077C5A27A86D16000C71DB /* Hyperlegible.nnwtheme in Resources */,
|
||||
516A09422361248000EAE89B /* Inspector.storyboard in Resources */,
|
||||
DDF9E1D928EDF2FC000BC355 /* notificationSoundBlip.mp3 in Resources */,
|
||||
51DEE81A26FBFF84006DAA56 /* Promenade.nnwtheme in Resources */,
|
||||
1768140B2564BB8300D98635 /* NetNewsWire_iOSwidgetextension_target.xcconfig in Resources */,
|
||||
5103A9B424216A4200410853 /* blank.html in Resources */,
|
||||
51D0214826ED617100FF2E0F /* core.css in Resources */,
|
||||
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 */,
|
||||
511D43CF231FA62200FB1562 /* DetailKeyboardShortcuts.plist in Resources */,
|
||||
51A1699A235E10D700EB091F /* Settings.storyboard in Resources */,
|
||||
49F40DF92335B71000552BF4 /* newsfoot.js in Resources */,
|
||||
512392C024E33A3C00F11704 /* RedditAdd.storyboard in Resources */,
|
||||
5177C21327B07CFE00643901 /* NewsFax.nnwtheme in Resources */,
|
||||
@@ -3813,6 +3919,7 @@
|
||||
65ED3FBD235DEF6C0081F399 /* AppDefaults.swift in Sources */,
|
||||
65ED3FBE235DEF6C0081F399 /* Account+Scriptability.swift in Sources */,
|
||||
65ED3FBF235DEF6C0081F399 /* NothingInspectorViewController.swift in Sources */,
|
||||
DF3630EC2936183D00326FB8 /* OPMLDocument.swift in Sources */,
|
||||
1710B92A255246F900679C0D /* EnableExtensionPointHelpView.swift in Sources */,
|
||||
51927A0528E28D1C000AE856 /* MainWindow.swift in Sources */,
|
||||
65ED3FC0235DEF6C0081F399 /* AppNotifications.swift in Sources */,
|
||||
@@ -3841,6 +3948,7 @@
|
||||
65ED3FD5235DEF6C0081F399 /* SmartFeed.swift in Sources */,
|
||||
51333D1724685D2E00EB5C91 /* AddRedditFeedWindowController.swift in Sources */,
|
||||
65ED3FD6235DEF6C0081F399 /* MarkStatusCommand.swift in Sources */,
|
||||
DFBB4EAD2951BC0200639228 /* NNWThemeDocument.swift in Sources */,
|
||||
5183CFB0254C78C8006B83A5 /* EnableExtensionPointView.swift in Sources */,
|
||||
65ED3FD7235DEF6C0081F399 /* NSApplication+Scriptability.swift in Sources */,
|
||||
65ED3FD8235DEF6C0081F399 /* NSView-Extensions.swift in Sources */,
|
||||
@@ -3993,18 +4101,22 @@
|
||||
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 */,
|
||||
51EF0F79227716380050506E /* ColorHash.swift in Sources */,
|
||||
DF28B4572950163F00C4D8CA /* EnableExtensionViewModel.swift in Sources */,
|
||||
DF59F072292085B800ACD33D /* ColorPaletteSelectorView.swift in Sources */,
|
||||
51F9F3FB23DFB25700A314FD /* Animations.swift in Sources */,
|
||||
DFB34988294B447F00BC81AD /* InjectedNavigationView.swift in Sources */,
|
||||
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 */,
|
||||
DFFC199827A0D0D7004B7AEF /* NotificationsViewController.swift in Sources */,
|
||||
DFD406FF291FDC0C00C02962 /* DisplayAndBehaviorsView.swift in Sources */,
|
||||
51A9A5F52380F6A60033AADF /* ModalNavigationController.swift in Sources */,
|
||||
DFBB4EAE2951BC0200639228 /* NNWThemeDocument.swift in Sources */,
|
||||
51EAED96231363EF00A9EEE3 /* NonIntrinsicButton.swift in Sources */,
|
||||
51C4527B2265091600C03939 /* MasterUnreadIndicatorView.swift in Sources */,
|
||||
5186A635235EF3A800C97195 /* VibrantLabel.swift in Sources */,
|
||||
@@ -4015,23 +4127,22 @@
|
||||
51C45291226509C800C03939 /* SmartFeed.swift in Sources */,
|
||||
51C452A722650A3D00C03939 /* RSImage-Extensions.swift in Sources */,
|
||||
511B9807237DCAC90028BCAA /* UserInfoKey.swift in Sources */,
|
||||
DFFC199A27A0D32A004B7AEF /* NotificationsTableViewCell.swift in Sources */,
|
||||
51C45269226508F600C03939 /* MasterFeedTableViewCell.swift in Sources */,
|
||||
51F85BFD2275DCA800C787DC /* SingleLineUILabelSizer.swift in Sources */,
|
||||
51E43962238037C400015C31 /* AddFeedFolderViewController.swift in Sources */,
|
||||
519ED47A24482AEB007F8E94 /* EnableExtensionPointViewController.swift in Sources */,
|
||||
51C4528F226509BD00C03939 /* UnreadFeed.swift in Sources */,
|
||||
51FD413B2342BD0500880194 /* MasterTimelineUnreadCountView.swift in Sources */,
|
||||
DFB3497A294A962D00BC81AD /* AddAccountListView.swift in Sources */,
|
||||
513146B2235A81A400387FDC /* AddWebFeedIntentHandler.swift in Sources */,
|
||||
51D87EE12311D34700E63F03 /* ActivityType.swift in Sources */,
|
||||
51C452772265091600C03939 /* MultilineUILabelSizer.swift in Sources */,
|
||||
51C452A522650A2D00C03939 /* SmallIconProvider.swift in Sources */,
|
||||
DF28B453294FE6C600C4D8CA /* EnableExtensionPointView.swift in Sources */,
|
||||
51AB8AB323B7F4C6008F147D /* WebViewController.swift in Sources */,
|
||||
516A09392360A2AE00EAE89B /* SettingsComboTableViewCell.swift in Sources */,
|
||||
DFD406F7291FB1A600C02962 /* SafariView.swift in Sources */,
|
||||
DF3630ED2936183D00326FB8 /* OPMLDocument.swift in Sources */,
|
||||
176813D22564BA5900D98635 /* WidgetDataEncoder.swift in Sources */,
|
||||
51D5948722668EFA00DFC836 /* MarkStatusCommand.swift in Sources */,
|
||||
51A1699C235E10D700EB091F /* AddAccountViewController.swift in Sources */,
|
||||
51A16999235E10D700EB091F /* LocalAccountViewController.swift in Sources */,
|
||||
176813D12564BA5900D98635 /* WidgetDataDecoder.swift in Sources */,
|
||||
176813D02564BA5900D98635 /* WidgetData.swift in Sources */,
|
||||
510289CD24519A1D00426DDF /* SelectComboTableViewCell.swift in Sources */,
|
||||
@@ -4046,20 +4157,24 @@
|
||||
51C4526B226508F600C03939 /* MasterFeedViewController.swift in Sources */,
|
||||
5126EE97226CB48A00C22AFC /* SceneCoordinator.swift in Sources */,
|
||||
84CAFCB022BC8C35007694F0 /* FetchRequestOperation.swift in Sources */,
|
||||
DFD406FA291FB5E400C02962 /* SettingsRows.swift in Sources */,
|
||||
DFB3499E294C5D5000BC81AD /* CloudKitAddAccountView.swift in Sources */,
|
||||
DFB3498C294B4CA700BC81AD /* WebFeedInspectorView.swift in Sources */,
|
||||
DFB349A0294E87B700BC81AD /* LocalAddAccountView.swift in Sources */,
|
||||
5193CD5A245E44A90092735E /* RedditFeedProvider-Extensions.swift in Sources */,
|
||||
51EF0F77227716200050506E /* FaviconGenerator.swift in Sources */,
|
||||
51938DF3231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift in Sources */,
|
||||
51C4525A226508D600C03939 /* UIStoryboard-Extensions.swift in Sources */,
|
||||
517A745B2443665000B553B9 /* UIPageViewController-Extensions.swift in Sources */,
|
||||
DF28B455294FE74A00C4D8CA /* ExtensionSectionHeader.swift in Sources */,
|
||||
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 */,
|
||||
5F323809231DF9F000706F6B /* VibrantTableViewCell.swift in Sources */,
|
||||
51FE10042345529D0056195D /* UserNotificationManager.swift in Sources */,
|
||||
519ED47C24488C6F007F8E94 /* ExtensionInspectorViewController.swift in Sources */,
|
||||
51C4CFF224D37D1F00AF9874 /* Secrets.swift in Sources */,
|
||||
51C452A022650A1900C03939 /* WebFeedIconDownloader.swift in Sources */,
|
||||
51C4529E22650A1900C03939 /* ImageDownloader.swift in Sources */,
|
||||
@@ -4071,17 +4186,19 @@
|
||||
51B5C87B23F2317700032075 /* ExtensionFeedAddRequest.swift in Sources */,
|
||||
51627A93238A3836007B3B4B /* CroppingPreviewParameters.swift in Sources */,
|
||||
512AF9DD236F05230066F8BE /* InteractiveLabel.swift in Sources */,
|
||||
DFB3498A294B45AC00BC81AD /* ExtensionInspectorView.swift in Sources */,
|
||||
51E3EB3D229AB08300645299 /* ErrorHandler.swift in Sources */,
|
||||
5183CCE5226F4DFA0010922C /* RefreshInterval.swift in Sources */,
|
||||
51C4529D22650A1000C03939 /* FaviconURLFinder.swift in Sources */,
|
||||
5142192A23522B5500E07E2C /* ImageViewController.swift in Sources */,
|
||||
516244E3241E19F000B61C47 /* ColorPaletteTableViewController.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 */,
|
||||
51C4529A22650A0400C03939 /* ArticleTheme.swift in Sources */,
|
||||
51C4527F2265092C00C03939 /* ArticleViewController.swift in Sources */,
|
||||
DFBB4EB02951BCAC00639228 /* ArticleThemeManagerView.swift in Sources */,
|
||||
51C4526A226508F600C03939 /* MasterFeedTableViewCellLayout.swift in Sources */,
|
||||
51C452AE2265104D00C03939 /* ArticleStringFormatter.swift in Sources */,
|
||||
512E08E62268800D00BDCFDD /* FolderTreeControllerDelegate.swift in Sources */,
|
||||
@@ -4096,61 +4213,68 @@
|
||||
51C9DE5823EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift in Sources */,
|
||||
51B5C87D23F2346200032075 /* ExtensionContainersFile.swift in Sources */,
|
||||
51102165233A7D6C0007A5F7 /* ArticleExtractorButton.swift in Sources */,
|
||||
5141E7392373C18B0013FF27 /* WebFeedInspectorViewController.swift in Sources */,
|
||||
C5A6ED6D23C9B0C800AB6BE2 /* UIActivityViewController-Extensions.swift in Sources */,
|
||||
5108F6D42375EEEF001ABC45 /* TimelinePreviewTableViewController.swift in Sources */,
|
||||
84CAFCA522BC8C08007694F0 /* FetchRequestQueue.swift in Sources */,
|
||||
512392BE24E33A3C00F11704 /* RedditSelectAccountTableViewController.swift in Sources */,
|
||||
515A517B243E90260089E588 /* ExtensionPoint.swift in Sources */,
|
||||
51C4529C22650A1000C03939 /* SingleFaviconDownloader.swift in Sources */,
|
||||
17D643B226F8A436008D4C05 /* ArticleThemeDownloader.swift in Sources */,
|
||||
DFB34997294C4DCB00BC81AD /* LocalizedNetNewsWireError.swift in Sources */,
|
||||
51E595A6228CC36500FCC42B /* ArticleStatusSyncTimer.swift in Sources */,
|
||||
51F9F3F723DF6DB200A314FD /* ArticleIconSchemeHandler.swift in Sources */,
|
||||
512AF9C2236ED52C0066F8BE /* ImageHeaderView.swift in Sources */,
|
||||
512392C124E33A3C00F11704 /* RedditSelectTypeTableViewController.swift in Sources */,
|
||||
515A5181243E90260089E588 /* ExtensionPointIdentifer.swift in Sources */,
|
||||
DF766FED29377FD9006FBBE2 /* ExtensionsManagementView.swift in Sources */,
|
||||
51C45290226509C100C03939 /* PseudoFeed.swift in Sources */,
|
||||
51C452A922650DC600C03939 /* ArticleRenderer.swift in Sources */,
|
||||
51C45297226509E300C03939 /* DefaultFeedsImporter.swift in Sources */,
|
||||
512E094D2268B8AB00BDCFDD /* DeleteCommand.swift in Sources */,
|
||||
5110C37D2373A8D100A9C04F /* InspectorIconHeaderView.swift in Sources */,
|
||||
DFD406FC291FB63B00C02962 /* SettingsHelpSheets.swift in Sources */,
|
||||
51F85BFB2275D85000C787DC /* Array-Extensions.swift in Sources */,
|
||||
51C452AC22650FD200C03939 /* AppNotifications.swift in Sources */,
|
||||
51EF0F7E2277A57D0050506E /* MasterTimelineAccessibilityCellLayout.swift in Sources */,
|
||||
51A1699B235E10D700EB091F /* AccountInspectorViewController.swift in Sources */,
|
||||
512D554423C804DE0023FFFA /* OpenInSafariActivity.swift in Sources */,
|
||||
DF47CDB2294803AB00FCD57E /* AddExtensionListView.swift in Sources */,
|
||||
DFB34994294C0E3900BC81AD /* ReaderAPIAddAccountView.swift in Sources */,
|
||||
512392C224E33A3C00F11704 /* RedditEnterDetailTableViewController.swift in Sources */,
|
||||
51C452762265091600C03939 /* MasterTimelineViewController.swift in Sources */,
|
||||
5195C1DC2720BD3000888867 /* MasterFeedRowIdentifier.swift in Sources */,
|
||||
5108F6D823763094001ABC45 /* TickMarkSlider.swift in Sources */,
|
||||
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 */,
|
||||
DF59F0742920DB5100ACD33D /* AccountsManagementView.swift in Sources */,
|
||||
518651DA235621840078E021 /* ImageTransition.swift in Sources */,
|
||||
51C266EA238C334800F53014 /* ContextMenuPreviewViewController.swift in Sources */,
|
||||
173A642C2547BE9600267F6E /* AccountType+Helpers.swift in Sources */,
|
||||
DFE522A32953DEF400376B77 /* CustomInsetGroupedRowStyle.swift in Sources */,
|
||||
51627A6923861DED007B3B4B /* MasterFeedViewController+Drop.swift in Sources */,
|
||||
514219372352510100E07E2C /* ImageScrollView.swift in Sources */,
|
||||
516AE9B32371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.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 */,
|
||||
C5A6ED5223C9AF4300AB6BE2 /* TitleActivityItemSource.swift in Sources */,
|
||||
17071EF126F8137400F5E71D /* ArticleTheme+Notifications.swift in Sources */,
|
||||
51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */,
|
||||
84DEE56622C32CA4005FC42C /* SmartFeedDelegate.swift in Sources */,
|
||||
DF28B44F294ED92F00C4D8CA /* NewsBlurAddAccountView.swift in Sources */,
|
||||
DF84E563295122BA0045C334 /* TimelineCustomizerView.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 */,
|
||||
51C4529F22650A1900C03939 /* AuthorAvatarDownloader.swift in Sources */,
|
||||
5108F6D22375EED2001ABC45 /* TimelineCustomizerViewController.swift in Sources */,
|
||||
519E743D22C663F900A78E47 /* SceneDelegate.swift in Sources */,
|
||||
FFD43E412340F488009E5CA3 /* MarkAsReadAlertController.swift in Sources */,
|
||||
51C452A322650A1E00C03939 /* HTMLMetadataDownloader.swift in Sources */,
|
||||
@@ -4161,12 +4285,9 @@
|
||||
51627A6723861DA3007B3B4B /* MasterFeedViewController+Drag.swift in Sources */,
|
||||
51FFF0C4235EE8E5002762AA /* VibrantButton.swift in Sources */,
|
||||
51C45259226508D300C03939 /* AppDefaults.swift in Sources */,
|
||||
510FFAB326EEA22C00F32265 /* ArticleThemesTableViewController.swift in Sources */,
|
||||
511D4419231FC02D00FB1562 /* KeyboardManager.swift in Sources */,
|
||||
51A1699D235E10D700EB091F /* SettingsViewController.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;
|
||||
@@ -4267,12 +4388,14 @@
|
||||
84AD1EBC2032AF5C00BC20B7 /* SidebarOutlineDataSource.swift in Sources */,
|
||||
845A29241FC9255E007B49E3 /* SidebarCellAppearance.swift in Sources */,
|
||||
845EE7B11FC2366500854A1F /* StarredFeedDelegate.swift in Sources */,
|
||||
DFBB4EAC2951BC0200639228 /* NNWThemeDocument.swift in Sources */,
|
||||
DFC14F1228EA5DC500F6EE86 /* AboutData.swift in Sources */,
|
||||
848F6AE51FC29CFB002D422E /* FaviconDownloader.swift in Sources */,
|
||||
511B9806237DCAC90028BCAA /* UserInfoKey.swift in Sources */,
|
||||
84C9FC7722629E1200D921D6 /* AdvancedPreferencesViewController.swift in Sources */,
|
||||
849EE72120391F560082A1EA /* SharingServicePickerDelegate.swift in Sources */,
|
||||
1710B9132552354E00679C0D /* AddAccountHelpView.swift in Sources */,
|
||||
DFB34996294C4DCB00BC81AD /* LocalizedNetNewsWireError.swift in Sources */,
|
||||
51D205EF28E3CF8D007C46EF /* LinkTextField.swift in Sources */,
|
||||
5108F6B62375E612001ABC45 /* CacheCleaner.swift in Sources */,
|
||||
849A97981ED9EFAA007D329B /* Node-Extensions.swift in Sources */,
|
||||
@@ -4321,6 +4444,7 @@
|
||||
51FA73A72332BE880090D516 /* ExtractedArticle.swift in Sources */,
|
||||
84B99C9D1FAE83C600ECDEDB /* DeleteCommand.swift in Sources */,
|
||||
849A97541ED9EAC0007D329B /* AddWebFeedWindowController.swift in Sources */,
|
||||
DF3630EB2936183D00326FB8 /* OPMLDocument.swift in Sources */,
|
||||
5144EA40227A37EC00D19003 /* ImportOPMLWindowController.swift in Sources */,
|
||||
178A9F9D2549449F00AB7E9D /* AddAccountsView.swift in Sources */,
|
||||
51C4CFF024D37D1F00AF9874 /* Secrets.swift in Sources */,
|
||||
|
||||
@@ -12,6 +12,7 @@ import Articles
|
||||
extension Notification.Name {
|
||||
static let InspectableObjectsDidChange = Notification.Name("TimelineSelectionDidChangeNotification")
|
||||
static let UserDidAddFeed = Notification.Name("UserDidAddFeedNotification")
|
||||
static let LaunchedFromExternalAction = Notification.Name("LaunchedFromExternalAction")
|
||||
|
||||
#if !MAC_APP_STORE
|
||||
static let WebInspectorEnabledDidChange = Notification.Name("WebInspectorEnabledDidChange")
|
||||
|
||||
@@ -13,13 +13,17 @@ import UIKit
|
||||
#endif
|
||||
|
||||
import RSCore
|
||||
import Combine
|
||||
#if canImport(AppKit)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
public extension Notification.Name {
|
||||
static let ArticleThemeNamesDidChangeNotification = Notification.Name("ArticleThemeNamesDidChangeNotification")
|
||||
static let CurrentArticleThemeDidChangeNotification = Notification.Name("CurrentArticleThemeDidChangeNotification")
|
||||
}
|
||||
|
||||
final class ArticleThemesManager: NSObject, NSFilePresenter, Logging {
|
||||
final class ArticleThemesManager: NSObject, NSFilePresenter, Logging, ObservableObject {
|
||||
|
||||
static var shared: ArticleThemesManager!
|
||||
public let folderPath: String
|
||||
@@ -36,6 +40,7 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging {
|
||||
do {
|
||||
currentTheme = try articleThemeWithThemeName(newValue)
|
||||
AppDefaults.shared.currentThemeName = newValue
|
||||
objectWillChange.send()
|
||||
updateFilePresenter()
|
||||
} catch {
|
||||
logger.error("Unable to set new theme: \(error.localizedDescription, privacy: .public)")
|
||||
@@ -54,12 +59,14 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging {
|
||||
}() {
|
||||
didSet {
|
||||
NotificationCenter.default.post(name: .CurrentArticleThemeDidChangeNotification, object: self)
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
lazy var themeNames = { buildThemeNames() }() {
|
||||
didSet {
|
||||
NotificationCenter.default.post(name: .ArticleThemeNamesDidChangeNotification, object: self)
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +120,7 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging {
|
||||
}
|
||||
|
||||
try FileManager.default.copyItem(atPath: filename, toPath: toFilename)
|
||||
objectWillChange.send()
|
||||
|
||||
themeNames = buildThemeNames()
|
||||
}
|
||||
@@ -136,6 +144,73 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging {
|
||||
|
||||
return try ArticleTheme(path: path, isAppTheme: isAppTheme)
|
||||
}
|
||||
|
||||
func themesByDeveloper() -> (builtIn: [ArticleTheme], other: [ArticleTheme]) {
|
||||
let installedProvidedThemes = themeNames.map({ try? articleThemeWithThemeName($0) }).compactMap({ $0 }).filter({ $0.isAppTheme }).sorted(by: { $0.name < $1.name }).filter({ $0.name != AppDefaults.defaultThemeName })
|
||||
|
||||
let installedOtherThemes = themeNames.map({ try? articleThemeWithThemeName($0) }).compactMap({ $0 }).filter({ !$0.isAppTheme }).sorted(by: { $0.name < $1.name })
|
||||
|
||||
return (installedProvidedThemes, installedOtherThemes)
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
func articleThemesMenu(for popUpButton: NSPopUpButton?) -> NSMenu {
|
||||
let menu = NSMenu()
|
||||
menu.autoenablesItems = false
|
||||
menu.removeAllItems()
|
||||
|
||||
let defaultMenuItem = NSMenuItem()
|
||||
defaultMenuItem.title = ArticleTheme.defaultTheme.name
|
||||
defaultMenuItem.action = #selector(updateThemeSelection(_:))
|
||||
defaultMenuItem.state = currentTheme.name == defaultMenuItem.title ? .on : .off
|
||||
defaultMenuItem.target = self
|
||||
menu.addItem(defaultMenuItem)
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
let rancheroHeading = NSMenuItem(title: "Built-in Themes", action: nil, keyEquivalent: "")
|
||||
rancheroHeading.attributedTitle = NSAttributedString(string: "Built-in Themes", attributes: [NSAttributedString.Key.foregroundColor : NSColor.secondaryLabelColor, NSAttributedString.Key.font: NSFont.boldSystemFont(ofSize: 12)])
|
||||
rancheroHeading.isEnabled = false
|
||||
menu.addItem(rancheroHeading)
|
||||
|
||||
let installedThemes = ArticleThemesManager.shared.themesByDeveloper()
|
||||
|
||||
for theme in installedThemes.0 {
|
||||
let item = NSMenuItem()
|
||||
item.title = theme.name
|
||||
item.action = #selector(updateThemeSelection(_:))
|
||||
item.state = currentTheme.name == theme.name ? .on : .off
|
||||
item.target = self
|
||||
menu.addItem(item)
|
||||
}
|
||||
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
let thirdPartyHeading = NSMenuItem(title: "Other Themes", action: nil, keyEquivalent: "")
|
||||
thirdPartyHeading.attributedTitle = NSAttributedString(string: "Other Themes", attributes: [NSAttributedString.Key.foregroundColor : NSColor.secondaryLabelColor, NSAttributedString.Key.font: NSFont.boldSystemFont(ofSize: 12)])
|
||||
thirdPartyHeading.isEnabled = false
|
||||
menu.addItem(thirdPartyHeading)
|
||||
|
||||
for theme in installedThemes.1 {
|
||||
let item = NSMenuItem()
|
||||
item.title = theme.name
|
||||
item.action = #selector(updateThemeSelection(_:))
|
||||
item.state = currentTheme.name == theme.name ? .on : .off
|
||||
item.target = self
|
||||
menu.addItem(item)
|
||||
}
|
||||
popUpButton?.selectItem(withTitle: ArticleThemesManager.shared.currentThemeName)
|
||||
if popUpButton?.indexOfSelectedItem == -1 {
|
||||
popUpButton?.selectItem(withTitle: ArticleTheme.defaultTheme.name)
|
||||
}
|
||||
return menu
|
||||
}
|
||||
|
||||
@objc
|
||||
func updateThemeSelection(_ menuItem: NSMenuItem) {
|
||||
currentThemeName = menuItem.title
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
func deleteTheme(themeName: String) {
|
||||
if let filename = pathForThemeName(themeName, folder: folderPath) {
|
||||
|
||||
@@ -162,7 +162,7 @@ extension CGImage {
|
||||
}
|
||||
|
||||
|
||||
enum IconSize: Int, CaseIterable {
|
||||
enum IconSize: Int, CaseIterable, CustomStringConvertible {
|
||||
case small = 1
|
||||
case medium = 2
|
||||
case large = 3
|
||||
@@ -181,5 +181,16 @@ enum IconSize: Int, CaseIterable {
|
||||
return CGSize(width: IconSize.largeDimension, height: IconSize.largeDimension)
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .small:
|
||||
return Bundle.main.localizedString(forKey: "SMALL_ICON_SIZE", value: nil, table: "Settings")
|
||||
case .medium:
|
||||
return Bundle.main.localizedString(forKey: "MEDIUM_ICON_SIZE", value: nil, table: "Settings")
|
||||
case .large:
|
||||
return Bundle.main.localizedString(forKey: "LARGE_ICON_SIZE", value: nil, table: "Settings")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
35
Shared/Importers/NNWThemeDocument.swift
Normal file
35
Shared/Importers/NNWThemeDocument.swift
Normal file
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// NNWThemeDocument.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Stuart Breckenridge on 20/12/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
public struct NNWThemeDocument: FileDocument {
|
||||
|
||||
public static var readableContentTypes: [UTType] {
|
||||
UTType.types(tag: "nnwtheme", tagClass: .filenameExtension, conformingTo: nil)
|
||||
}
|
||||
|
||||
public static var writableContentTypes: [UTType] {
|
||||
UTType.types(tag: "nnwtheme", tagClass: .filenameExtension, conformingTo: nil)
|
||||
}
|
||||
|
||||
public init(configuration: ReadConfiguration) throws {
|
||||
guard let _ = configuration.file.regularFileContents else {
|
||||
throw CocoaError(.fileReadCorruptFile)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
public func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
|
||||
let wrapper = try FileWrapper(url: URL(string: "")!)
|
||||
return wrapper
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
42
Shared/Importers/OPMLDocument.swift
Normal file
42
Shared/Importers/OPMLDocument.swift
Normal file
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// OPMLDocument.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Stuart Breckenridge on 29/11/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Account
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
public struct OPMLDocument: FileDocument {
|
||||
|
||||
public var account: Account!
|
||||
|
||||
public static var readableContentTypes: [UTType] {
|
||||
UTType.types(tag: "opml", tagClass: .filenameExtension, conformingTo: nil)
|
||||
}
|
||||
|
||||
public static var writableContentTypes: [UTType] {
|
||||
UTType.types(tag: "opml", tagClass: .filenameExtension, conformingTo: nil)
|
||||
}
|
||||
|
||||
public init(configuration: ReadConfiguration) throws {
|
||||
|
||||
}
|
||||
|
||||
public init(_ account: Account) throws {
|
||||
self.account = account
|
||||
}
|
||||
|
||||
@MainActor public func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
|
||||
let accountName = account.nameForDisplay.replacingOccurrences(of: " ", with: "").trimmingCharacters(in: .whitespaces)
|
||||
let filename = "Subscriptions-\(accountName).opml"
|
||||
let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent(filename)
|
||||
let opmlString = OPMLExporter.OPMLString(with: account, title: filename)
|
||||
try opmlString.write(to: tempFile, atomically: true, encoding: String.Encoding.utf8)
|
||||
let wrapper = try FileWrapper(url: tempFile)
|
||||
return wrapper
|
||||
}
|
||||
}
|
||||
46
Shared/Localizations/LocalizedNetNewsWireError.swift
Normal file
46
Shared/Localizations/LocalizedNetNewsWireError.swift
Normal file
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// LocalizedNetNewsWireError.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Stuart Breckenridge on 16/12/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum LocalizedNetNewsWireError: LocalizedError {
|
||||
|
||||
/// Displayed when the user tries to create a duplicate
|
||||
/// account with the same username.
|
||||
case duplicateAccount
|
||||
|
||||
/// Displayed when the user attempts to add a
|
||||
/// iCloud account but iCloud and/or iCloud Drive
|
||||
/// are not enabled.
|
||||
case iCloudDriveMissing
|
||||
|
||||
case userNameAndPasswordRequired
|
||||
|
||||
case invalidUsernameOrPassword
|
||||
|
||||
case keychainError
|
||||
|
||||
case duplicateDefaultTheme
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .duplicateAccount:
|
||||
return NSLocalizedString("There is already an account of that type with that username created.", comment: "Error message: duplicate account with same username.")
|
||||
case .iCloudDriveMissing:
|
||||
return NSLocalizedString("Unable to add iCloud Account. Please make sure you have iCloud and iCloud Drive enabled in System Settings.", comment: "Error message: The user cannot enable the iCloud account becasue iCloud or iCloud Drive isn't enabled in Settings.")
|
||||
case .userNameAndPasswordRequired:
|
||||
return NSLocalizedString("Username and password required", comment: "Error message: The user must provide a username and password.")
|
||||
case .invalidUsernameOrPassword:
|
||||
return NSLocalizedString("Invalid username or password", comment: "Error message: The user provided an invalid username or password.")
|
||||
case .keychainError:
|
||||
return NSLocalizedString("Keychain error while storing credentials.", comment: "Error message: Unable to save due a Keychain error.")
|
||||
case .duplicateDefaultTheme:
|
||||
return NSLocalizedString("You cannot import a theme that shares the same name as a provided theme.", comment: "Error message: cannot import theme as this is a duplicate of a provided theme.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,9 @@
|
||||
<key>ThemeIdentifier</key>
|
||||
<string>com.mynameisstuart.themes.newsfax</string>
|
||||
<key>CreatorHomePage</key>
|
||||
<string>https://mynameisstuart.com/</string>
|
||||
<string>https://stuartbreckenridge.net/</string>
|
||||
<key>CreatorName</key>
|
||||
<string>Stuart Breckenridge</string>
|
||||
<string>Ranchero Software</string>
|
||||
<key>Version</key>
|
||||
<integer>3</integer>
|
||||
</dict>
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
<key>ThemeIdentifier</key>
|
||||
<string>com.mynameisstuart.themes.promenade</string>
|
||||
<key>CreatorHomePage</key>
|
||||
<string>https://mynameisstuart.com/</string>
|
||||
<string>https://stuartbreckenridge.net/</string>
|
||||
<key>CreatorName</key>
|
||||
<string>Stuart Breckenridge</string>
|
||||
<string>Ranchero Software</string>
|
||||
<key>Version</key>
|
||||
<integer>14</integer>
|
||||
</dict>
|
||||
|
||||
@@ -73,7 +73,7 @@ class AccountRefreshTimer {
|
||||
lastTimedRefresh = Date()
|
||||
update()
|
||||
|
||||
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log)
|
||||
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log, completion: nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,810 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Modal Navigation Controller-->
|
||||
<scene sceneID="98f-PW-S1C">
|
||||
<objects>
|
||||
<navigationController storyboardIdentifier="LocalAccountNavigationViewController" id="TMY-HB-vAu" customClass="ModalNavigationController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="p8g-7e-3f4">
|
||||
<rect key="frame" x="0.0" y="48" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<connections>
|
||||
<segue destination="vi3-jb-8XS" kind="relationship" relationship="rootViewController" id="dIe-7d-ZQX"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="6sV-68-OXu" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1880" y="-528"/>
|
||||
</scene>
|
||||
<!--Modal Navigation Controller-->
|
||||
<scene sceneID="6i4-ho-e4F">
|
||||
<objects>
|
||||
<navigationController storyboardIdentifier="FeedbinAccountNavigationViewController" id="sFg-MZ-PqJ" customClass="ModalNavigationController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="wq6-np-tNn">
|
||||
<rect key="frame" x="0.0" y="48" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<connections>
|
||||
<segue destination="ECy-jg-Kyc" kind="relationship" relationship="rootViewController" id="usT-8C-GGf"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Lfz-4s-0Vn" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="3177" y="-528"/>
|
||||
</scene>
|
||||
<!--On My Device-->
|
||||
<scene sceneID="J93-FN-Yey">
|
||||
<objects>
|
||||
<tableViewController id="vi3-jb-8XS" customClass="LocalAccountViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="YLa-nM-G7t">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<view key="tableFooterView" contentMode="scaleToFill" id="Vxr-5V-V6R">
|
||||
<rect key="frame" x="0.0" y="159" width="414" height="150"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Local accounts do not sync your feeds across devices." textAlignment="center" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="5ce-ZL-glQ">
|
||||
<rect key="frame" x="20" y="8" width="373" height="13.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="5ce-ZL-glQ" secondAttribute="trailing" constant="21" id="YLV-d0-1us"/>
|
||||
<constraint firstItem="5ce-ZL-glQ" firstAttribute="leading" secondItem="Vxr-5V-V6R" secondAttribute="leading" constant="20" symbolic="YES" id="dmE-Zi-5FR"/>
|
||||
<constraint firstItem="5ce-ZL-glQ" firstAttribute="top" secondItem="Vxr-5V-V6R" secondAttribute="top" constant="8" id="z4G-hO-VUE"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<sections>
|
||||
<tableViewSection id="TfM-Jc-Fr0">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="uFU-j6-qP1">
|
||||
<rect key="frame" x="20" y="18" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="uFU-j6-qP1" id="fr4-mL-3Yf">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Name" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="Yl1-R6-xZi">
|
||||
<rect key="frame" x="20" y="12.5" width="334" height="18.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="words"/>
|
||||
</textField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="Yl1-R6-xZi" firstAttribute="leading" secondItem="fr4-mL-3Yf" secondAttribute="leading" constant="20" id="HJ4-VN-e9Y"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Yl1-R6-xZi" secondAttribute="trailing" constant="20" id="vbZ-dD-yZM"/>
|
||||
<constraint firstItem="Yl1-R6-xZi" firstAttribute="centerY" secondItem="fr4-mL-3Yf" secondAttribute="centerY" id="zsZ-z6-IFh"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection id="Sgf-NV-3Di">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="pTk-WJ-j5h" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="97.5" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="pTk-WJ-j5h" id="ahe-yz-PGg">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="mQv-3O-Y2d" customClass="VibrantButton" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="-0.5" width="374" height="44.5"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="44" id="EEL-8n-nHO"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<state key="normal" title="Add Account">
|
||||
<color key="titleColor" name="secondaryAccentColor"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="add:" destination="vi3-jb-8XS" eventType="touchUpInside" id="lCb-LW-xZ0"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="mQv-3O-Y2d" firstAttribute="centerY" secondItem="ahe-yz-PGg" secondAttribute="centerY" id="6bl-bA-qYE"/>
|
||||
<constraint firstItem="mQv-3O-Y2d" firstAttribute="leading" secondItem="ahe-yz-PGg" secondAttribute="leading" id="7gZ-8n-bWs"/>
|
||||
<constraint firstAttribute="trailing" secondItem="mQv-3O-Y2d" secondAttribute="trailing" id="FQu-yU-a4k"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
</sections>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="vi3-jb-8XS" id="U1Z-Kw-46j"/>
|
||||
<outlet property="delegate" destination="vi3-jb-8XS" id="4El-ci-jdg"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="On My Device" id="AOA-LS-PIB">
|
||||
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="b2H-re-cgN">
|
||||
<connections>
|
||||
<action selector="cancel:" destination="vi3-jb-8XS" id="gRE-sR-r4Z"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="footerLabel" destination="5ce-ZL-glQ" id="V50-Yc-hD6"/>
|
||||
<outlet property="nameTextField" destination="Yl1-R6-xZi" id="jcl-vI-Rde"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="XJD-sO-MSq" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1879.7101449275365" y="144.64285714285714"/>
|
||||
</scene>
|
||||
<!--Feedbin-->
|
||||
<scene sceneID="IDj-HA-olN">
|
||||
<objects>
|
||||
<tableViewController id="ECy-jg-Kyc" customClass="FeedbinAccountViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="Y0x-RC-7ln">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<view key="tableFooterView" contentMode="scaleToFill" id="3KO-DU-JXG">
|
||||
<rect key="frame" x="0.0" y="202.5" width="414" height="150"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="center" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sgL-0C-JZa">
|
||||
<rect key="frame" x="20" y="8" width="373" height="65.5"/>
|
||||
<string key="text">Sign in to your Feedbin account to sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.
|
||||
|
||||
Don’t have a Feedbin account?</string>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Xhf-bK-vzm">
|
||||
<rect key="frame" x="172" y="72" width="70" height="26"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
|
||||
<state key="normal" title="Sign Up Here"/>
|
||||
<connections>
|
||||
<action selector="signUpWithProvider:" destination="ECy-jg-Kyc" eventType="touchUpInside" id="fIY-hq-q3H"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="sgL-0C-JZa" firstAttribute="top" secondItem="3KO-DU-JXG" secondAttribute="top" constant="8" id="BgR-gH-qHf"/>
|
||||
<constraint firstItem="sgL-0C-JZa" firstAttribute="leading" secondItem="3KO-DU-JXG" secondAttribute="leading" constant="20" symbolic="YES" id="PLI-kz-MMq"/>
|
||||
<constraint firstItem="Xhf-bK-vzm" firstAttribute="top" secondItem="sgL-0C-JZa" secondAttribute="bottom" constant="-1.5" id="R9l-5y-aMr"/>
|
||||
<constraint firstAttribute="trailing" secondItem="sgL-0C-JZa" secondAttribute="trailing" constant="21" id="ddS-HE-f1J"/>
|
||||
<constraint firstItem="Xhf-bK-vzm" firstAttribute="centerX" secondItem="3KO-DU-JXG" secondAttribute="centerX" id="xs6-P4-4Vj"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<sections>
|
||||
<tableViewSection id="xBN-Pb-KAy">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="lsa-Fl-Pc7">
|
||||
<rect key="frame" x="20" y="18" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="lsa-Fl-Pc7" id="Lpd-D1-1PQ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Email" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="vJa-NN-yjR">
|
||||
<rect key="frame" x="20" y="12.5" width="334" height="18.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<textInputTraits key="textInputTraits" spellCheckingType="no" keyboardType="emailAddress" textContentType="username"/>
|
||||
</textField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="vJa-NN-yjR" secondAttribute="trailing" constant="20" id="7xY-Mz-Szf"/>
|
||||
<constraint firstItem="vJa-NN-yjR" firstAttribute="centerY" secondItem="Lpd-D1-1PQ" secondAttribute="centerY" id="E8M-nD-KIN"/>
|
||||
<constraint firstItem="vJa-NN-yjR" firstAttribute="leading" secondItem="Lpd-D1-1PQ" secondAttribute="leading" constant="20" id="Lgm-1L-4xL"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="Hwv-Q0-zT0">
|
||||
<rect key="frame" x="20" y="61.5" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Hwv-Q0-zT0" id="jIT-5L-d8d">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Password" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="YC2-RH-QoV">
|
||||
<rect key="frame" x="20" y="13.5" width="290" height="17"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<textInputTraits key="textInputTraits" secureTextEntry="YES" textContentType="password"/>
|
||||
</textField>
|
||||
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="TfW-wf-V06">
|
||||
<rect key="frame" x="318" y="7.5" width="36" height="29"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<state key="normal" title="Show"/>
|
||||
<connections>
|
||||
<action selector="showHidePassword:" destination="ECy-jg-Kyc" eventType="touchUpInside" id="QcS-lr-SPG"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="TfW-wf-V06" firstAttribute="leading" secondItem="YC2-RH-QoV" secondAttribute="trailing" constant="8" symbolic="YES" id="MHu-Ut-Kox"/>
|
||||
<constraint firstItem="TfW-wf-V06" firstAttribute="centerY" secondItem="jIT-5L-d8d" secondAttribute="centerY" id="O3Y-Jd-n9t"/>
|
||||
<constraint firstItem="YC2-RH-QoV" firstAttribute="leading" secondItem="jIT-5L-d8d" secondAttribute="leading" constant="20" id="W79-WW-Buk"/>
|
||||
<constraint firstItem="YC2-RH-QoV" firstAttribute="centerY" secondItem="jIT-5L-d8d" secondAttribute="centerY" id="iDt-ym-Qjf"/>
|
||||
<constraint firstAttribute="trailing" secondItem="TfW-wf-V06" secondAttribute="trailing" constant="20" symbolic="YES" id="rMZ-af-tPg"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection id="Kkf-rn-qdv">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="hWd-EN-p7e">
|
||||
<rect key="frame" x="20" y="141" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="hWd-EN-p7e" id="S8S-1a-vVf">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="gv7-yG-aE3" customClass="VibrantButton" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="-0.5" width="374" height="44.5"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="44" id="pt0-rn-0JI"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<state key="normal" title="Action">
|
||||
<color key="titleColor" name="secondaryAccentColor"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="action:" destination="ECy-jg-Kyc" eventType="touchUpInside" id="79h-R2-s0C"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="gv7-yG-aE3" firstAttribute="leading" secondItem="S8S-1a-vVf" secondAttribute="leading" id="cbE-1E-Mem"/>
|
||||
<constraint firstAttribute="trailing" secondItem="gv7-yG-aE3" secondAttribute="trailing" id="tQC-jk-evr"/>
|
||||
<constraint firstItem="gv7-yG-aE3" firstAttribute="centerY" secondItem="S8S-1a-vVf" secondAttribute="centerY" id="xSU-R7-XQ8"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
</sections>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="ECy-jg-Kyc" id="hUr-Xx-9Ho"/>
|
||||
<outlet property="delegate" destination="ECy-jg-Kyc" id="DKA-Lp-mNb"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Feedbin" id="tYg-9f-kyd">
|
||||
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="pfF-Of-5NT">
|
||||
<connections>
|
||||
<action selector="cancel:" destination="ECy-jg-Kyc" id="ZKI-gV-ylg"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem key="rightBarButtonItem" id="Xwp-LO-qff">
|
||||
<view key="customView" contentMode="scaleToFill" id="cn4-b1-uZa">
|
||||
<rect key="frame" x="374" y="12" width="20" height="20"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" style="medium" translatesAutoresizingMaskIntoConstraints="NO" id="YvV-hB-lzT">
|
||||
<rect key="frame" x="36" y="6" width="20" height="20"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
</activityIndicatorView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</view>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="actionButton" destination="gv7-yG-aE3" id="ENc-5A-hQc"/>
|
||||
<outlet property="activityIndicator" destination="YvV-hB-lzT" id="n1F-tV-5ZV"/>
|
||||
<outlet property="cancelBarButtonItem" destination="pfF-Of-5NT" id="Zr3-qD-1Yi"/>
|
||||
<outlet property="emailTextField" destination="vJa-NN-yjR" id="nCF-9W-YsF"/>
|
||||
<outlet property="footerLabel" destination="sgL-0C-JZa" id="b6I-Mk-2K3"/>
|
||||
<outlet property="passwordTextField" destination="YC2-RH-QoV" id="qaX-0i-7jq"/>
|
||||
<outlet property="showHideButton" destination="TfW-wf-V06" id="PbL-67-Nrg"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="L24-0i-kyr" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="3177" y="145"/>
|
||||
</scene>
|
||||
<!--Modal Navigation Controller-->
|
||||
<scene sceneID="j4N-ax-exh">
|
||||
<objects>
|
||||
<navigationController storyboardIdentifier="NewsBlurAccountNavigationViewController" id="eE3-pu-HdL" customClass="ModalNavigationController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="Fsp-NG-hoR">
|
||||
<rect key="frame" x="0.0" y="48" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<connections>
|
||||
<segue destination="Cge-ND-NpD" kind="relationship" relationship="rootViewController" id="1D5-CN-liN"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="8t3-0U-5vL" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="4562" y="-528"/>
|
||||
</scene>
|
||||
<!--NewsBlur-->
|
||||
<scene sceneID="tfA-kz-P6O">
|
||||
<objects>
|
||||
<tableViewController id="Cge-ND-NpD" customClass="NewsBlurAccountViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="fLL-7i-HdK">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<view key="tableFooterView" contentMode="scaleToFill" id="mgO-Iq-dEg">
|
||||
<rect key="frame" x="0.0" y="202.5" width="414" height="150"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="center" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fal-e8-3BB">
|
||||
<rect key="frame" x="20" y="8" width="373" height="65.5"/>
|
||||
<string key="text">Sign in to your NewsBlur account to sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.
|
||||
|
||||
Don’t have a NewsBlur account?</string>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="YhB-G0-eeJ">
|
||||
<rect key="frame" x="172" y="72" width="70" height="26"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
|
||||
<state key="normal" title="Sign Up Here"/>
|
||||
<connections>
|
||||
<action selector="signUpWithProvider:" destination="Cge-ND-NpD" eventType="touchUpInside" id="Vfz-DD-Kwm"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="YhB-G0-eeJ" firstAttribute="centerX" secondItem="mgO-Iq-dEg" secondAttribute="centerX" id="7r5-l6-NNv"/>
|
||||
<constraint firstItem="fal-e8-3BB" firstAttribute="leading" secondItem="mgO-Iq-dEg" secondAttribute="leading" constant="20" symbolic="YES" id="O4q-GI-2AO"/>
|
||||
<constraint firstItem="YhB-G0-eeJ" firstAttribute="top" secondItem="fal-e8-3BB" secondAttribute="bottom" constant="-1.5" id="UHc-sh-Xq4"/>
|
||||
<constraint firstAttribute="trailing" secondItem="fal-e8-3BB" secondAttribute="trailing" constant="21" id="V0d-ny-GRE"/>
|
||||
<constraint firstItem="fal-e8-3BB" firstAttribute="top" secondItem="mgO-Iq-dEg" secondAttribute="top" constant="8" id="h5B-kg-rZj"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<sections>
|
||||
<tableViewSection id="I5T-12-2jC">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="gAY-Bo-c0L">
|
||||
<rect key="frame" x="20" y="18" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="gAY-Bo-c0L" id="mqD-6S-DIl">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Username or Email" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="S4v-fs-DIO">
|
||||
<rect key="frame" x="20" y="12.5" width="334" height="18.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<textInputTraits key="textInputTraits" spellCheckingType="no" keyboardType="emailAddress" textContentType="username"/>
|
||||
</textField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="S4v-fs-DIO" secondAttribute="trailing" constant="20" id="Upe-dm-4DP"/>
|
||||
<constraint firstItem="S4v-fs-DIO" firstAttribute="leading" secondItem="mqD-6S-DIl" secondAttribute="leading" constant="20" id="pQc-Fh-6T3"/>
|
||||
<constraint firstItem="S4v-fs-DIO" firstAttribute="centerY" secondItem="mqD-6S-DIl" secondAttribute="centerY" id="s9a-ew-C5W"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="iCK-kn-Au6">
|
||||
<rect key="frame" x="20" y="61.5" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="iCK-kn-Au6" id="9Ej-wB-9Tr">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Password" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="fct-XR-fEa">
|
||||
<rect key="frame" x="20" y="13.5" width="290" height="17"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<textInputTraits key="textInputTraits" secureTextEntry="YES" textContentType="password"/>
|
||||
</textField>
|
||||
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="GY9-nr-jFb">
|
||||
<rect key="frame" x="318" y="7.5" width="36" height="29"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<state key="normal" title="Show"/>
|
||||
<connections>
|
||||
<action selector="showHidePassword:" destination="Cge-ND-NpD" eventType="touchUpInside" id="8JH-LX-URH"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="GY9-nr-jFb" firstAttribute="centerY" secondItem="9Ej-wB-9Tr" secondAttribute="centerY" id="3jf-KC-nd8"/>
|
||||
<constraint firstItem="GY9-nr-jFb" firstAttribute="leading" secondItem="fct-XR-fEa" secondAttribute="trailing" constant="8" symbolic="YES" id="Ibr-pt-eGr"/>
|
||||
<constraint firstAttribute="trailing" secondItem="GY9-nr-jFb" secondAttribute="trailing" constant="20" symbolic="YES" id="mcZ-cl-knP"/>
|
||||
<constraint firstItem="fct-XR-fEa" firstAttribute="leading" secondItem="9Ej-wB-9Tr" secondAttribute="leading" constant="20" id="u5f-tJ-8ce"/>
|
||||
<constraint firstItem="fct-XR-fEa" firstAttribute="centerY" secondItem="9Ej-wB-9Tr" secondAttribute="centerY" id="z5e-jg-0nm"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection id="L37-iZ-GVj">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="fyQ-K8-byV">
|
||||
<rect key="frame" x="20" y="141" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="fyQ-K8-byV" id="CtR-ZJ-FG5">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="E1I-C4-JdL" customClass="VibrantButton" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="-0.5" width="374" height="44.5"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="44" id="yoo-36-msf"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<state key="normal" title="Action">
|
||||
<color key="titleColor" name="secondaryAccentColor"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="action:" destination="Cge-ND-NpD" eventType="touchUpInside" id="YQw-1k-e8G"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="E1I-C4-JdL" firstAttribute="centerY" secondItem="CtR-ZJ-FG5" secondAttribute="centerY" id="2vc-Ys-4Cj"/>
|
||||
<constraint firstAttribute="trailing" secondItem="E1I-C4-JdL" secondAttribute="trailing" id="SLX-wc-QR7"/>
|
||||
<constraint firstItem="E1I-C4-JdL" firstAttribute="leading" secondItem="CtR-ZJ-FG5" secondAttribute="leading" id="Veu-Wo-GSZ"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
</sections>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="Cge-ND-NpD" id="u8B-p4-Vlv"/>
|
||||
<outlet property="delegate" destination="Cge-ND-NpD" id="RIw-V2-EJC"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="NewsBlur" id="jCQ-pH-6AD">
|
||||
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="bl6-Y1-wQ8">
|
||||
<connections>
|
||||
<action selector="cancel:" destination="Cge-ND-NpD" id="9zR-LJ-IWk"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem key="rightBarButtonItem" id="4yi-H0-B9J">
|
||||
<view key="customView" contentMode="scaleToFill" id="8DU-L0-P6c">
|
||||
<rect key="frame" x="374" y="12" width="20" height="20"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" style="medium" translatesAutoresizingMaskIntoConstraints="NO" id="HfW-jV-MjK">
|
||||
<rect key="frame" x="36" y="6" width="20" height="20"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
</activityIndicatorView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</view>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="actionButton" destination="E1I-C4-JdL" id="q2T-4o-c8i"/>
|
||||
<outlet property="activityIndicator" destination="HfW-jV-MjK" id="AIV-uG-9uC"/>
|
||||
<outlet property="cancelBarButtonItem" destination="bl6-Y1-wQ8" id="ohR-gW-5J2"/>
|
||||
<outlet property="footerLabel" destination="fal-e8-3BB" id="7Fq-Oz-aEx"/>
|
||||
<outlet property="passwordTextField" destination="fct-XR-fEa" id="fGL-4k-gZ6"/>
|
||||
<outlet property="showHideButton" destination="GY9-nr-jFb" id="1p9-9F-GMY"/>
|
||||
<outlet property="usernameTextField" destination="S4v-fs-DIO" id="B7I-yz-M0T"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="8Ku-6P-yPg" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="4561" y="145"/>
|
||||
</scene>
|
||||
<!--Reader-->
|
||||
<scene sceneID="3fU-9I-RDp">
|
||||
<objects>
|
||||
<tableViewController id="MzG-hS-TpF" customClass="ReaderAPIAccountViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="bQC-XA-xWP">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<view key="tableFooterView" contentMode="scaleToFill" id="6sa-hD-iAT">
|
||||
<rect key="frame" x="0.0" y="246" width="414" height="150"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="center" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7Jj-p8-lYw">
|
||||
<rect key="frame" x="20" y="8" width="373" height="39.5"/>
|
||||
<string key="text">Use your Reader account to sync your feeds across your devices.
|
||||
|
||||
Don’t have a Reader account?</string>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="3Fq-U4-PS5">
|
||||
<rect key="frame" x="172" y="46" width="70" height="26"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
|
||||
<state key="normal" title="Sign Up Here"/>
|
||||
<connections>
|
||||
<action selector="signUpWithProvider:" destination="MzG-hS-TpF" eventType="touchUpInside" id="pMC-f8-E4G"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="3Fq-U4-PS5" firstAttribute="top" secondItem="7Jj-p8-lYw" secondAttribute="bottom" constant="-1.5" id="6ma-pw-DSZ"/>
|
||||
<constraint firstItem="7Jj-p8-lYw" firstAttribute="leading" secondItem="6sa-hD-iAT" secondAttribute="leading" constant="20" symbolic="YES" id="CnY-UA-F3a"/>
|
||||
<constraint firstItem="3Fq-U4-PS5" firstAttribute="centerX" secondItem="6sa-hD-iAT" secondAttribute="centerX" id="Rgg-oG-KOZ"/>
|
||||
<constraint firstAttribute="trailing" secondItem="7Jj-p8-lYw" secondAttribute="trailing" constant="21" id="mgT-t6-6c8"/>
|
||||
<constraint firstItem="7Jj-p8-lYw" firstAttribute="top" secondItem="6sa-hD-iAT" secondAttribute="top" constant="8" id="syN-5x-dbM"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<sections>
|
||||
<tableViewSection id="Rju-xt-yUY">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="kW8-SV-Byq">
|
||||
<rect key="frame" x="20" y="18" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="kW8-SV-Byq" id="4mV-Au-W6t">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Username or Email" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="CZg-x8-936">
|
||||
<rect key="frame" x="14" y="12.5" width="347" height="18.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<textInputTraits key="textInputTraits" spellCheckingType="no" keyboardType="emailAddress" textContentType="username"/>
|
||||
</textField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="CZg-x8-936" secondAttribute="trailing" constant="13" id="7BW-D6-ZAW"/>
|
||||
<constraint firstItem="CZg-x8-936" firstAttribute="centerY" secondItem="4mV-Au-W6t" secondAttribute="centerY" id="Fii-Qu-oXf"/>
|
||||
<constraint firstItem="CZg-x8-936" firstAttribute="leading" secondItem="4mV-Au-W6t" secondAttribute="leading" constant="14" id="MKL-Nm-1Po"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="pNe-n6-tVf">
|
||||
<rect key="frame" x="20" y="61.5" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="pNe-n6-tVf" id="yQJ-L0-qVZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Password" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="KgN-kQ-Cyc">
|
||||
<rect key="frame" x="14" y="13.5" width="300" height="17"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<textInputTraits key="textInputTraits" secureTextEntry="YES" textContentType="password"/>
|
||||
</textField>
|
||||
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="cFF-qt-WLs">
|
||||
<rect key="frame" x="322" y="7.5" width="36" height="29"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<state key="normal" title="Show"/>
|
||||
<connections>
|
||||
<action selector="showHidePassword:" destination="MzG-hS-TpF" eventType="touchUpInside" id="IsF-iJ-oxT"/>
|
||||
<action selector="showHidePassword:" destination="Cge-ND-NpD" eventType="touchUpInside" id="b9p-LX-Wk7"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="cFF-qt-WLs" firstAttribute="leading" secondItem="KgN-kQ-Cyc" secondAttribute="trailing" constant="8" symbolic="YES" id="Cwh-XX-m2G"/>
|
||||
<constraint firstItem="cFF-qt-WLs" firstAttribute="centerY" secondItem="yQJ-L0-qVZ" secondAttribute="centerY" id="GDc-9f-afL"/>
|
||||
<constraint firstAttribute="trailing" secondItem="cFF-qt-WLs" secondAttribute="trailing" constant="16" id="K93-X3-UuK"/>
|
||||
<constraint firstItem="KgN-kQ-Cyc" firstAttribute="centerY" secondItem="yQJ-L0-qVZ" secondAttribute="centerY" id="UpQ-pU-DYv"/>
|
||||
<constraint firstItem="KgN-kQ-Cyc" firstAttribute="leading" secondItem="yQJ-L0-qVZ" secondAttribute="leading" constant="14" id="fam-16-kf6"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="mCx-af-pd3">
|
||||
<rect key="frame" x="20" y="105" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="mCx-af-pd3" id="o1U-Qv-4gz">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="API URL" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="iPv-M2-U8Q">
|
||||
<rect key="frame" x="14" y="11" width="340" height="18.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" keyboardType="URL" smartDashesType="no" smartInsertDeleteType="no" smartQuotesType="no" textContentType="url"/>
|
||||
</textField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="iPv-M2-U8Q" firstAttribute="leading" secondItem="o1U-Qv-4gz" secondAttribute="leading" constant="14" id="4UP-GO-kmh"/>
|
||||
<constraint firstItem="iPv-M2-U8Q" firstAttribute="top" secondItem="o1U-Qv-4gz" secondAttribute="topMargin" id="Gap-aN-LP7"/>
|
||||
<constraint firstItem="iPv-M2-U8Q" firstAttribute="trailing" secondItem="o1U-Qv-4gz" secondAttribute="trailingMargin" id="npR-r8-mpF"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection id="UWZ-Vu-0Pp">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" rowHeight="43.5" id="d3E-Ds-Thm">
|
||||
<rect key="frame" x="20" y="184.5" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="d3E-Ds-Thm" id="Frb-uH-Sff">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="7L9-X7-1Oc">
|
||||
<rect key="frame" x="0.0" y="-0.5" width="374" height="44.5"/>
|
||||
<state key="normal" title="Action"/>
|
||||
<connections>
|
||||
<action selector="action:" destination="MzG-hS-TpF" eventType="touchUpInside" id="d10-4f-ZUn"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="7L9-X7-1Oc" firstAttribute="centerY" secondItem="Frb-uH-Sff" secondAttribute="centerY" id="NVm-XD-zND"/>
|
||||
<constraint firstItem="7L9-X7-1Oc" firstAttribute="centerX" secondItem="Frb-uH-Sff" secondAttribute="centerX" id="YB9-O8-Z15"/>
|
||||
<constraint firstItem="7L9-X7-1Oc" firstAttribute="height" secondItem="Frb-uH-Sff" secondAttribute="height" multiplier="0.689655" constant="13.999999999999996" id="iNV-NE-jhW"/>
|
||||
<constraint firstItem="7L9-X7-1Oc" firstAttribute="width" secondItem="Frb-uH-Sff" secondAttribute="width" multiplier="0.893048" constant="40" id="lfQ-KQ-9nR"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
</sections>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="MzG-hS-TpF" id="QvG-cd-Q3P"/>
|
||||
<outlet property="delegate" destination="MzG-hS-TpF" id="o4Z-RV-8uW"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Reader" id="z3N-XM-EXU">
|
||||
<barButtonItem key="leftBarButtonItem" title="Cancel" id="n8H-ai-4Df">
|
||||
<connections>
|
||||
<action selector="cancel:" destination="MzG-hS-TpF" id="a49-Fh-i1S"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem key="rightBarButtonItem" id="Ih6-jI-jFg">
|
||||
<view key="customView" contentMode="scaleToFill" id="gSl-PT-7DH">
|
||||
<rect key="frame" x="374" y="12" width="20" height="20"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" style="medium" translatesAutoresizingMaskIntoConstraints="NO" id="pdn-6v-d9a">
|
||||
<rect key="frame" x="36" y="6" width="20" height="20"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
</activityIndicatorView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</view>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="actionButton" destination="7L9-X7-1Oc" id="VnP-sl-Cmd"/>
|
||||
<outlet property="activityIndicator" destination="pdn-6v-d9a" id="vgt-C6-fy6"/>
|
||||
<outlet property="apiURLTextField" destination="iPv-M2-U8Q" id="8kn-Xk-a8w"/>
|
||||
<outlet property="cancelBarButtonItem" destination="n8H-ai-4Df" id="u86-HH-HYC"/>
|
||||
<outlet property="footerLabel" destination="7Jj-p8-lYw" id="Tqv-qR-WBR"/>
|
||||
<outlet property="passwordTextField" destination="KgN-kQ-Cyc" id="A0K-JL-CEW"/>
|
||||
<outlet property="showHideButton" destination="cFF-qt-WLs" id="AxI-Gl-NdM"/>
|
||||
<outlet property="signUpButton" destination="3Fq-U4-PS5" id="Wuj-5g-vDH"/>
|
||||
<outlet property="usernameTextField" destination="CZg-x8-936" id="nUT-WL-fKD"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Fj8-E0-Aeh" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="5260.8695652173919" y="144.64285714285714"/>
|
||||
</scene>
|
||||
<!--Modal Navigation Controller-->
|
||||
<scene sceneID="gfi-2F-rht">
|
||||
<objects>
|
||||
<navigationController storyboardIdentifier="CloudKitAccountNavigationViewController" id="LhW-Dq-qqj" customClass="ModalNavigationController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="MVG-BZ-ALL">
|
||||
<rect key="frame" x="0.0" y="48" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<connections>
|
||||
<segue destination="qj9-Vr-VIU" kind="relationship" relationship="rootViewController" id="n8n-iF-qeC"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="z9f-5I-8GC" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2533" y="-528"/>
|
||||
</scene>
|
||||
<!--iCloud-->
|
||||
<scene sceneID="ULt-VE-viU">
|
||||
<objects>
|
||||
<tableViewController id="qj9-Vr-VIU" customClass="CloudKitAccountViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="j6U-sh-M9y">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<view key="tableFooterView" contentMode="scaleToFill" id="iYz-ri-yys">
|
||||
<rect key="frame" x="0.0" y="79.5" width="414" height="150"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Your iCloud account syncs your feeds across your Mac and iOS devices" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="aFS-Y0-2MH">
|
||||
<rect key="frame" x="20" y="8" width="373" height="36"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="vXG-7q-4qg">
|
||||
<rect key="frame" x="75" y="50.5" width="264" height="30"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
|
||||
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
|
||||
<state key="normal" title="iCloud Syncing Limitations & Solutions"/>
|
||||
<connections>
|
||||
<action selector="openLimitationsAndSolutions:" destination="qj9-Vr-VIU" eventType="touchUpInside" id="JZ5-hQ-PLl"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="vXG-7q-4qg" firstAttribute="centerX" secondItem="iYz-ri-yys" secondAttribute="centerX" id="8Pg-BU-zIj"/>
|
||||
<constraint firstItem="aFS-Y0-2MH" firstAttribute="leading" secondItem="iYz-ri-yys" secondAttribute="leading" constant="20" symbolic="YES" id="E97-lo-arw"/>
|
||||
<constraint firstItem="vXG-7q-4qg" firstAttribute="top" secondItem="aFS-Y0-2MH" secondAttribute="bottom" id="TEh-B3-9Ci"/>
|
||||
<constraint firstAttribute="trailing" secondItem="aFS-Y0-2MH" secondAttribute="trailing" constant="21" id="XUo-oQ-MbK"/>
|
||||
<constraint firstItem="aFS-Y0-2MH" firstAttribute="top" secondItem="iYz-ri-yys" secondAttribute="top" constant="8" id="xpj-LW-4l7"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<sections>
|
||||
<tableViewSection id="bGn-Io-KuQ">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="FSY-KL-m3i" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="18" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="FSY-KL-m3i" id="ds7-ib-VgJ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="T1S-zH-rIp" customClass="VibrantButton" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="-0.5" width="374" height="44.5"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="44" id="dOv-Gz-h7s"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<state key="normal" title="Use iCloud">
|
||||
<color key="titleColor" name="secondaryAccentColor"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="add:" destination="qj9-Vr-VIU" eventType="touchUpInside" id="kUm-lW-g62"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="T1S-zH-rIp" firstAttribute="leading" secondItem="ds7-ib-VgJ" secondAttribute="leading" id="7F5-Ym-ew3"/>
|
||||
<constraint firstAttribute="trailing" secondItem="T1S-zH-rIp" secondAttribute="trailing" id="ON3-nQ-kd8"/>
|
||||
<constraint firstItem="T1S-zH-rIp" firstAttribute="centerY" secondItem="ds7-ib-VgJ" secondAttribute="centerY" id="dAM-F2-peY"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
</sections>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="qj9-Vr-VIU" id="j7u-Yd-rbe"/>
|
||||
<outlet property="delegate" destination="qj9-Vr-VIU" id="NhE-Pt-JGp"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="iCloud" id="idp-kp-cGU">
|
||||
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="hKZ-OI-mTV">
|
||||
<connections>
|
||||
<action selector="cancel:" destination="qj9-Vr-VIU" id="n5q-9M-3ME"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="footerLabel" destination="aFS-Y0-2MH" id="gDw-R1-HSK"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="weY-OS-9NV" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2533" y="145"/>
|
||||
</scene>
|
||||
<!--Modal Navigation Controller-->
|
||||
<scene sceneID="JBz-7C-wEc">
|
||||
<objects>
|
||||
<navigationController storyboardIdentifier="ReaderAPIAccountNavigationViewController" id="Son-xT-GLx" customClass="ModalNavigationController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="sdL-X8-E6K">
|
||||
<rect key="frame" x="0.0" y="48" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<connections>
|
||||
<segue destination="MzG-hS-TpF" kind="relationship" relationship="rootViewController" id="gsQ-9o-AMJ"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="vls-xO-YVi" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="5261" y="-528"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<namedColor name="secondaryAccentColor">
|
||||
<color red="0.031372549019607843" green="0.41568627450980394" blue="0.93333333333333335" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
<systemColor name="secondaryLabelColor">
|
||||
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="systemGroupedBackgroundColor">
|
||||
<color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
74
iOS/Account/CloudKitAddAccountView.swift
Normal file
74
iOS/Account/CloudKitAddAccountView.swift
Normal file
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// CloudKitAddAccountView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 16/12/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Account
|
||||
|
||||
struct CloudKitAddAccountView: View {
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var accountError: (Error?, Bool) = (nil, false)
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Form {
|
||||
AccountSectionHeader(accountType: .cloudKit)
|
||||
Section { createCloudKitAccount }
|
||||
Section(footer: cloudKitExplainer) {}
|
||||
}
|
||||
.navigationTitle(Text(verbatim: "iCloud"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button(action: { dismiss() }, label: { Text("Cancel", comment: "Button title") })
|
||||
}
|
||||
}
|
||||
.alert(Text("Error", comment: "Alert title: Error"), isPresented: $accountError.1) {
|
||||
Button(action: {}, label: { Text("Dismiss", comment: "Button title") })
|
||||
} message: {
|
||||
Text(accountError.0?.localizedDescription ?? "Unknown Error")
|
||||
}
|
||||
.dismissOnExternalContextLaunch()
|
||||
.dismissOnAccountAdd()
|
||||
}
|
||||
}
|
||||
|
||||
var createCloudKitAccount: some View {
|
||||
Button {
|
||||
guard FileManager.default.ubiquityIdentityToken != nil else {
|
||||
accountError = (LocalizedNetNewsWireError.iCloudDriveMissing, true)
|
||||
return
|
||||
}
|
||||
let _ = AccountManager.shared.createAccount(type: .cloudKit)
|
||||
} label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("Use iCloud", comment: "Button title")
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var cloudKitExplainer: some View {
|
||||
VStack(spacing: 4) {
|
||||
if !AccountManager.shared.accounts.contains(where: { $0.type == .cloudKit }) {
|
||||
// The explainer is only shown when a CloudKit account doesn't exist.
|
||||
Text("NetNewsWire will use your iCloud account to sync your subscriptions across your Mac and iOS devices.", comment: "iCloud account explanatory text")
|
||||
}
|
||||
Text("[iCloud Syncing Limitations & Solutions](https://netnewswire.com/help/iCloud)", comment: "Link which opens webpage describing iCloud syncing limitations.")
|
||||
}.multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
struct iCloudAccountView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CloudKitAddAccountView()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
188
iOS/Account/FeedbinAddAccountView.swift
Normal file
188
iOS/Account/FeedbinAddAccountView.swift
Normal file
@@ -0,0 +1,188 @@
|
||||
//
|
||||
// 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", comment: "Button title") })
|
||||
.disabled(showProgressIndicator)
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
if showProgressIndicator { ProgressView() }
|
||||
}
|
||||
}
|
||||
.alert(Text("Error", comment: "Alert title: Error"), isPresented: $accountError.1) {
|
||||
Button(role: .cancel) {
|
||||
//
|
||||
} label: {
|
||||
Text("Dismiss", comment: "Button title")
|
||||
}
|
||||
} message: {
|
||||
Text(accountError.0?.localizedDescription ?? "Error")
|
||||
}
|
||||
.navigationTitle(Text(verbatim: account?.type.localizedAccountName() ?? "Feedbin"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.interactiveDismissDisabled(showProgressIndicator)
|
||||
.dismissOnExternalContextLaunch()
|
||||
.dismissOnAccountAdd()
|
||||
}
|
||||
}
|
||||
|
||||
var accountDetails: some View {
|
||||
Section {
|
||||
TextField("Email", text: $accountEmail, prompt: Text("Email Address", comment: "Textfield for the user to enter their account email address."))
|
||||
.autocorrectionDisabled()
|
||||
.autocapitalization(.none)
|
||||
.textContentType(.username)
|
||||
SecureField("Password", text: $accountPassword, prompt: Text("Password", comment: "Textfield for the user to enter their account password."))
|
||||
.textContentType(.password)
|
||||
}
|
||||
}
|
||||
|
||||
var accountButton: some View {
|
||||
Section {
|
||||
Button {
|
||||
Task {
|
||||
do {
|
||||
if account == nil {
|
||||
// Create a new account
|
||||
try await executeAccountCredentials()
|
||||
} else {
|
||||
// Updating account credentials
|
||||
try await executeAccountCredentials()
|
||||
dismiss()
|
||||
}
|
||||
} catch {
|
||||
accountError = (error, true)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack{
|
||||
Spacer()
|
||||
if account == nil {
|
||||
Text("Add Account", comment: "Button title")
|
||||
} else {
|
||||
Text("Update Credentials", comment: "Button title")
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.disabled(!validateCredentials())
|
||||
}
|
||||
}
|
||||
|
||||
var feedbinAccountExplainer: some View {
|
||||
if account == nil {
|
||||
return Text("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)", comment: "Explanatory text describing the Feedbin account.")
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
return Text("").multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
private func validateCredentials() -> Bool {
|
||||
if (accountEmail.trimmingWhitespace.count == 0) || (accountPassword.trimmingWhitespace.count == 0) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
//
|
||||
// LocalAccountViewController.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 5/19/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Account
|
||||
|
||||
class LocalAccountViewController: UITableViewController {
|
||||
|
||||
@IBOutlet weak var nameTextField: UITextField!
|
||||
@IBOutlet weak var footerLabel: UILabel!
|
||||
|
||||
weak var delegate: AddAccountDismissDelegate?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setupFooter()
|
||||
navigationItem.title = Account.defaultLocalAccountName
|
||||
nameTextField.delegate = self
|
||||
tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
|
||||
}
|
||||
|
||||
private func setupFooter() {
|
||||
footerLabel.text = NSLocalizedString("Local accounts do not sync your feeds across devices.", comment: "Local")
|
||||
}
|
||||
|
||||
@IBAction func cancel(_ sender: Any) {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@IBAction func add(_ sender: Any) {
|
||||
let account = AccountManager.shared.createAccount(type: .onMyMac)
|
||||
account.name = nameTextField.text
|
||||
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: .onMyMac)
|
||||
return headerView
|
||||
} else {
|
||||
return super.tableView(tableView, viewForHeaderInSection: section)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension LocalAccountViewController: UITextFieldDelegate {
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
textField.resignFirstResponder()
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
86
iOS/Account/LocalAddAccountView.swift
Normal file
86
iOS/Account/LocalAddAccountView.swift
Normal file
@@ -0,0 +1,86 @@
|
||||
//
|
||||
// LocalAddAccountView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 18/12/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Account
|
||||
|
||||
struct LocalAddAccountView: View {
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@State private var accountName: String = ""
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Form {
|
||||
AccountSectionHeader(accountType: .onMyMac)
|
||||
Section { accountNameSection }
|
||||
Section { addAccountButton }
|
||||
Section(footer: accountFooterView) {}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button(action: { dismiss() }, label: { Text("Cancel", comment: "Button title") })
|
||||
}
|
||||
}
|
||||
.navigationTitle(deviceAccountName())
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.dismissOnExternalContextLaunch()
|
||||
.dismissOnAccountAdd()
|
||||
}
|
||||
}
|
||||
|
||||
var accountNameSection: some View {
|
||||
TextField("Name",
|
||||
text: $accountName,
|
||||
prompt: Text("Name", comment: "Textfield placeholder for the name of the account."))
|
||||
.autocorrectionDisabled()
|
||||
.autocapitalization(.none)
|
||||
}
|
||||
|
||||
var addAccountButton: some View {
|
||||
Button {
|
||||
let account = AccountManager.shared.createAccount(type: .onMyMac)
|
||||
if accountName.trimmingWhitespace.count > 0 { account.name = accountName }
|
||||
} label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("Add Account", comment: "Button title")
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var accountFooterView: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("Local accounts do not sync your feeds across devices.", comment: "Explanatory text describing the local account.")
|
||||
.multilineTextAlignment(.center)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
private func accountImage() -> UIImage {
|
||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||
return AppAssets.accountLocalPadImage
|
||||
}
|
||||
return AppAssets.accountLocalPhoneImage
|
||||
}
|
||||
|
||||
private func deviceAccountName() -> Text {
|
||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||
return Text("On My iPad", comment: "Account name for iPad")
|
||||
}
|
||||
return Text("On My iPhone", comment: "Account name for iPhone")
|
||||
}
|
||||
}
|
||||
|
||||
struct LocalAddAccountView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
LocalAddAccountView()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
196
iOS/Account/NewsBlurAddAccountView.swift
Normal file
196
iOS/Account/NewsBlurAddAccountView.swift
Normal file
@@ -0,0 +1,196 @@
|
||||
//
|
||||
// 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, Logging {
|
||||
|
||||
@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", comment: "Button title") })
|
||||
.disabled(showProgressIndicator)
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
if showProgressIndicator { ProgressView() }
|
||||
}
|
||||
}
|
||||
.navigationTitle(Text(AccountType.newsBlur.localizedAccountName()))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.task {
|
||||
retreiveCredentials()
|
||||
}
|
||||
.alert(Text("Error", comment: "Alert title: Error"), isPresented: $accountError.1) {
|
||||
Button(role: .cancel) {
|
||||
//
|
||||
} label: {
|
||||
Text("Dismiss", comment: "Button title")
|
||||
}
|
||||
} message: {
|
||||
Text(accountError.0?.localizedDescription ?? "")
|
||||
}
|
||||
.interactiveDismissDisabled(showProgressIndicator)
|
||||
.dismissOnExternalContextLaunch()
|
||||
.dismissOnAccountAdd()
|
||||
}
|
||||
}
|
||||
|
||||
func retreiveCredentials() {
|
||||
if let account = account {
|
||||
let credentials = try? account.retrieveCredentials(type: .newsBlurBasic)
|
||||
if let credentials = credentials {
|
||||
self.accountUserName = credentials.username
|
||||
self.accountPassword = credentials.secret
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var accountDetails: some View {
|
||||
Section {
|
||||
TextField("Email", text: $accountUserName, prompt: Text("Username or Email", comment: "Textfield for the user to enter their account username or email."))
|
||||
.autocorrectionDisabled()
|
||||
.autocapitalization(.none)
|
||||
.textContentType(.username)
|
||||
SecureField("Password", text: $accountPassword, prompt: Text("Password", comment: "Textfield for the user to enter their account password."))
|
||||
.textContentType(.password)
|
||||
}
|
||||
}
|
||||
|
||||
var accountButton: some View {
|
||||
Section {
|
||||
Button {
|
||||
Task {
|
||||
do {
|
||||
if account == nil {
|
||||
// Create a new account
|
||||
try await executeAccountCredentials()
|
||||
} else {
|
||||
// Updating account credentials
|
||||
try await executeAccountCredentials()
|
||||
dismiss()
|
||||
}
|
||||
} catch {
|
||||
accountError = (error, true)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack{
|
||||
Spacer()
|
||||
if account == nil {
|
||||
Text("Add Account", comment: "Button title")
|
||||
} else {
|
||||
Text("Update Credentials", comment: "Button title")
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.disabled(!validateCredentials())
|
||||
}
|
||||
}
|
||||
|
||||
var newsBlurAccountExplainer: some View {
|
||||
if account == nil {
|
||||
return Text("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)", comment: "Explanatory text describing the NewsBlur account")
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
return Text("").multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
private func validateCredentials() -> Bool {
|
||||
if (accountUserName.trimmingWhitespace.count == 0) || (accountPassword.trimmingWhitespace.count == 0) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func executeAccountCredentials() async throws {
|
||||
let trimmedUsername = accountUserName.trimmingWhitespace
|
||||
|
||||
guard (account != nil || !AccountManager.shared.duplicateServiceAccount(type: .newsBlur, username: trimmedUsername)) else {
|
||||
throw LocalizedNetNewsWireError.duplicateAccount
|
||||
}
|
||||
showProgressIndicator = true
|
||||
|
||||
let basicCredentials = Credentials(type: .newsBlurBasic, username: trimmedUsername, 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 {
|
||||
do {
|
||||
try self.account?.removeCredentials(type: .newsBlurBasic)
|
||||
try self.account?.removeCredentials(type: .newsBlurSessionId)
|
||||
} catch {
|
||||
NewsBlurAddAccountView.logger.error("\(error.localizedDescription)")
|
||||
}
|
||||
|
||||
try self.account?.storeCredentials(basicCredentials)
|
||||
try self.account?.storeCredentials(sessionsCredentials)
|
||||
|
||||
self.account?.refreshAll(completion: { result in
|
||||
switch result {
|
||||
case .success(_):
|
||||
showProgressIndicator = false
|
||||
continuation.resume()
|
||||
return
|
||||
case .failure(let failure):
|
||||
showProgressIndicator = false
|
||||
continuation.resume(throwing: failure)
|
||||
return
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
showProgressIndicator = false
|
||||
continuation.resume(throwing: LocalizedNetNewsWireError.keychainError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
showProgressIndicator = false
|
||||
continuation.resume(throwing: LocalizedNetNewsWireError.invalidUsernameOrPassword)
|
||||
return
|
||||
}
|
||||
case .failure(let failure):
|
||||
showProgressIndicator = false
|
||||
continuation.resume(throwing: failure)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NewsBlurAddAccountView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NewsBlurAddAccountView()
|
||||
}
|
||||
}
|
||||
@@ -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: "Account 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
|
||||
}
|
||||
|
||||
}
|
||||
240
iOS/Account/ReaderAPIAddAccountView.swift
Normal file
240
iOS/Account/ReaderAPIAddAccountView.swift
Normal file
@@ -0,0 +1,240 @@
|
||||
//
|
||||
// ReaderAPIAccountView.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Stuart Breckenridge on 16/12/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Account
|
||||
import Secrets
|
||||
import RSWeb
|
||||
import SafariServices
|
||||
import RSCore
|
||||
|
||||
struct ReaderAPIAddAccountView: View {
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
var accountType: AccountType?
|
||||
@State var account: Account?
|
||||
@State private var accountCredentials: Credentials?
|
||||
@State private var accountUserName: String = ""
|
||||
@State private var accountSecret: String = ""
|
||||
@State private var accountAPIUrl: String = ""
|
||||
@State private var showProgressIndicator: Bool = false
|
||||
@State private var accountError: (Error?, Bool) = (nil, false)
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Form {
|
||||
if accountType != nil {
|
||||
AccountSectionHeader(accountType: accountType!)
|
||||
}
|
||||
accountDetails
|
||||
accountButton
|
||||
Section(footer: readerAccountExplainer) {}
|
||||
}
|
||||
.navigationTitle(Text(accountType?.localizedAccountName() ?? ""))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.task {
|
||||
retrieveAccountCredentials()
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button(action: { dismiss() }, label: { Text("Cancel", comment: "Button title") })
|
||||
.disabled(showProgressIndicator)
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
if showProgressIndicator { ProgressView() }
|
||||
}
|
||||
}
|
||||
.alert(Text("Error", comment: "Alert title: Error"), isPresented: $accountError.1) {
|
||||
Button(role: .cancel) {
|
||||
//
|
||||
} label: {
|
||||
Text("Dismiss", comment: "Button title")
|
||||
}
|
||||
} message: {
|
||||
Text(accountError.0?.localizedDescription ?? "")
|
||||
}
|
||||
.interactiveDismissDisabled(showProgressIndicator)
|
||||
.dismissOnExternalContextLaunch()
|
||||
.dismissOnAccountAdd()
|
||||
}
|
||||
}
|
||||
|
||||
var readerAccountExplainer: some View {
|
||||
if accountType == nil { return Text("").multilineTextAlignment(.center) }
|
||||
switch accountType! {
|
||||
case .bazQux:
|
||||
return Text("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? [Sign Up Here](https://bazqux.com)", comment: "Explanatory text describing the BazQux account").multilineTextAlignment(.center)
|
||||
case .inoreader:
|
||||
return Text("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? [Sign Up Here](https://www.inoreader.com)", comment: "Explanatory text describing the Inoreader account").multilineTextAlignment(.center)
|
||||
case .theOldReader:
|
||||
return Text("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)", comment: "Explanatory text describing The Old Reader account").multilineTextAlignment(.center)
|
||||
case .freshRSS:
|
||||
return Text("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)", comment: "Explanatory text describing the FreshRSS account").multilineTextAlignment(.center)
|
||||
default:
|
||||
return Text("").multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
var accountDetails: some View {
|
||||
Section {
|
||||
TextField("Username", text: $accountUserName)
|
||||
.autocorrectionDisabled()
|
||||
.autocapitalization(.none)
|
||||
.textContentType(.username)
|
||||
SecureField("Password", text: $accountSecret)
|
||||
.textContentType(.password)
|
||||
if accountType == .freshRSS && accountCredentials == nil {
|
||||
TextField("FreshRSS URL", text: $accountAPIUrl, prompt: Text("fresh.rss.net/api/greader.php"))
|
||||
.autocorrectionDisabled()
|
||||
.autocapitalization(.none)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var accountButton: some View {
|
||||
Section {
|
||||
Button {
|
||||
Task {
|
||||
do {
|
||||
if account == nil {
|
||||
// Create a new account
|
||||
try await executeAccountCredentials()
|
||||
} else {
|
||||
// Updating account credentials
|
||||
try await executeAccountCredentials()
|
||||
dismiss()
|
||||
}
|
||||
} catch {
|
||||
accountError = (error, true)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
if accountCredentials == nil {
|
||||
Text("Add Account", comment: "Button title")
|
||||
} else {
|
||||
Text("Update Credentials", comment: "Button title")
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.disabled(!validateCredentials())
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - API
|
||||
|
||||
private func retrieveAccountCredentials() {
|
||||
if let account = account {
|
||||
do {
|
||||
if let creds = try account.retrieveCredentials(type: .readerBasic) {
|
||||
self.accountCredentials = creds
|
||||
accountUserName = creds.username
|
||||
accountSecret = creds.secret
|
||||
}
|
||||
} catch {
|
||||
accountError = (error, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func validateCredentials() -> Bool {
|
||||
if accountType == nil { return false }
|
||||
switch accountType! {
|
||||
case .freshRSS:
|
||||
if (accountUserName.trimmingWhitespace.count == 0) || (accountSecret.trimmingWhitespace.count == 0) || (accountAPIUrl.trimmingWhitespace.count == 0) {
|
||||
return false
|
||||
}
|
||||
default:
|
||||
if (accountUserName.trimmingWhitespace.count == 0) || (accountSecret.trimmingWhitespace.count == 0) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func executeAccountCredentials() async throws {
|
||||
|
||||
let trimmedAccountUserName = accountUserName.trimmingWhitespace
|
||||
|
||||
guard (account != nil || !AccountManager.shared.duplicateServiceAccount(type: accountType!, username: trimmedAccountUserName)) else {
|
||||
throw LocalizedNetNewsWireError.duplicateAccount
|
||||
}
|
||||
|
||||
showProgressIndicator = true
|
||||
let credentials = Credentials(type: .readerBasic, username: trimmedAccountUserName, secret: accountSecret)
|
||||
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
Account.validateCredentials(type: accountType!, credentials: credentials, endpoint: apiURL()) { result in
|
||||
switch result {
|
||||
case .success(let validatedCredentials):
|
||||
if let validatedCredentials = validatedCredentials {
|
||||
if self.account == nil {
|
||||
self.account = AccountManager.shared.createAccount(type: accountType!)
|
||||
}
|
||||
|
||||
do {
|
||||
self.account?.endpointURL = apiURL()
|
||||
try? self.account?.removeCredentials(type: .readerBasic)
|
||||
try? self.account?.removeCredentials(type: .readerAPIKey)
|
||||
try self.account?.storeCredentials(credentials)
|
||||
try self.account?.storeCredentials(validatedCredentials)
|
||||
|
||||
self.account?.refreshAll(completion: { result in
|
||||
switch result {
|
||||
case .success:
|
||||
showProgressIndicator = false
|
||||
continuation.resume()
|
||||
return
|
||||
case .failure(let error):
|
||||
showProgressIndicator = false
|
||||
continuation.resume(throwing: error)
|
||||
return
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
showProgressIndicator = false
|
||||
continuation.resume(throwing: LocalizedNetNewsWireError.keychainError)
|
||||
return
|
||||
}
|
||||
}
|
||||
case .failure(let failure):
|
||||
showProgressIndicator = false
|
||||
continuation.resume(throwing: failure)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func apiURL() -> URL? {
|
||||
switch accountType! {
|
||||
case .freshRSS:
|
||||
return URL(string: accountAPIUrl)!
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct ReaderAPIAccountView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ReaderAPIAddAccountView()
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
enum UserInterfaceColorPalette: Int, CustomStringConvertible, CaseIterable {
|
||||
case automatic = 0
|
||||
@@ -26,7 +28,7 @@ enum UserInterfaceColorPalette: Int, CustomStringConvertible, CaseIterable {
|
||||
|
||||
}
|
||||
|
||||
final class AppDefaults {
|
||||
final class AppDefaults: ObservableObject {
|
||||
|
||||
static let defaultThemeName = "NetNewsWire"
|
||||
|
||||
@@ -86,6 +88,7 @@ final class AppDefaults {
|
||||
}
|
||||
set {
|
||||
setInt(for: Key.userInterfaceColorPalette, newValue.rawValue)
|
||||
AppDefaults.shared.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +98,7 @@ final class AppDefaults {
|
||||
}
|
||||
set {
|
||||
AppDefaults.setString(for: Key.addWebFeedAccountID, newValue)
|
||||
AppDefaults.shared.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +108,7 @@ final class AppDefaults {
|
||||
}
|
||||
set {
|
||||
AppDefaults.setString(for: Key.addWebFeedFolderName, newValue)
|
||||
AppDefaults.shared.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +118,7 @@ final class AppDefaults {
|
||||
}
|
||||
set {
|
||||
AppDefaults.setString(for: Key.addFolderAccountID, newValue)
|
||||
AppDefaults.shared.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +128,7 @@ final class AppDefaults {
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: Key.activeExtensionPointIDs)
|
||||
AppDefaults.shared.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +138,7 @@ final class AppDefaults {
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: Key.hasUsedFullScreenPreviously)
|
||||
AppDefaults.shared.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +148,7 @@ final class AppDefaults {
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.setValue(newValue, forKey: Key.useSystemBrowser)
|
||||
AppDefaults.shared.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +158,7 @@ final class AppDefaults {
|
||||
}
|
||||
set {
|
||||
AppDefaults.setDate(for: Key.lastImageCacheFlushDate, newValue)
|
||||
AppDefaults.shared.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,6 +168,7 @@ final class AppDefaults {
|
||||
}
|
||||
set {
|
||||
AppDefaults.setBool(for: Key.timelineGroupByFeed, newValue)
|
||||
AppDefaults.shared.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,6 +178,7 @@ final class AppDefaults {
|
||||
}
|
||||
set {
|
||||
AppDefaults.setBool(for: Key.refreshClearsReadArticles, newValue)
|
||||
AppDefaults.shared.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,15 +188,33 @@ final class AppDefaults {
|
||||
}
|
||||
set {
|
||||
AppDefaults.setSortDirection(for: Key.timelineSortDirection, newValue)
|
||||
AppDefaults.shared.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// This is a `Bool` wrapper for `timelineSortDirection`'s
|
||||
/// `ComparisonResult`
|
||||
var timelineSortDirectionBool: Bool {
|
||||
get {
|
||||
if AppDefaults.shared.timelineSortDirection == .orderedAscending {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
set {
|
||||
if newValue == true { timelineSortDirection = .orderedAscending } else {
|
||||
timelineSortDirection = .orderedDescending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var articleFullscreenEnabled: Bool {
|
||||
get {
|
||||
return AppDefaults.bool(for: Key.articleFullscreenEnabled)
|
||||
}
|
||||
set {
|
||||
AppDefaults.setBool(for: Key.articleFullscreenEnabled, newValue)
|
||||
AppDefaults.shared.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +233,7 @@ final class AppDefaults {
|
||||
}
|
||||
set {
|
||||
AppDefaults.setInt(for: Key.timelineNumberOfLines, newValue)
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,6 +244,7 @@ final class AppDefaults {
|
||||
}
|
||||
set {
|
||||
AppDefaults.store.set(newValue.rawValue, forKey: Key.timelineIconDimension)
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,6 +254,7 @@ final class AppDefaults {
|
||||
}
|
||||
set {
|
||||
AppDefaults.setString(for: Key.currentThemeName, newValue)
|
||||
AppDefaults.shared.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,6 +273,7 @@ final class AppDefaults {
|
||||
}
|
||||
set {
|
||||
AppDefaults.setBool(for: Key.markArticlesAsReadOnScroll, newValue)
|
||||
AppDefaults.shared.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
145
iOS/Inspector/AccountInspectorView.swift
Normal file
145
iOS/Inspector/AccountInspectorView.swift
Normal file
@@ -0,0 +1,145 @@
|
||||
//
|
||||
// AccountInspectorView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 15/12/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SafariServices
|
||||
import Account
|
||||
|
||||
struct AccountInspectorView: View {
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@State private var showRemoveAccountAlert: Bool = false
|
||||
@State private var showAccountCredentialsSheet: Bool = false
|
||||
var account: Account
|
||||
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
AccountSectionHeader(accountType: account.type)
|
||||
accountNameAndActiveSection
|
||||
|
||||
if account.type != .onMyMac &&
|
||||
account.type != .cloudKit &&
|
||||
account.type != .feedly {
|
||||
credentialsSection
|
||||
}
|
||||
|
||||
if account != AccountManager.shared.defaultAccount {
|
||||
removeAccountSection
|
||||
}
|
||||
|
||||
if account.type == .cloudKit {
|
||||
Section(footer: cloudKitLimitations){}
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle(account.nameForDisplay)
|
||||
.tint(Color(uiColor: AppAssets.primaryAccentColor))
|
||||
.edgesIgnoringSafeArea(.bottom)
|
||||
.sheet(isPresented: $showAccountCredentialsSheet) {
|
||||
switch account.type {
|
||||
case .theOldReader, .bazQux, .inoreader, .freshRSS:
|
||||
ReaderAPIAddAccountView(accountType: account.type, account: account)
|
||||
case .feedbin:
|
||||
FeedbinAddAccountView(account: account)
|
||||
case .newsBlur:
|
||||
NewsBlurAddAccountView(account: account)
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.dismissOnExternalContextLaunch()
|
||||
}
|
||||
|
||||
var accountHeaderView: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
Image(uiImage: account.smallIcon!.image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 48, height: 48)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
var accountNameAndActiveSection: some View {
|
||||
Section {
|
||||
TextField(text: Binding(
|
||||
get: { account.name ?? account.defaultName },
|
||||
set: { account.name = $0 }),
|
||||
prompt: Text(account.defaultName)) {
|
||||
Text("Name", comment: "Textfield for the user to enter account name.")
|
||||
}
|
||||
|
||||
Toggle(isOn: Binding(get: {
|
||||
account.isActive
|
||||
}, set: { account.isActive = $0 })) {
|
||||
Text("Active", comment: "Toggle denoting if the account is active.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var credentialsSection: some View {
|
||||
Section {
|
||||
Button {
|
||||
showAccountCredentialsSheet = true
|
||||
} label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("Credentials", comment: "Button title")
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var removeAccountSection: some View {
|
||||
Section {
|
||||
Button(role: .destructive) {
|
||||
showRemoveAccountAlert = true
|
||||
} label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("Remove Account", comment: "Button title")
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.alert(Text("Are you sure you want to remove “\(account.nameForDisplay)”?", comment: "Alert title: confirm account removal"), isPresented: $showRemoveAccountAlert) {
|
||||
Button(role: .destructive) {
|
||||
AccountManager.shared.deleteAccount(account)
|
||||
dismiss()
|
||||
} label: {
|
||||
Text("Remove Account", comment: "Button title")
|
||||
}
|
||||
|
||||
Button(role: .cancel) {
|
||||
//
|
||||
} label: {
|
||||
Text("Cancel", comment: "Button title")
|
||||
}
|
||||
|
||||
} message: {
|
||||
Text("This action cannot be undone.", comment: "Alert message: remove account confirmation")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var cloudKitLimitations: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("[iCloud Syncing Limitations & Solutions](https://netnewswire.com/help/iCloud)", comment: "Link to the NetNewsWire iCloud syncing limitations and soltutions website.")
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AccountInspectorView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AccountInspectorView(account: AccountManager.shared.defaultAccount)
|
||||
}
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
//
|
||||
// AccountInspectorViewController.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 5/17/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SafariServices
|
||||
import Account
|
||||
|
||||
class AccountInspectorViewController: UITableViewController {
|
||||
|
||||
static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 400.0)
|
||||
|
||||
@IBOutlet weak var nameTextField: UITextField!
|
||||
@IBOutlet weak var activeSwitch: UISwitch!
|
||||
@IBOutlet weak var deleteAccountButton: VibrantButton!
|
||||
@IBOutlet weak var limitationsAndSolutionsView: UIView!
|
||||
|
||||
var isModal = false
|
||||
weak var account: Account?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
guard let account = account else { return }
|
||||
|
||||
nameTextField.placeholder = account.defaultName
|
||||
nameTextField.text = account.name
|
||||
nameTextField.delegate = self
|
||||
activeSwitch.isOn = account.isActive
|
||||
|
||||
navigationItem.title = account.nameForDisplay
|
||||
|
||||
if account.type != .onMyMac {
|
||||
deleteAccountButton.setTitle(NSLocalizedString("Remove Account", comment: "Remove Account"), for: .normal)
|
||||
}
|
||||
|
||||
if account.type != .cloudKit {
|
||||
limitationsAndSolutionsView.isHidden = true
|
||||
}
|
||||
|
||||
if isModal {
|
||||
let doneBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(done))
|
||||
navigationItem.leftBarButtonItem = doneBarButtonItem
|
||||
}
|
||||
|
||||
tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
|
||||
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
account?.name = nameTextField.text
|
||||
account?.isActive = activeSwitch.isOn
|
||||
}
|
||||
|
||||
@objc func done() {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
@IBAction func credentials(_ sender: Any) {
|
||||
guard let account = account else { return }
|
||||
switch account.type {
|
||||
case .feedbin:
|
||||
let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "FeedbinAccountNavigationViewController") as! UINavigationController
|
||||
let addViewController = navController.topViewController as! FeedbinAccountViewController
|
||||
addViewController.account = account
|
||||
navController.modalPresentationStyle = .currentContext
|
||||
present(navController, animated: true)
|
||||
case .newsBlur:
|
||||
let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "NewsBlurAccountNavigationViewController") as! UINavigationController
|
||||
let addViewController = navController.topViewController as! NewsBlurAccountViewController
|
||||
addViewController.account = account
|
||||
navController.modalPresentationStyle = .currentContext
|
||||
present(navController, animated: true)
|
||||
case .inoreader, .bazQux, .theOldReader, .freshRSS:
|
||||
let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "ReaderAPIAccountNavigationViewController") as! UINavigationController
|
||||
let addViewController = navController.topViewController as! ReaderAPIAccountViewController
|
||||
addViewController.accountType = account.type
|
||||
addViewController.account = account
|
||||
navController.modalPresentationStyle = .currentContext
|
||||
present(navController, animated: true)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func deleteAccount(_ sender: Any) {
|
||||
guard let account = account else {
|
||||
return
|
||||
}
|
||||
|
||||
let title = NSLocalizedString("Remove Account", comment: "Remove Account")
|
||||
let message: String = {
|
||||
switch account.type {
|
||||
case .feedly:
|
||||
return NSLocalizedString("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.", comment: "Log Out and Remove Account")
|
||||
default:
|
||||
return NSLocalizedString("Are you sure you want to remove this account? This cannot be undone.", comment: "Remove Account")
|
||||
}
|
||||
}()
|
||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
|
||||
let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel)
|
||||
alertController.addAction(cancelAction)
|
||||
|
||||
let markTitle = NSLocalizedString("Remove", comment: "Remove")
|
||||
let markAction = UIAlertAction(title: markTitle, style: .default) { [weak self] (action) in
|
||||
guard let self = self, let account = self.account else { return }
|
||||
AccountManager.shared.deleteAccount(account)
|
||||
if self.isModal {
|
||||
self.dismiss(animated: true)
|
||||
} else {
|
||||
self.navigationController?.popViewController(animated: true)
|
||||
}
|
||||
}
|
||||
alertController.addAction(markAction)
|
||||
alertController.preferredAction = markAction
|
||||
|
||||
present(alertController, animated: true)
|
||||
}
|
||||
|
||||
@IBAction func openLimitationsAndSolutions(_ sender: Any) {
|
||||
let vc = SFSafariViewController(url: CloudKitWebDocumentation.limitationsAndSolutionsURL)
|
||||
vc.modalPresentationStyle = .pageSheet
|
||||
present(vc, animated: true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Table View
|
||||
|
||||
extension AccountInspectorViewController {
|
||||
|
||||
var hidesCredentialsSection: Bool {
|
||||
guard let account = account else {
|
||||
return true
|
||||
}
|
||||
switch account.type {
|
||||
case .onMyMac, .cloudKit, .feedly:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
guard let account = account else { return 0 }
|
||||
|
||||
if account == AccountManager.shared.defaultAccount {
|
||||
return 1
|
||||
} else if hidesCredentialsSection {
|
||||
return 2
|
||||
} else {
|
||||
return super.numberOfSections(in: tableView)
|
||||
}
|
||||
}
|
||||
|
||||
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? {
|
||||
guard let account = account else { return nil }
|
||||
|
||||
if section == 0 {
|
||||
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView
|
||||
headerView.imageView.image = AppAssets.image(for: account.type)
|
||||
return headerView
|
||||
} else {
|
||||
return super.tableView(tableView, viewForHeaderInSection: section)
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: UITableViewCell
|
||||
|
||||
if indexPath.section == 1, hidesCredentialsSection {
|
||||
cell = super.tableView(tableView, cellForRowAt: IndexPath(row: 0, section: 2))
|
||||
} else {
|
||||
cell = super.tableView(tableView, cellForRowAt: indexPath)
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
|
||||
if indexPath.section > 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: UITextFieldDelegate
|
||||
|
||||
extension AccountInspectorViewController: UITextFieldDelegate {
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
textField.resignFirstResponder()
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
79
iOS/Inspector/ExtensionInspectorView.swift
Normal file
79
iOS/Inspector/ExtensionInspectorView.swift
Normal file
@@ -0,0 +1,79 @@
|
||||
//
|
||||
// ExtensionInspectorView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 15/12/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ExtensionInspectorView: View {
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@State private var showDeactivateConfirmation: Bool = false
|
||||
var extensionPoint: ExtensionPoint?
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section(header: extensionHeader) {}
|
||||
Section(footer: extensionExplainer, content: {
|
||||
//
|
||||
})
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(role: .destructive) {
|
||||
showDeactivateConfirmation = true
|
||||
} label: {
|
||||
Text("Deactivate Extension", comment: "Button title")
|
||||
}
|
||||
.alert(Text("Are you sure you want to deactivate “\(extensionPoint?.title ?? "")?", comment: "Alert title: confirm deactivate extension") , isPresented: $showDeactivateConfirmation) {
|
||||
|
||||
Button(role: .destructive) {
|
||||
ExtensionPointManager.shared.deactivateExtensionPoint(extensionPoint!.extensionPointID)
|
||||
dismiss()
|
||||
} label: {
|
||||
Text("Deactivate Extension", comment: "Button title")
|
||||
}
|
||||
|
||||
Button(role: .cancel) {
|
||||
//
|
||||
} label: {
|
||||
Text("Cancel", comment: "Button title")
|
||||
}
|
||||
} message: {
|
||||
Text("This action cannot be undone.", comment: "Alert message: remove account confirmation")
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
.navigationTitle(Text(extensionPoint?.title ?? ""))
|
||||
.edgesIgnoringSafeArea(.bottom)
|
||||
.dismissOnExternalContextLaunch()
|
||||
}
|
||||
|
||||
var extensionHeader: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
Image(uiImage: extensionPoint!.image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 48, height: 48)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
var extensionExplainer: some View {
|
||||
Text(extensionPoint?.description.string ?? "")
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
|
||||
struct ExtensionInspectorView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ExtensionInspectorView()
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
//
|
||||
// ExtensionPointInspectorViewController.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 4/16/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class ExtensionPointInspectorViewController: UITableViewController {
|
||||
|
||||
@IBOutlet weak var extensionDescription: UILabel!
|
||||
var extensionPoint: ExtensionPoint?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
guard let extensionPoint = extensionPoint else { return }
|
||||
navigationItem.title = extensionPoint.title
|
||||
extensionDescription.attributedText = extensionPoint.description
|
||||
tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
|
||||
}
|
||||
|
||||
@IBAction func disable(_ sender: Any) {
|
||||
guard let extensionPoint = extensionPoint else { return }
|
||||
|
||||
let title = NSLocalizedString("Deactivate Extension", comment: "Deactivate Extension")
|
||||
let extensionPointTypeTitle = extensionPoint.extensionPointID.extensionPointType.title
|
||||
let message = NSLocalizedString("Are you sure you want to deactivate the \(extensionPointTypeTitle) extension “\(extensionPoint.title)”?", comment: "Deactivate text")
|
||||
|
||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
|
||||
let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel)
|
||||
alertController.addAction(cancelAction)
|
||||
|
||||
let markTitle = NSLocalizedString("Deactivate", comment: "Deactivate")
|
||||
let markAction = UIAlertAction(title: markTitle, style: .default) { [weak self] (action) in
|
||||
ExtensionPointManager.shared.deactivateExtensionPoint(extensionPoint.extensionPointID)
|
||||
self?.navigationController?.popViewController(animated: true)
|
||||
}
|
||||
alertController.addAction(markAction)
|
||||
alertController.preferredAction = markAction
|
||||
|
||||
present(alertController, animated: true)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Table View
|
||||
|
||||
extension ExtensionPointInspectorViewController {
|
||||
|
||||
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? {
|
||||
guard let extensionPoint = extensionPoint else { return nil }
|
||||
|
||||
if section == 0 {
|
||||
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView
|
||||
headerView.imageView.image = extensionPoint.image
|
||||
return headerView
|
||||
} else {
|
||||
return super.tableView(tableView, viewForHeaderInSection: section)
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
|
||||
if indexPath.section > 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,516 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Account Inspector View Controller-->
|
||||
<scene sceneID="nCF-Ns-xpY">
|
||||
<objects>
|
||||
<tableViewController storyboardIdentifier="AccountInspectorViewController" id="1m3-fZ-n7g" customClass="AccountInspectorViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="xcc-i3-tPS">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<view key="tableFooterView" contentMode="scaleToFill" id="3V2-Cm-ezj">
|
||||
<rect key="frame" x="0.0" y="282" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="dgD-uX-vcx">
|
||||
<rect key="frame" x="75" y="7" width="264" height="30"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
|
||||
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
|
||||
<state key="normal" title="iCloud Syncing Limitations & Solutions"/>
|
||||
<connections>
|
||||
<action selector="openLimitationsAndSolutions:" destination="1m3-fZ-n7g" eventType="touchUpInside" id="DKt-dF-a6L"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="dgD-uX-vcx" firstAttribute="centerX" secondItem="3V2-Cm-ezj" secondAttribute="centerX" id="IDg-5p-aZs"/>
|
||||
<constraint firstItem="dgD-uX-vcx" firstAttribute="centerY" secondItem="3V2-Cm-ezj" secondAttribute="centerY" id="w0c-Qa-ADC"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<sections>
|
||||
<tableViewSection id="vec-ab-Ylg">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="4Ue-UW-e0l" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="18" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="4Ue-UW-e0l" id="9E1-ww-kYn">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="19" translatesAutoresizingMaskIntoConstraints="NO" id="mQa-0W-eVS">
|
||||
<rect key="frame" x="20" y="11" width="334" height="22"/>
|
||||
<subviews>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Name (Optional)" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="LUW-uv-piz">
|
||||
<rect key="frame" x="0.0" y="0.0" width="334" height="22"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="words"/>
|
||||
</textField>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="mQa-0W-eVS" firstAttribute="leading" secondItem="9E1-ww-kYn" secondAttribute="leading" constant="20" symbolic="YES" id="ATM-Pf-PSm"/>
|
||||
<constraint firstItem="mQa-0W-eVS" firstAttribute="centerY" secondItem="9E1-ww-kYn" secondAttribute="centerY" id="DWl-vJ-i3I"/>
|
||||
<constraint firstAttribute="trailing" secondItem="mQa-0W-eVS" secondAttribute="trailing" constant="20" symbolic="YES" id="aIV-cb-QTV"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="zQY-gY-BOY" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="61.5" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="zQY-gY-BOY" id="dBp-J5-ZsY">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Active" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="zf0-Gm-p4F">
|
||||
<rect key="frame" x="20" y="11.5" width="47.5" height="20.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6YV-K0-yPS">
|
||||
<rect key="frame" x="305" y="6.5" width="51" height="31"/>
|
||||
<color key="onTintColor" name="primaryAccentColor"/>
|
||||
</switch>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="zf0-Gm-p4F" firstAttribute="leading" secondItem="dBp-J5-ZsY" secondAttribute="leading" constant="20" symbolic="YES" id="lAQ-Ps-JwD"/>
|
||||
<constraint firstItem="zf0-Gm-p4F" firstAttribute="centerY" secondItem="dBp-J5-ZsY" secondAttribute="centerY" id="ofD-kM-lP4"/>
|
||||
<constraint firstItem="6YV-K0-yPS" firstAttribute="centerY" secondItem="dBp-J5-ZsY" secondAttribute="centerY" id="yOM-Bc-HU8"/>
|
||||
<constraint firstAttribute="trailing" secondItem="6YV-K0-yPS" secondAttribute="trailing" constant="20" symbolic="YES" id="yk4-Dh-YVh"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection id="9UW-s0-NPI">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="FsT-vH-rTo" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="141" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="FsT-vH-rTo" id="rJW-6J-9DM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="TYD-py-8IF" customClass="VibrantButton" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="-0.5" width="374" height="44.5"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="44" id="WmU-wG-RLQ"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<state key="normal" title="Credentials">
|
||||
<color key="titleColor" name="secondaryAccentColor"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="credentials:" destination="1m3-fZ-n7g" eventType="touchUpInside" id="Kkh-Ag-aGX"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="TYD-py-8IF" firstAttribute="centerY" secondItem="rJW-6J-9DM" secondAttribute="centerY" id="We5-ck-yMq"/>
|
||||
<constraint firstItem="TYD-py-8IF" firstAttribute="leading" secondItem="rJW-6J-9DM" secondAttribute="leading" id="gIs-Z2-bBf"/>
|
||||
<constraint firstAttribute="trailing" secondItem="TYD-py-8IF" secondAttribute="trailing" id="uFe-xv-QBo"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection id="mgY-wW-xiO">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="lgY-im-vCo" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="220.5" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="lgY-im-vCo" id="fIH-xP-nza">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="obv-a5-Pl6" customClass="VibrantButton" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="-0.5" width="374" height="44.5"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="44" id="WtN-fp-Ldt"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<state key="normal" title="Remove Account">
|
||||
<color key="titleColor" systemColor="systemRedColor"/>
|
||||
</state>
|
||||
<state key="highlighted">
|
||||
<color key="titleColor" systemColor="systemRedColor"/>
|
||||
</state>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="color" keyPath="backgroundHighlightColor">
|
||||
<color key="value" name="deleteBackgroundColor"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="deleteAccount:" destination="1m3-fZ-n7g" eventType="touchUpInside" id="gYt-Fi-Eqe"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="obv-a5-Pl6" secondAttribute="trailing" id="1Po-cv-lMZ"/>
|
||||
<constraint firstItem="obv-a5-Pl6" firstAttribute="leading" secondItem="fIH-xP-nza" secondAttribute="leading" id="4Xk-ff-eUv"/>
|
||||
<constraint firstItem="obv-a5-Pl6" firstAttribute="centerY" secondItem="fIH-xP-nza" secondAttribute="centerY" id="Vs4-U1-hKv"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
</sections>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="1m3-fZ-n7g" id="eTZ-Uy-P3l"/>
|
||||
<outlet property="delegate" destination="1m3-fZ-n7g" id="ZUV-ww-yOH"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" id="Fkf-MF-Fdf"/>
|
||||
<connections>
|
||||
<outlet property="activeSwitch" destination="6YV-K0-yPS" id="d9M-GP-aTR"/>
|
||||
<outlet property="deleteAccountButton" destination="obv-a5-Pl6" id="idW-gm-BIJ"/>
|
||||
<outlet property="limitationsAndSolutionsView" destination="3V2-Cm-ezj" id="Na9-t7-crH"/>
|
||||
<outlet property="nameTextField" destination="LUW-uv-piz" id="e2P-Hq-guh"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="zcI-Zg-28D" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="863.768115942029" y="-591.29464285714278"/>
|
||||
</scene>
|
||||
<!--Web Feed Inspector View Controller-->
|
||||
<scene sceneID="jnI-2I-AcU">
|
||||
<objects>
|
||||
<tableViewController storyboardIdentifier="FeedInspectorViewControllelr" id="lEH-bG-pQW" customClass="WebFeedInspectorViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="26V-ZC-Q2R">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<sections>
|
||||
<tableViewSection id="LEk-I1-Kkn">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="FPJ-5s-QTm">
|
||||
<rect key="frame" x="20" y="18" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="FPJ-5s-QTm" id="S7f-Et-p8x">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Name" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="ZdA-rl-9eP">
|
||||
<rect key="frame" x="20" y="11" width="334" height="22"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="words"/>
|
||||
</textField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="ZdA-rl-9eP" firstAttribute="leading" secondItem="S7f-Et-p8x" secondAttribute="leading" constant="20" symbolic="YES" id="9aN-eQ-OxO"/>
|
||||
<constraint firstAttribute="trailing" secondItem="ZdA-rl-9eP" secondAttribute="trailing" constant="20" symbolic="YES" id="AAZ-un-HNQ"/>
|
||||
<constraint firstItem="ZdA-rl-9eP" firstAttribute="centerY" secondItem="S7f-Et-p8x" secondAttribute="centerY" id="CDZ-Wg-H0P"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="6Yy-4a-kYM">
|
||||
<rect key="frame" x="20" y="61.5" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="6Yy-4a-kYM" id="FfV-P4-TNg">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Notify About New Articles" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="YV2-gG-lMP">
|
||||
<rect key="frame" x="24" y="11.5" width="196.5" height="20.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="g39-wK-xUs">
|
||||
<rect key="frame" x="305" y="6.5" width="51" height="31"/>
|
||||
<color key="onTintColor" name="primaryAccentColor"/>
|
||||
<connections>
|
||||
<action selector="notifyAboutNewArticlesChanged:" destination="lEH-bG-pQW" eventType="valueChanged" id="1Mv-Im-H5v"/>
|
||||
</connections>
|
||||
</switch>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="g39-wK-xUs" firstAttribute="centerY" secondItem="FfV-P4-TNg" secondAttribute="centerY" id="2bF-Jp-tob"/>
|
||||
<constraint firstItem="YV2-gG-lMP" firstAttribute="centerY" secondItem="FfV-P4-TNg" secondAttribute="centerY" id="IaF-U5-CVN"/>
|
||||
<constraint firstItem="g39-wK-xUs" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="YV2-gG-lMP" secondAttribute="trailing" constant="8" id="PfE-bK-rrS"/>
|
||||
<constraint firstItem="YV2-gG-lMP" firstAttribute="leading" secondItem="FfV-P4-TNg" secondAttribute="leadingMargin" constant="4" id="dT8-rP-umA"/>
|
||||
<constraint firstAttribute="trailing" secondItem="g39-wK-xUs" secondAttribute="trailing" constant="20" symbolic="YES" id="hMe-jH-tbS"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="glX-68-wCa">
|
||||
<rect key="frame" x="20" y="105" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="glX-68-wCa" id="FQk-5f-EtL">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Always Use Reader View" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Bf4-3X-Rfr">
|
||||
<rect key="frame" x="24" y="11.5" width="187" height="20.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="fUd-JB-UMK">
|
||||
<rect key="frame" x="305" y="6.5" width="51" height="31"/>
|
||||
<color key="onTintColor" name="primaryAccentColor"/>
|
||||
<connections>
|
||||
<action selector="alwaysShowReaderViewChanged:" destination="lEH-bG-pQW" eventType="valueChanged" id="grg-EM-T3u"/>
|
||||
</connections>
|
||||
</switch>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="fUd-JB-UMK" secondAttribute="trailing" constant="20" symbolic="YES" id="4vJ-pa-iCa"/>
|
||||
<constraint firstItem="fUd-JB-UMK" firstAttribute="centerY" secondItem="FQk-5f-EtL" secondAttribute="centerY" id="BT1-8x-hrj"/>
|
||||
<constraint firstItem="Bf4-3X-Rfr" firstAttribute="leading" secondItem="FQk-5f-EtL" secondAttribute="leadingMargin" constant="4" id="vqc-MM-8vz"/>
|
||||
<constraint firstItem="Bf4-3X-Rfr" firstAttribute="centerY" secondItem="FQk-5f-EtL" secondAttribute="centerY" id="wyr-Kk-g5H"/>
|
||||
<constraint firstItem="fUd-JB-UMK" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Bf4-3X-Rfr" secondAttribute="trailing" constant="8" id="ywd-qz-cAd"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="Home Page" id="dTd-6q-SZd">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="0zc-o6-Sjh" customClass="VibrantBasicTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="204.5" width="374" height="22.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="0zc-o6-Sjh" id="vJs-XK-ebf">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="22.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="characterWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="HUP-Cu-FGT">
|
||||
<rect key="frame" x="20" y="11" width="301" height="0.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="safari" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="p82-kn-lfh">
|
||||
<rect key="frame" x="329" y="-1" width="25" height="24.5"/>
|
||||
<color key="tintColor" name="primaryAccentColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="25" id="Suu-bu-Lar"/>
|
||||
<constraint firstAttribute="height" constant="25" id="gD0-zu-2pr"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="p82-kn-lfh" firstAttribute="leading" secondItem="HUP-Cu-FGT" secondAttribute="trailing" constant="8" symbolic="YES" id="4Ze-t9-SMR"/>
|
||||
<constraint firstAttribute="trailing" secondItem="p82-kn-lfh" secondAttribute="trailing" constant="20" symbolic="YES" id="69m-bk-BHO"/>
|
||||
<constraint firstItem="HUP-Cu-FGT" firstAttribute="leading" secondItem="vJs-XK-ebf" secondAttribute="leadingMargin" id="GrO-sc-ZMe"/>
|
||||
<constraint firstAttribute="bottomMargin" secondItem="HUP-Cu-FGT" secondAttribute="bottom" id="Jtq-bB-vJN"/>
|
||||
<constraint firstItem="p82-kn-lfh" firstAttribute="centerY" secondItem="vJs-XK-ebf" secondAttribute="centerY" id="f1u-Mm-Arn"/>
|
||||
<constraint firstItem="HUP-Cu-FGT" firstAttribute="top" secondItem="vJs-XK-ebf" secondAttribute="topMargin" id="lBd-G7-RdW"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="image" keyPath="imageNormal" value="safari" catalog="system"/>
|
||||
<userDefinedRuntimeAttribute type="image" keyPath="imageSelected" value="safari.fill" catalog="system"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<outlet property="icon" destination="p82-kn-lfh" id="qYr-gp-cbS"/>
|
||||
<outlet property="label" destination="HUP-Cu-FGT" id="FWP-ba-TIm"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="Feed URL" id="MtQ-oG-lrU">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="fKD-Vi-B8O">
|
||||
<rect key="frame" x="20" y="283" width="374" height="22.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="fKD-Vi-B8O" id="2G0-9f-qwN">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="22.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="characterWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rOV-XS-bNW" customClass="InteractiveLabel" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="11" width="334" height="0.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="rOV-XS-bNW" secondAttribute="trailing" id="M1N-hj-51g"/>
|
||||
<constraint firstItem="rOV-XS-bNW" firstAttribute="top" secondItem="2G0-9f-qwN" secondAttribute="topMargin" id="SCe-Ln-xRa"/>
|
||||
<constraint firstAttribute="bottomMargin" secondItem="rOV-XS-bNW" secondAttribute="bottom" id="cfZ-oe-dTK"/>
|
||||
<constraint firstItem="rOV-XS-bNW" firstAttribute="leading" secondItem="2G0-9f-qwN" secondAttribute="leadingMargin" id="kTa-5N-K3n"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
</sections>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="lEH-bG-pQW" id="B83-a4-nKt"/>
|
||||
<outlet property="delegate" destination="lEH-bG-pQW" id="M6w-j0-89G"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" id="RpI-ip-5vN">
|
||||
<barButtonItem key="leftBarButtonItem" style="done" systemItem="done" id="jly-Cb-Ezg">
|
||||
<connections>
|
||||
<action selector="done:" destination="lEH-bG-pQW" id="Jie-OM-mje"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="alwaysShowReaderViewSwitch" destination="fUd-JB-UMK" id="kUh-ob-Ego"/>
|
||||
<outlet property="feedURLLabel" destination="rOV-XS-bNW" id="mPv-yb-H4I"/>
|
||||
<outlet property="homePageLabel" destination="HUP-Cu-FGT" id="EDm-vt-2r0"/>
|
||||
<outlet property="nameTextField" destination="ZdA-rl-9eP" id="GzR-uc-fpV"/>
|
||||
<outlet property="notifyAboutNewArticlesSwitch" destination="g39-wK-xUs" id="ahS-ZK-6xN"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="qKF-0N-vfa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="864" y="58"/>
|
||||
</scene>
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="fZt-Nz-Jvk">
|
||||
<objects>
|
||||
<navigationController storyboardIdentifier="FeedInspectorNavigationViewController" id="Ybm-En-qAg" sceneMemberID="viewController">
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="YQK-Se-EBX">
|
||||
<rect key="frame" x="0.0" y="48" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<connections>
|
||||
<segue destination="lEH-bG-pQW" kind="relationship" relationship="rootViewController" id="zEG-1X-hgE"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="8Yb-xu-2p2" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="168" y="58"/>
|
||||
</scene>
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="Oea-QH-lLX">
|
||||
<objects>
|
||||
<navigationController storyboardIdentifier="AccountInspectorNavigationViewController" id="5wr-gz-V2t" sceneMemberID="viewController">
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="3Os-JI-n4e">
|
||||
<rect key="frame" x="0.0" y="48" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<connections>
|
||||
<segue destination="1m3-fZ-n7g" kind="relationship" relationship="rootViewController" id="N6Y-D7-T01"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Uz1-sQ-sSV" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="157" y="-591"/>
|
||||
</scene>
|
||||
<!--Extension Point Inspector View Controller-->
|
||||
<scene sceneID="OYN-w4-caJ">
|
||||
<objects>
|
||||
<tableViewController storyboardIdentifier="ExtensionPointInspectorViewController" id="1B7-3Y-VYf" customClass="ExtensionPointInspectorViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="sMo-hq-Gps">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<sections>
|
||||
<tableViewSection id="hGI-fH-ovr">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="hx7-HU-HV2" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="18" width="374" height="43"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="hx7-HU-HV2" id="fEV-cR-i6h">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="YJL-5w-N2S">
|
||||
<rect key="frame" x="20" y="11" width="334" height="21"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="YJL-5w-N2S" firstAttribute="leading" secondItem="fEV-cR-i6h" secondAttribute="leadingMargin" id="kx6-6v-VNw"/>
|
||||
<constraint firstItem="YJL-5w-N2S" firstAttribute="top" secondItem="fEV-cR-i6h" secondAttribute="topMargin" id="l6Y-A0-BFs"/>
|
||||
<constraint firstAttribute="bottomMargin" secondItem="YJL-5w-N2S" secondAttribute="bottom" id="nKi-1U-g6E"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="YJL-5w-N2S" secondAttribute="trailing" id="o5n-Co-7RG"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection id="OcC-FF-jGv">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="k4j-va-uaO" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="97" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="k4j-va-uaO" id="bQ8-mc-QAj">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="IhW-3B-PM7" customClass="VibrantButton" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="-0.5" width="374" height="44.5"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="44" id="qwy-Nb-VrG"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<state key="normal" title="Deactivate Extension">
|
||||
<color key="titleColor" systemColor="systemRedColor"/>
|
||||
</state>
|
||||
<state key="highlighted">
|
||||
<color key="titleColor" systemColor="systemRedColor"/>
|
||||
</state>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="color" keyPath="backgroundHighlightColor">
|
||||
<color key="value" name="deleteBackgroundColor"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="disable:" destination="1B7-3Y-VYf" eventType="touchUpInside" id="hVd-LH-FhC"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="IhW-3B-PM7" firstAttribute="centerY" secondItem="bQ8-mc-QAj" secondAttribute="centerY" id="DLY-b7-amc"/>
|
||||
<constraint firstItem="IhW-3B-PM7" firstAttribute="leading" secondItem="bQ8-mc-QAj" secondAttribute="leading" id="LlY-8t-XJf"/>
|
||||
<constraint firstAttribute="trailing" secondItem="IhW-3B-PM7" secondAttribute="trailing" id="Ua9-qY-YaN"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
</sections>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="1B7-3Y-VYf" id="Ro3-xz-VDK"/>
|
||||
<outlet property="delegate" destination="1B7-3Y-VYf" id="X3w-Zg-zKw"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" id="bBU-mK-vL1"/>
|
||||
<connections>
|
||||
<outlet property="extensionDescription" destination="YJL-5w-N2S" id="zam-r3-KIC"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="cc3-yd-zmS" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1549" y="-591"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<designables>
|
||||
<designable name="rOV-XS-bNW"/>
|
||||
</designables>
|
||||
<resources>
|
||||
<image name="safari" catalog="system" width="128" height="123"/>
|
||||
<image name="safari.fill" catalog="system" width="128" height="123"/>
|
||||
<namedColor name="deleteBackgroundColor">
|
||||
<color red="1" green="0.23100003600120544" blue="0.18799999356269836" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
<namedColor name="primaryAccentColor">
|
||||
<color red="0.031372549019607843" green="0.41568627450980394" blue="0.93333333333333335" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
<namedColor name="secondaryAccentColor">
|
||||
<color red="0.031372549019607843" green="0.41568627450980394" blue="0.93333333333333335" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
<systemColor name="systemRedColor">
|
||||
<color red="1" green="0.23137254901960785" blue="0.18823529411764706" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -1,34 +0,0 @@
|
||||
//
|
||||
// InspectorIconHeaderView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 11/6/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class InspectorIconHeaderView: UITableViewHeaderFooterView {
|
||||
|
||||
var iconView = IconView()
|
||||
|
||||
override init(reuseIdentifier: String?) {
|
||||
super.init(reuseIdentifier: reuseIdentifier)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
func commonInit() {
|
||||
addSubview(iconView)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
let x = (bounds.width - 48.0) / 2
|
||||
let y = (bounds.height - 48.0) / 2
|
||||
iconView.frame = CGRect(x: x, y: y, width: 48.0, height: 48.0)
|
||||
}
|
||||
}
|
||||
87
iOS/Inspector/WebFeedInspectorView.swift
Normal file
87
iOS/Inspector/WebFeedInspectorView.swift
Normal file
@@ -0,0 +1,87 @@
|
||||
//
|
||||
// WebFeedInspectorView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 15/12/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Account
|
||||
import SafariServices
|
||||
import UserNotifications
|
||||
|
||||
struct WebFeedInspectorView: View {
|
||||
|
||||
var webFeed: WebFeed!
|
||||
@State private var showHomePage: Bool = false
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
|
||||
Section(header: webFeedHeaderView) {}
|
||||
|
||||
Section {
|
||||
TextField(webFeed.nameForDisplay,
|
||||
text: Binding(
|
||||
get: { webFeed.name ?? webFeed.nameForDisplay },
|
||||
set: { webFeed.name = $0 }),
|
||||
prompt: nil)
|
||||
|
||||
Toggle(isOn: Binding(get: { webFeed.isNotifyAboutNewArticles ?? false }, set: { webFeed.isNotifyAboutNewArticles = $0 })) {
|
||||
Text("Notify About New Articles", comment: "Toggle denoting whether the user has enabled new article notifications for this feed.")
|
||||
}
|
||||
|
||||
if webFeed.isFeedProvider == false {
|
||||
Toggle(isOn: Binding(
|
||||
get: { webFeed.isArticleExtractorAlwaysOn ?? false },
|
||||
set: { webFeed.isArticleExtractorAlwaysOn = $0 })) {
|
||||
Text("Always Show Reader View", comment: "Toggle denoting whether the user has enabled Reader view for this feed.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Home Page", comment: "Home Page section header in the Feed inspector.")) {
|
||||
HStack {
|
||||
Text(webFeed.homePageURL?.decodedURLString ?? "")
|
||||
Spacer()
|
||||
Image(uiImage: AppAssets.safariImage)
|
||||
.renderingMode(.template)
|
||||
.foregroundColor(Color(uiColor: AppAssets.primaryAccentColor))
|
||||
}
|
||||
.onTapGesture {
|
||||
if webFeed.homePageURL != nil { showHomePage = true }
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Feed URL", comment: "Feed URL section header in the Feed inspector.")) {
|
||||
Text(webFeed.url.description)
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle(webFeed.nameForDisplay)
|
||||
.sheet(isPresented: $showHomePage, onDismiss: nil) {
|
||||
SafariView(url: URL(string: webFeed.homePageURL!)!)
|
||||
}
|
||||
.tint(Color(uiColor: AppAssets.primaryAccentColor))
|
||||
.dismissOnExternalContextLaunch()
|
||||
}
|
||||
|
||||
var webFeedHeaderView: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
Image(uiImage: webFeed.smallIcon!.image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 48, height: 48)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WebFeedInspectorView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
WebFeedInspectorView()
|
||||
}
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
//
|
||||
// WebFeedInspectorViewController.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 11/6/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Account
|
||||
import SafariServices
|
||||
import UserNotifications
|
||||
|
||||
class WebFeedInspectorViewController: UITableViewController {
|
||||
|
||||
static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 500.0)
|
||||
|
||||
var webFeed: WebFeed!
|
||||
@IBOutlet weak var nameTextField: UITextField!
|
||||
@IBOutlet weak var notifyAboutNewArticlesSwitch: UISwitch!
|
||||
@IBOutlet weak var alwaysShowReaderViewSwitch: UISwitch!
|
||||
@IBOutlet weak var homePageLabel: InteractiveLabel!
|
||||
@IBOutlet weak var feedURLLabel: InteractiveLabel!
|
||||
|
||||
private var headerView: InspectorIconHeaderView?
|
||||
private var iconImage: IconImage? {
|
||||
return IconImageCache.shared.imageForFeed(webFeed)
|
||||
}
|
||||
|
||||
private let homePageIndexPath = IndexPath(row: 0, section: 1)
|
||||
|
||||
private var shouldHideHomePageSection: Bool {
|
||||
return webFeed.homePageURL == nil
|
||||
}
|
||||
|
||||
private var userNotificationSettings: UNNotificationSettings?
|
||||
|
||||
override func viewDidLoad() {
|
||||
tableView.register(InspectorIconHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
|
||||
|
||||
navigationItem.title = webFeed.nameForDisplay
|
||||
nameTextField.text = webFeed.nameForDisplay
|
||||
|
||||
notifyAboutNewArticlesSwitch.setOn(webFeed.isNotifyAboutNewArticles ?? false, animated: false)
|
||||
|
||||
if webFeed.isFeedProvider {
|
||||
alwaysShowReaderViewSwitch.isOn = false
|
||||
alwaysShowReaderViewSwitch.isEnabled = false
|
||||
} else {
|
||||
alwaysShowReaderViewSwitch.setOn(webFeed.isArticleExtractorAlwaysOn ?? false, animated: false)
|
||||
}
|
||||
|
||||
|
||||
homePageLabel.text = webFeed.homePageURL?.decodedURLString
|
||||
feedURLLabel.text = webFeed.url.decodedURLString
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(webFeedIconDidBecomeAvailable(_:)), name: .WebFeedIconDidBecomeAvailable, object: nil)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(updateNotificationSettings), name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
updateNotificationSettings()
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
if nameTextField.text != webFeed.nameForDisplay {
|
||||
let nameText = nameTextField.text ?? ""
|
||||
let newName = nameText.isEmpty ? (webFeed.name ?? NSLocalizedString("Untitled", comment: "Feed name")) : nameText
|
||||
webFeed.rename(to: newName) { _ in }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Notifications
|
||||
@objc func webFeedIconDidBecomeAvailable(_ notification: Notification) {
|
||||
headerView?.iconView.iconImage = iconImage
|
||||
}
|
||||
|
||||
@IBAction func notifyAboutNewArticlesChanged(_ sender: Any) {
|
||||
guard let settings = userNotificationSettings else {
|
||||
notifyAboutNewArticlesSwitch.isOn = !notifyAboutNewArticlesSwitch.isOn
|
||||
return
|
||||
}
|
||||
if settings.authorizationStatus == .denied {
|
||||
notifyAboutNewArticlesSwitch.isOn = !notifyAboutNewArticlesSwitch.isOn
|
||||
present(notificationUpdateErrorAlert(), animated: true, completion: nil)
|
||||
} else if settings.authorizationStatus == .authorized {
|
||||
webFeed.isNotifyAboutNewArticles = notifyAboutNewArticlesSwitch.isOn
|
||||
} else {
|
||||
UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .sound, .alert]) { (granted, error) in
|
||||
self.updateNotificationSettings()
|
||||
if granted {
|
||||
DispatchQueue.main.async {
|
||||
self.webFeed.isNotifyAboutNewArticles = self.notifyAboutNewArticlesSwitch.isOn
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.notifyAboutNewArticlesSwitch.isOn = !self.notifyAboutNewArticlesSwitch.isOn
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func alwaysShowReaderViewChanged(_ sender: Any) {
|
||||
webFeed.isArticleExtractorAlwaysOn = alwaysShowReaderViewSwitch.isOn
|
||||
}
|
||||
|
||||
@IBAction func done(_ sender: Any) {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
/// Returns a new indexPath, taking into consideration any
|
||||
/// conditions that may require the tableView to be
|
||||
/// displayed differently than what is setup in the storyboard.
|
||||
private func shift(_ indexPath: IndexPath) -> IndexPath {
|
||||
return IndexPath(row: indexPath.row, section: shift(indexPath.section))
|
||||
}
|
||||
|
||||
/// Returns a new section, taking into consideration any
|
||||
/// conditions that may require the tableView to be
|
||||
/// displayed differently than what is setup in the storyboard.
|
||||
private func shift(_ section: Int) -> Int {
|
||||
if section >= homePageIndexPath.section && shouldHideHomePageSection {
|
||||
return section + 1
|
||||
}
|
||||
return section
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: Table View
|
||||
|
||||
extension WebFeedInspectorViewController {
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
let numberOfSections = super.numberOfSections(in: tableView)
|
||||
return shouldHideHomePageSection ? numberOfSections - 1 : numberOfSections
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return super.tableView(tableView, numberOfRowsInSection: shift(section))
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: shift(section))
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = super.tableView(tableView, cellForRowAt: shift(indexPath))
|
||||
if indexPath.section == 0 && indexPath.row == 1 {
|
||||
guard let label = cell.contentView.subviews.filter({ $0.isKind(of: UILabel.self) })[0] as? UILabel else {
|
||||
return cell
|
||||
}
|
||||
label.numberOfLines = 2
|
||||
label.text = webFeed.notificationDisplayName.capitalized
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
super.tableView(tableView, titleForHeaderInSection: shift(section))
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
if shift(section) == 0 {
|
||||
headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as? InspectorIconHeaderView
|
||||
headerView?.iconView.iconImage = iconImage
|
||||
return headerView
|
||||
} else {
|
||||
return super.tableView(tableView, viewForHeaderInSection: shift(section))
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
if shift(indexPath) == homePageIndexPath,
|
||||
let homePageUrlString = webFeed.homePageURL,
|
||||
let homePageUrl = URL(string: homePageUrlString) {
|
||||
|
||||
let safari = SFSafariViewController(url: homePageUrl)
|
||||
safari.modalPresentationStyle = .pageSheet
|
||||
present(safari, animated: true) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: UITextFieldDelegate
|
||||
|
||||
extension WebFeedInspectorViewController: UITextFieldDelegate {
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
textField.resignFirstResponder()
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: UNUserNotificationCenter
|
||||
|
||||
extension WebFeedInspectorViewController {
|
||||
|
||||
@objc
|
||||
func updateNotificationSettings() {
|
||||
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
|
||||
DispatchQueue.main.async {
|
||||
self.userNotificationSettings = settings
|
||||
if settings.authorizationStatus == .authorized {
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func notificationUpdateErrorAlert() -> UIAlertController {
|
||||
let alert = UIAlertController(title: NSLocalizedString("Enable Notifications", comment: "Notifications"),
|
||||
message: NSLocalizedString("Notifications need to be enabled in the Settings app.", comment: "Notifications need to be enabled in the Settings app."), preferredStyle: .alert)
|
||||
let openSettings = UIAlertAction(title: NSLocalizedString("Open Settings", comment: "Open Settings"), style: .default) { (action) in
|
||||
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly : false], completionHandler: nil)
|
||||
}
|
||||
let dismiss = UIAlertAction(title: NSLocalizedString("Dismiss", comment: "Dismiss"), style: .cancel, handler: nil)
|
||||
alert.addAction(openSettings)
|
||||
alert.addAction(dismiss)
|
||||
alert.preferredAction = openSettings
|
||||
return alert
|
||||
}
|
||||
|
||||
}
|
||||
6
iOS/Resources/Assets.xcassets/Settings/Contents.json
Normal file
6
iOS/Resources/Assets.xcassets/Settings/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
12
iOS/Resources/Assets.xcassets/Settings/app.account.imageset/Contents.json
vendored
Normal file
12
iOS/Resources/Assets.xcassets/Settings/app.account.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "app.account.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
iOS/Resources/Assets.xcassets/Settings/app.account.imageset/app.account.pdf
vendored
Normal file
BIN
iOS/Resources/Assets.xcassets/Settings/app.account.imageset/app.account.pdf
vendored
Normal file
Binary file not shown.
22
iOS/Resources/Assets.xcassets/Settings/app.appearance.automatic.imageset/Contents.json
vendored
Normal file
22
iOS/Resources/Assets.xcassets/Settings/app.appearance.automatic.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "app.appearance.automatic.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "app.appearance.automatic 1.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
22
iOS/Resources/Assets.xcassets/Settings/app.appearance.dark.imageset/Contents.json
vendored
Normal file
22
iOS/Resources/Assets.xcassets/Settings/app.appearance.dark.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "app.appearance.dark.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "app.appearance.dark 1.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
iOS/Resources/Assets.xcassets/Settings/app.appearance.dark.imageset/app.appearance.dark 1.png
vendored
Normal file
BIN
iOS/Resources/Assets.xcassets/Settings/app.appearance.dark.imageset/app.appearance.dark 1.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
iOS/Resources/Assets.xcassets/Settings/app.appearance.dark.imageset/app.appearance.dark.png
vendored
Normal file
BIN
iOS/Resources/Assets.xcassets/Settings/app.appearance.dark.imageset/app.appearance.dark.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
12
iOS/Resources/Assets.xcassets/Settings/app.appearance.imageset/Contents.json
vendored
Normal file
12
iOS/Resources/Assets.xcassets/Settings/app.appearance.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "app.appearance.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
iOS/Resources/Assets.xcassets/Settings/app.appearance.imageset/app.appearance.pdf
vendored
Normal file
BIN
iOS/Resources/Assets.xcassets/Settings/app.appearance.imageset/app.appearance.pdf
vendored
Normal file
Binary file not shown.
22
iOS/Resources/Assets.xcassets/Settings/app.appearance.light.imageset/Contents.json
vendored
Normal file
22
iOS/Resources/Assets.xcassets/Settings/app.appearance.light.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "app.appearance.light.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "app.appearance.light 1.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
iOS/Resources/Assets.xcassets/Settings/app.appearance.light.imageset/app.appearance.light 1.png
vendored
Normal file
BIN
iOS/Resources/Assets.xcassets/Settings/app.appearance.light.imageset/app.appearance.light 1.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
iOS/Resources/Assets.xcassets/Settings/app.appearance.light.imageset/app.appearance.light.png
vendored
Normal file
BIN
iOS/Resources/Assets.xcassets/Settings/app.appearance.light.imageset/app.appearance.light.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
12
iOS/Resources/Assets.xcassets/Settings/app.export.opml.imageset/Contents.json
vendored
Normal file
12
iOS/Resources/Assets.xcassets/Settings/app.export.opml.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "app.export.opml.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
iOS/Resources/Assets.xcassets/Settings/app.export.opml.imageset/app.export.opml.pdf
vendored
Normal file
BIN
iOS/Resources/Assets.xcassets/Settings/app.export.opml.imageset/app.export.opml.pdf
vendored
Normal file
Binary file not shown.
12
iOS/Resources/Assets.xcassets/Settings/app.extension.imageset/Contents.json
vendored
Normal file
12
iOS/Resources/Assets.xcassets/Settings/app.extension.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "app.extension.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
iOS/Resources/Assets.xcassets/Settings/app.extension.imageset/app.extension.pdf
vendored
Normal file
BIN
iOS/Resources/Assets.xcassets/Settings/app.extension.imageset/app.extension.pdf
vendored
Normal file
Binary file not shown.
12
iOS/Resources/Assets.xcassets/Settings/app.import.opml.imageset/Contents.json
vendored
Normal file
12
iOS/Resources/Assets.xcassets/Settings/app.import.opml.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "app.import.opml.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
iOS/Resources/Assets.xcassets/Settings/app.import.opml.imageset/app.import.opml.pdf
vendored
Normal file
BIN
iOS/Resources/Assets.xcassets/Settings/app.import.opml.imageset/app.import.opml.pdf
vendored
Normal file
Binary file not shown.
12
iOS/Resources/Assets.xcassets/Settings/notifications.sounds.imageset/Contents.json
vendored
Normal file
12
iOS/Resources/Assets.xcassets/Settings/notifications.sounds.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "notifications.sounds.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
iOS/Resources/Assets.xcassets/Settings/notifications.sounds.imageset/notifications.sounds.pdf
vendored
Normal file
BIN
iOS/Resources/Assets.xcassets/Settings/notifications.sounds.imageset/notifications.sounds.pdf
vendored
Normal file
Binary file not shown.
12
iOS/Resources/Assets.xcassets/Settings/system.settings.imageset/Contents.json
vendored
Normal file
12
iOS/Resources/Assets.xcassets/Settings/system.settings.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "system.settings.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
iOS/Resources/Assets.xcassets/Settings/system.settings.imageset/system.settings.pdf
vendored
Normal file
BIN
iOS/Resources/Assets.xcassets/Settings/system.settings.imageset/system.settings.pdf
vendored
Normal file
Binary file not shown.
@@ -1,16 +1,6 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "feedbin-logo-filled-1.pdf",
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "feedbin-logo-filled.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -13,6 +13,7 @@ import Articles
|
||||
import RSCore
|
||||
import RSTree
|
||||
import SafariServices
|
||||
import SwiftUI
|
||||
|
||||
protocol MainControllerIdentifiable {
|
||||
var mainControllerIdentifier: MainControllerIdentifier { get }
|
||||
@@ -1172,23 +1173,14 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner, Logging {
|
||||
}
|
||||
|
||||
func showSettings(scrollToArticlesSection: Bool = false) {
|
||||
let settingsNavController = UIStoryboard.settings.instantiateInitialViewController() as! UINavigationController
|
||||
let settingsViewController = settingsNavController.topViewController as! SettingsViewController
|
||||
settingsViewController.scrollToArticlesSection = scrollToArticlesSection
|
||||
settingsNavController.modalPresentationStyle = .formSheet
|
||||
settingsViewController.presentingParentController = rootSplitViewController
|
||||
rootSplitViewController.present(settingsNavController, animated: true)
|
||||
var s = scrollToArticlesSection
|
||||
let hostedSettings = UIHostingController(rootView: SettingsView(isConfigureAppearanceShown: Binding(get: { s }, set: { s = $0 })))
|
||||
rootSplitViewController.present(hostedSettings, animated: true)
|
||||
}
|
||||
|
||||
func showAccountInspector(for account: Account) {
|
||||
let accountInspectorNavController =
|
||||
UIStoryboard.inspector.instantiateViewController(identifier: "AccountInspectorNavigationViewController") as! UINavigationController
|
||||
let accountInspectorController = accountInspectorNavController.topViewController as! AccountInspectorViewController
|
||||
accountInspectorNavController.modalPresentationStyle = .formSheet
|
||||
accountInspectorNavController.preferredContentSize = AccountInspectorViewController.preferredContentSizeForFormSheetDisplay
|
||||
accountInspectorController.isModal = true
|
||||
accountInspectorController.account = account
|
||||
rootSplitViewController.present(accountInspectorNavController, animated: true)
|
||||
let hosting = UIHostingController(rootView: InjectedNavigationView(injectedView: AccountInspectorView(account: account)))
|
||||
rootSplitViewController.present(hosting, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func showFeedInspector() {
|
||||
@@ -1201,13 +1193,10 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner, Logging {
|
||||
}
|
||||
|
||||
func showFeedInspector(for feed: WebFeed) {
|
||||
let feedInspectorNavController =
|
||||
UIStoryboard.inspector.instantiateViewController(identifier: "FeedInspectorNavigationViewController") as! UINavigationController
|
||||
let feedInspectorController = feedInspectorNavController.topViewController as! WebFeedInspectorViewController
|
||||
feedInspectorNavController.modalPresentationStyle = .formSheet
|
||||
feedInspectorNavController.preferredContentSize = WebFeedInspectorViewController.preferredContentSizeForFormSheetDisplay
|
||||
feedInspectorController.webFeed = feed
|
||||
rootSplitViewController.present(feedInspectorNavController, animated: true)
|
||||
|
||||
let hosting = UIHostingController(rootView: InjectedNavigationView(injectedView: WebFeedInspectorView(webFeed: feed)))
|
||||
|
||||
rootSplitViewController.present(hosting, animated: true)
|
||||
}
|
||||
|
||||
func showAddWebFeed(initialFeed: String? = nil, initialFeedName: String? = nil) {
|
||||
@@ -1333,8 +1322,11 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner, Logging {
|
||||
if presentedController.isKind(of: SFSafariViewController.self) {
|
||||
presentedController.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
guard let settings = presentedController.children.first as? SettingsViewController else { return }
|
||||
settings.dismiss(animated: true, completion: nil)
|
||||
|
||||
// There's no obvious way to detect if the presented controller
|
||||
// is the SwiftUI UIHostingController<SettingsView>. Posting a notification
|
||||
// which it can react to seems to be the simplest solution.
|
||||
NotificationCenter.default.post(name: .LaunchedFromExternalAction, object: nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -202,7 +202,7 @@ import RSCore
|
||||
}
|
||||
task.resume()
|
||||
} else {
|
||||
print("No theme URL")
|
||||
self.logger.debug("No theme URL.")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
//
|
||||
// AccountsManagementView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 13/11/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Account
|
||||
import Combine
|
||||
|
||||
public final class AccountManagementViewModel: ObservableObject {
|
||||
|
||||
@Published var sortedActiveAccounts = [Account]()
|
||||
@Published var sortedInactiveAccounts = [Account]()
|
||||
@Published var accountsForDeletion = [Account]()
|
||||
@Published var showAccountDeletionAlert: Bool = false
|
||||
@Published var showAddAccountSheet: Bool = false
|
||||
public var accountToDelete: Account? = nil
|
||||
|
||||
init() {
|
||||
refreshAccounts()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(refreshAccounts(_:)), name: .AccountStateDidChange, object: nil)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(refreshAccounts(_:)), name: .UserDidAddAccount, object: nil)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(refreshAccounts(_:)), name: .UserDidDeleteAccount, object: nil)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(refreshAccounts(_:)), name: .DisplayNameDidChange, object: nil)
|
||||
}
|
||||
|
||||
func temporarilyDeleteAccount(_ account: Account) {
|
||||
if account.isActive {
|
||||
sortedActiveAccounts.removeAll(where: { $0.accountID == account.accountID })
|
||||
} else {
|
||||
sortedInactiveAccounts.removeAll(where: { $0.accountID == account.accountID })
|
||||
}
|
||||
accountToDelete = account
|
||||
showAccountDeletionAlert = true
|
||||
}
|
||||
|
||||
func restoreAccount(_ account: Account) {
|
||||
accountToDelete = nil
|
||||
self.refreshAccounts()
|
||||
}
|
||||
|
||||
@objc
|
||||
private func refreshAccounts(_ sender: Any? = nil) {
|
||||
sortedActiveAccounts = AccountManager.shared.sortedActiveAccounts
|
||||
sortedInactiveAccounts = AccountManager.shared.sortedAccounts.filter({ $0.isActive == false })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
struct AccountsManagementView: View {
|
||||
|
||||
@StateObject private var viewModel = AccountManagementViewModel()
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section(header: Text("Active Accounts", comment: "Active accounts section header")) {
|
||||
ForEach(viewModel.sortedActiveAccounts, id: \.self) { account in
|
||||
accountRow(account)
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Inactive Accounts", comment: "Inactive accounts section header")) {
|
||||
ForEach(viewModel.sortedInactiveAccounts, id: \.self) { account in
|
||||
accountRow(account)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(Text("Manage Accounts", comment: "Navigation title: Manage Accounts"))
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
viewModel.showAddAccountSheet = true
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $viewModel.showAddAccountSheet) {
|
||||
AddAccountListView()
|
||||
}
|
||||
.alert(Text("Are you sure you want to remove “\(viewModel.accountToDelete?.nameForDisplay ?? "")”?", comment: "Alert title: confirm account removal"),
|
||||
isPresented: $viewModel.showAccountDeletionAlert) {
|
||||
Button(role: .destructive) {
|
||||
AccountManager.shared.deleteAccount(viewModel.accountToDelete!)
|
||||
} label: {
|
||||
Text("Remove Account", comment: "Button title")
|
||||
}
|
||||
|
||||
Button(role: .cancel) {
|
||||
viewModel.restoreAccount(viewModel.accountToDelete!)
|
||||
} label: {
|
||||
Text("Cancel", comment: "Button title")
|
||||
}
|
||||
} message: {
|
||||
Text("This action cannot be undone.", comment: "Alert message: remove account confirmation")
|
||||
}
|
||||
}
|
||||
|
||||
func accountRow(_ account: Account) -> some View {
|
||||
NavigationLink {
|
||||
AccountInspectorView(account: account)
|
||||
} label: {
|
||||
Image(uiImage: account.smallIcon!.image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 25, height: 25)
|
||||
Text(account.nameForDisplay)
|
||||
}
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||
if account != AccountManager.shared.defaultAccount {
|
||||
Button(role: .destructive) {
|
||||
viewModel.temporarilyDeleteAccount(account)
|
||||
} label: {
|
||||
Label {
|
||||
Text("Remove Account", comment: "Button title")
|
||||
} icon: {
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
Button {
|
||||
withAnimation {
|
||||
account.isActive.toggle()
|
||||
}
|
||||
} label: {
|
||||
if account.isActive {
|
||||
Image(systemName: "minus.circle")
|
||||
} else {
|
||||
Image(systemName: "togglepower")
|
||||
}
|
||||
}.tint(account.isActive ? .yellow : Color(uiColor: AppAssets.primaryAccentColor))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AddAccountView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AccountsManagementView()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
//
|
||||
// AddAccountListView.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 AddAccountListViewModel: ObservableObject, OAuthAccountAuthorizationOperationDelegate {
|
||||
|
||||
@Published public var showAddAccountSheet: (Bool, accountType: AccountType) = (false, .onMyMac)
|
||||
@Published public var showAddAccountError: (Error?, Bool) = (nil, false)
|
||||
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) {
|
||||
showAddAccountError = (error, true)
|
||||
}
|
||||
}
|
||||
|
||||
struct AddAccountListView: View {
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@StateObject private var viewModel = AddAccountListViewModel()
|
||||
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
localAccountSection
|
||||
cloudKitSection
|
||||
webAccountSection
|
||||
selfHostedSection
|
||||
}
|
||||
.navigationTitle(Text("Add Account", comment: "Navigation title: Add Account"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.listItemTint(.primary)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button(role: .cancel) {
|
||||
dismiss()
|
||||
} label: {
|
||||
Text("Cancel", comment: "Button title")
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $viewModel.showAddAccountSheet.0) {
|
||||
switch viewModel.showAddAccountSheet.accountType {
|
||||
case .onMyMac:
|
||||
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())
|
||||
}
|
||||
}
|
||||
.alert(Text("Error", comment: "Alert title: Error"),
|
||||
isPresented: $viewModel.showAddAccountError.1,
|
||||
actions: { },
|
||||
message: {
|
||||
Text("\(viewModel.showAddAccountError.0?.localizedDescription ?? "Unknown Error")")
|
||||
})
|
||||
.dismissOnAccountAdd()
|
||||
}
|
||||
}
|
||||
|
||||
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("Local", comment: "Add Account: Local account section header")
|
||||
} footer: {
|
||||
Text("Local accounts do not sync your feeds across devices", comment: "Local account section footer")
|
||||
}
|
||||
}
|
||||
|
||||
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("iCloud", comment: "Add Account: iCloud section header")
|
||||
} footer: {
|
||||
Text("Your iCloud account syncs your feeds across your Mac and iOS devices", comment: "Add Account: iCloud section footer")
|
||||
}
|
||||
}
|
||||
|
||||
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("Web Account", comment: "Add Account: Web Account section header")
|
||||
} footer: {
|
||||
Text("Web accounts sync your feeds across all your devices", comment: "Add Account: Web Account section footer")
|
||||
}
|
||||
}
|
||||
|
||||
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("Self-Hosted", comment: "Add Accont: Self-hosted section header")
|
||||
} footer: {
|
||||
Text("Self-hosted accounts sync your feeds across all your devices", comment: "Add Account: Self-hosted section footer")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// AddExtensionListView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 13/12/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Accounts
|
||||
|
||||
struct AddExtensionListView: View {
|
||||
|
||||
@State private var availableExtensionPointTypes = ExtensionPointManager.shared.availableExtensionPointTypes.sorted(by: { $0.title < $1.title })
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@State private var showExtensionPointView: (ExtensionPoint.Type?, Bool) = (nil, false)
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
Section(header: Text("Feed Providers", comment: "Feed Providers section header"),
|
||||
footer: Text("Feed Providers allow you to subscribe to some pages as if they were RSS feeds.", comment: "Feed Providers section footer.")) {
|
||||
ForEach(0..<availableExtensionPointTypes.count, id: \.self) { i in
|
||||
Button {
|
||||
showExtensionPointView = (availableExtensionPointTypes[i], true)
|
||||
} label: {
|
||||
Image(uiImage: availableExtensionPointTypes[i].image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 25, height: 25)
|
||||
|
||||
Text("\(availableExtensionPointTypes[i].title)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle(Text("Add Extensions", comment: "Navigation title: Add Extensions"))
|
||||
.sheet(isPresented: $showExtensionPointView.1, content: {
|
||||
if showExtensionPointView.0 != nil {
|
||||
EnableExtensionPointView(extensionPoint: showExtensionPointView.0!)
|
||||
}
|
||||
})
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button(role: .cancel) {
|
||||
dismiss()
|
||||
} label: {
|
||||
Text("Cancel", comment: "Button title")
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .ActiveExtensionPointsDidChange), perform: { _ in
|
||||
dismiss()
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
struct AddExtensionListView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AddExtensionListView()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// EnableExtensionPointView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 19/12/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EnableExtensionPointView: View {
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@StateObject private var viewModel = EnableExtensionViewModel()
|
||||
@State private var extensionError: (Error?, Bool) = (nil, false)
|
||||
var extensionPoint: ExtensionPoint.Type
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
ExtensionSectionHeader(extensionPoint: extensionPoint)
|
||||
Section(footer: extensionExplainer) {}
|
||||
Section { enableButton }
|
||||
}
|
||||
.alert(Text("Error", comment: "Alert title: Error"), isPresented: $extensionError.1, actions: {
|
||||
}, message: {
|
||||
Text(extensionError.0?.localizedDescription ?? "Unknown Error")
|
||||
})
|
||||
.alert(Text("Error", comment: "Alert title: Error"), isPresented: $viewModel.showExtensionError.1, actions: {
|
||||
}, message: {
|
||||
Text(viewModel.showExtensionError.0?.localizedDescription ?? "Unknown Error")
|
||||
})
|
||||
.navigationTitle(extensionPoint.title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.dismissOnExternalContextLaunch()
|
||||
.onReceive(NotificationCenter.default.publisher(for: .ActiveExtensionPointsDidChange), perform: { _ in
|
||||
dismiss()
|
||||
})
|
||||
.edgesIgnoringSafeArea(.bottom)
|
||||
}
|
||||
|
||||
var extensionExplainer: some View {
|
||||
Text(extensionPoint.description.string)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
var enableButton: some View {
|
||||
Button {
|
||||
Task {
|
||||
viewModel.configure(extensionPoint)
|
||||
do {
|
||||
try await viewModel.enableExtension()
|
||||
} catch {
|
||||
extensionError = (error, true)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("Enable Extension", comment: "Button title")
|
||||
Spacer()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
//
|
||||
// EnableExtensionViewModel.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 19/12/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AuthenticationServices
|
||||
import Account
|
||||
import OAuthSwift
|
||||
import Secrets
|
||||
import RSCore
|
||||
|
||||
|
||||
public final class EnableExtensionViewModel: NSObject, ObservableObject, OAuthSwiftURLHandlerType, ASWebAuthenticationPresentationContextProviding, Logging {
|
||||
|
||||
@Published public var showExtensionError: (Error?, Bool) = (nil, false)
|
||||
private var extensionPointType: ExtensionPoint.Type?
|
||||
private var oauth: OAuthSwift?
|
||||
private var callbackURL: URL? = nil
|
||||
|
||||
|
||||
func configure(_ extensionPointType: ExtensionPoint.Type) {
|
||||
self.extensionPointType = extensionPointType
|
||||
}
|
||||
|
||||
func enableExtension() async throws {
|
||||
guard let extensionPointType = extensionPointType else { return }
|
||||
if let oauth1 = extensionPointType as? OAuth1SwiftProvider.Type {
|
||||
try await enableOAuth1(oauth1)
|
||||
} else if let oauth2 = extensionPointType as? OAuth2SwiftProvider.Type {
|
||||
try await enableOAuth2(oauth2)
|
||||
} else {
|
||||
try await activateExtensionPoint(extensionPointType)
|
||||
}
|
||||
}
|
||||
|
||||
private func activateExtensionPoint(_ point: ExtensionPoint.Type) async throws {
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
ExtensionPointManager.shared.activateExtensionPoint(point) { result in
|
||||
switch result {
|
||||
case .success(_):
|
||||
continuation.resume()
|
||||
return
|
||||
case .failure(let failure):
|
||||
continuation.resume(throwing: failure)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Enable OAuth
|
||||
private func enableOAuth1(_ provider: OAuth1SwiftProvider.Type) async throws {
|
||||
callbackURL = provider.callbackURL
|
||||
|
||||
let oauth1 = provider.oauth1Swift
|
||||
self.oauth = oauth1
|
||||
oauth1.authorizeURLHandler = self
|
||||
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
oauth1.authorize(withCallbackURL: callbackURL!) { [weak self] result in
|
||||
|
||||
guard let self = self, let extensionPointType = self.extensionPointType else { return }
|
||||
|
||||
switch result {
|
||||
case .success(let tokenSuccess):
|
||||
ExtensionPointManager.shared.activateExtensionPoint(extensionPointType, tokenSuccess: tokenSuccess) { result in
|
||||
switch result {
|
||||
case .success(_):
|
||||
continuation.resume()
|
||||
return
|
||||
case .failure(let failure):
|
||||
continuation.resume(throwing: failure)
|
||||
return
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
return
|
||||
}
|
||||
|
||||
self.oauth?.cancel()
|
||||
self.oauth = nil
|
||||
}
|
||||
continuation.resume()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func enableOAuth2(_ provider: OAuth2SwiftProvider.Type) async throws {
|
||||
|
||||
callbackURL = provider.callbackURL
|
||||
|
||||
let oauth2 = provider.oauth2Swift
|
||||
self.oauth = oauth2
|
||||
oauth2.authorizeURLHandler = self
|
||||
|
||||
let oauth2Vars = provider.oauth2Vars
|
||||
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
oauth2.authorize(withCallbackURL: callbackURL!, scope: oauth2Vars.scope, state: oauth2Vars.state, parameters: oauth2Vars.params) { [weak self] result in
|
||||
guard let self = self, let extensionPointType = self.extensionPointType else { return }
|
||||
|
||||
switch result {
|
||||
case .success(let tokenSuccess):
|
||||
ExtensionPointManager.shared.activateExtensionPoint(extensionPointType, tokenSuccess: tokenSuccess) { [weak self] result in
|
||||
switch result {
|
||||
case .success(_):
|
||||
self?.logger.debug("Enabled extension successfully.")
|
||||
case .failure(let failure):
|
||||
continuation.resume(throwing: failure)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
case .failure(let oauthSwiftError):
|
||||
continuation.resume(throwing: oauthSwiftError)
|
||||
return
|
||||
}
|
||||
|
||||
self.oauth?.cancel()
|
||||
self.oauth = nil
|
||||
}
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
|
||||
public func handle(_ url: URL) {
|
||||
let session = ASWebAuthenticationSession(url: url, callbackURLScheme: callbackURL!.scheme, completionHandler: { (url, error) in
|
||||
if let callbackedURL = url {
|
||||
OAuth1Swift.handle(url: callbackedURL)
|
||||
}
|
||||
|
||||
guard let error = error else { return }
|
||||
|
||||
self.oauth?.cancel()
|
||||
self.oauth = nil
|
||||
|
||||
DispatchQueue.main.async {
|
||||
//self.dismiss(animated: true, completion: nil)
|
||||
//self.delegate?.dismiss()
|
||||
}
|
||||
|
||||
if case ASWebAuthenticationSessionError.canceledLogin = error {
|
||||
self.logger.debug("Login cancelled.")
|
||||
} else {
|
||||
self.showExtensionError = (error, true)
|
||||
}
|
||||
})
|
||||
|
||||
session.presentationContextProvider = self
|
||||
if !session.start() {
|
||||
logger.debug("Session failed to start!!!")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
|
||||
return rootViewController!.view.window!
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
//
|
||||
// ExtensionsManagementView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 30/11/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Account
|
||||
|
||||
struct ExtensionsManagementView: View {
|
||||
|
||||
@State private var availableExtensionPointTypes = ExtensionPointManager.shared.availableExtensionPointTypes.sorted(by: { $0.title < $1.title })
|
||||
@State private var showAddExtensionView: Bool = false
|
||||
@State private var showDeactivateAlert: Bool = false
|
||||
@State private var extensionToDeactivate: Dictionary<ExtensionPointIdentifer, any ExtensionPoint>.Element? = nil
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
activeExtensionsSection
|
||||
}
|
||||
.navigationTitle(Text("Manage Extensions", comment: "Navigation title: Manage Extensions"))
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
showAddExtensionView = true
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showAddExtensionView) {
|
||||
AddExtensionListView()
|
||||
}
|
||||
.alert(Text("Are you sure you want to deactivate “\(extensionToDeactivate?.value.title ?? "")?", comment: "Alert title: confirm deactivate extension"),
|
||||
isPresented: $showDeactivateAlert) {
|
||||
|
||||
Button(role: .destructive) {
|
||||
ExtensionPointManager.shared.deactivateExtensionPoint(extensionToDeactivate!.value.extensionPointID)
|
||||
} label: {
|
||||
Text("Deactivate Extension", comment: "Button: deactivate extension.")
|
||||
}
|
||||
|
||||
Button(role: .cancel) {
|
||||
extensionToDeactivate = nil
|
||||
} label: {
|
||||
Text("Cancel", comment: "Button title")
|
||||
}
|
||||
|
||||
} message: {
|
||||
Text("This action cannot be undone.", comment: "Alert message: confirmation that deactivation of extension cannot be undone.")
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .ActiveExtensionPointsDidChange), perform: { _ in
|
||||
availableExtensionPointTypes = ExtensionPointManager.shared.availableExtensionPointTypes.sorted(by: { $0.title < $1.title })
|
||||
})
|
||||
}
|
||||
|
||||
private var activeExtensionsSection: some View {
|
||||
Section(header: Text("Active Extensions", comment: "Active Extensions section header")) {
|
||||
ForEach(0..<ExtensionPointManager.shared.activeExtensionPoints.count, id: \.self) { i in
|
||||
let point = Array(ExtensionPointManager.shared.activeExtensionPoints)[i]
|
||||
NavigationLink {
|
||||
ExtensionInspectorView(extensionPoint: point.value)
|
||||
} label: {
|
||||
Image(uiImage: point.value.image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 25, height: 25)
|
||||
Text(point.value.title)
|
||||
}.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||
Button(role: .destructive) {
|
||||
extensionToDeactivate = point
|
||||
showDeactivateAlert = true
|
||||
} label: {
|
||||
Text("Deactivate", comment: "Button: deactivates extension")
|
||||
Image(systemName: "minus.circle")
|
||||
}.tint(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct ExtensionsManagementView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ExtensionsManagementView()
|
||||
}
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
//
|
||||
// AddAccountViewController.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 5/16/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Account
|
||||
import UIKit
|
||||
import RSCore
|
||||
|
||||
protocol AddAccountDismissDelegate: UIViewController {
|
||||
func dismiss()
|
||||
}
|
||||
|
||||
class AddAccountViewController: UITableViewController, AddAccountDismissDelegate {
|
||||
|
||||
private enum AddAccountSections: Int, CaseIterable {
|
||||
case local = 0
|
||||
case icloud
|
||||
case web
|
||||
case selfhosted
|
||||
|
||||
var sectionHeader: String {
|
||||
switch self {
|
||||
case .local:
|
||||
return NSLocalizedString("Local", comment: "Local Account")
|
||||
case .icloud:
|
||||
return NSLocalizedString("iCloud", comment: "iCloud Account")
|
||||
case .web:
|
||||
return NSLocalizedString("Web", comment: "Web Account")
|
||||
case .selfhosted:
|
||||
return NSLocalizedString("Self-hosted", comment: "Self hosted Account")
|
||||
}
|
||||
}
|
||||
|
||||
var sectionFooter: String {
|
||||
switch self {
|
||||
case .local:
|
||||
return NSLocalizedString("Local accounts do not sync your feeds across devices", comment: "Local Account")
|
||||
case .icloud:
|
||||
return NSLocalizedString("Your iCloud account syncs your feeds across your Mac and iOS devices", comment: "iCloud Account")
|
||||
case .web:
|
||||
return NSLocalizedString("Web accounts sync your feeds across all your devices", comment: "Web Account")
|
||||
case .selfhosted:
|
||||
return NSLocalizedString("Self-hosted accounts sync your feeds across all your devices", comment: "Self hosted Account")
|
||||
}
|
||||
}
|
||||
|
||||
var sectionContent: [AccountType] {
|
||||
switch self {
|
||||
case .local:
|
||||
return [.onMyMac]
|
||||
case .icloud:
|
||||
return [.cloudKit]
|
||||
case .web:
|
||||
#if DEBUG
|
||||
return [.bazQux, .feedbin, .feedly, .inoreader, .newsBlur, .theOldReader]
|
||||
#else
|
||||
return [.bazQux, .feedbin, .feedly, .inoreader, .newsBlur, .theOldReader]
|
||||
#endif
|
||||
case .selfhosted:
|
||||
return [.freshRSS]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
}
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return AddAccountSections.allCases.count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
if section == AddAccountSections.local.rawValue {
|
||||
return AddAccountSections.local.sectionContent.count
|
||||
}
|
||||
|
||||
if section == AddAccountSections.icloud.rawValue {
|
||||
return AddAccountSections.icloud.sectionContent.count
|
||||
}
|
||||
|
||||
if section == AddAccountSections.web.rawValue {
|
||||
return AddAccountSections.web.sectionContent.count
|
||||
}
|
||||
|
||||
if section == AddAccountSections.selfhosted.rawValue {
|
||||
return AddAccountSections.selfhosted.sectionContent.count
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
switch section {
|
||||
case AddAccountSections.local.rawValue:
|
||||
return AddAccountSections.local.sectionHeader
|
||||
case AddAccountSections.icloud.rawValue:
|
||||
return AddAccountSections.icloud.sectionHeader
|
||||
case AddAccountSections.web.rawValue:
|
||||
return AddAccountSections.web.sectionHeader
|
||||
case AddAccountSections.selfhosted.rawValue:
|
||||
return AddAccountSections.selfhosted.sectionHeader
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
|
||||
switch section {
|
||||
case AddAccountSections.local.rawValue:
|
||||
return AddAccountSections.local.sectionFooter
|
||||
case AddAccountSections.icloud.rawValue:
|
||||
return AddAccountSections.icloud.sectionFooter
|
||||
case AddAccountSections.web.rawValue:
|
||||
return AddAccountSections.web.sectionFooter
|
||||
case AddAccountSections.selfhosted.rawValue:
|
||||
return AddAccountSections.selfhosted.sectionFooter
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "SettingsAccountTableViewCell", for: indexPath) as! SettingsComboTableViewCell
|
||||
|
||||
switch indexPath.section {
|
||||
case AddAccountSections.local.rawValue:
|
||||
cell.comboNameLabel?.text = AddAccountSections.local.sectionContent[indexPath.row].localizedAccountName()
|
||||
cell.comboImage?.image = AppAssets.image(for: .onMyMac)
|
||||
case AddAccountSections.icloud.rawValue:
|
||||
cell.comboNameLabel?.text = AddAccountSections.icloud.sectionContent[indexPath.row].localizedAccountName()
|
||||
cell.comboImage?.image = AppAssets.image(for: AddAccountSections.icloud.sectionContent[indexPath.row])
|
||||
if AppDefaults.shared.isDeveloperBuild || AccountManager.shared.accounts.contains(where: { $0.type == .cloudKit }) {
|
||||
cell.isUserInteractionEnabled = false
|
||||
cell.comboNameLabel?.isEnabled = false
|
||||
}
|
||||
case AddAccountSections.web.rawValue:
|
||||
cell.comboNameLabel?.text = AddAccountSections.web.sectionContent[indexPath.row].localizedAccountName()
|
||||
cell.comboImage?.image = AppAssets.image(for: AddAccountSections.web.sectionContent[indexPath.row])
|
||||
let type = AddAccountSections.web.sectionContent[indexPath.row]
|
||||
if (type == .feedly || type == .inoreader) && AppDefaults.shared.isDeveloperBuild {
|
||||
cell.isUserInteractionEnabled = false
|
||||
cell.comboNameLabel?.isEnabled = false
|
||||
}
|
||||
case AddAccountSections.selfhosted.rawValue:
|
||||
cell.comboNameLabel?.text = AddAccountSections.selfhosted.sectionContent[indexPath.row].localizedAccountName()
|
||||
cell.comboImage?.image = AppAssets.image(for: AddAccountSections.selfhosted.sectionContent[indexPath.row])
|
||||
|
||||
default:
|
||||
return cell
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
|
||||
switch indexPath.section {
|
||||
case AddAccountSections.local.rawValue:
|
||||
let type = AddAccountSections.local.sectionContent[indexPath.row]
|
||||
presentController(for: type)
|
||||
case AddAccountSections.icloud.rawValue:
|
||||
let type = AddAccountSections.icloud.sectionContent[indexPath.row]
|
||||
presentController(for: type)
|
||||
case AddAccountSections.web.rawValue:
|
||||
let type = AddAccountSections.web.sectionContent[indexPath.row]
|
||||
presentController(for: type)
|
||||
case AddAccountSections.selfhosted.rawValue:
|
||||
let type = AddAccountSections.selfhosted.sectionContent[indexPath.row]
|
||||
presentController(for: type)
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private func presentController(for accountType: AccountType) {
|
||||
switch accountType {
|
||||
case .onMyMac:
|
||||
let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "LocalAccountNavigationViewController") as! UINavigationController
|
||||
navController.modalPresentationStyle = .currentContext
|
||||
let addViewController = navController.topViewController as! LocalAccountViewController
|
||||
addViewController.delegate = self
|
||||
present(navController, animated: true)
|
||||
case .cloudKit:
|
||||
let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "CloudKitAccountNavigationViewController") as! UINavigationController
|
||||
navController.modalPresentationStyle = .currentContext
|
||||
let addViewController = navController.topViewController as! CloudKitAccountViewController
|
||||
addViewController.delegate = self
|
||||
present(navController, animated: true)
|
||||
case .feedbin:
|
||||
let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "FeedbinAccountNavigationViewController") as! UINavigationController
|
||||
navController.modalPresentationStyle = .currentContext
|
||||
let addViewController = navController.topViewController as! FeedbinAccountViewController
|
||||
addViewController.delegate = self
|
||||
present(navController, animated: true)
|
||||
case .feedly:
|
||||
let addAccount = OAuthAccountAuthorizationOperation(accountType: .feedly)
|
||||
addAccount.delegate = self
|
||||
addAccount.presentationAnchor = self.view.window!
|
||||
MainThreadOperationQueue.shared.add(addAccount)
|
||||
case .newsBlur:
|
||||
let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "NewsBlurAccountNavigationViewController") as! UINavigationController
|
||||
navController.modalPresentationStyle = .currentContext
|
||||
let addViewController = navController.topViewController as! NewsBlurAccountViewController
|
||||
addViewController.delegate = self
|
||||
present(navController, animated: true)
|
||||
case .bazQux, .inoreader, .freshRSS, .theOldReader:
|
||||
let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "ReaderAPIAccountNavigationViewController") as! UINavigationController
|
||||
navController.modalPresentationStyle = .currentContext
|
||||
let addViewController = navController.topViewController as! ReaderAPIAccountViewController
|
||||
addViewController.accountType = accountType
|
||||
addViewController.delegate = self
|
||||
present(navController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
navigationController?.popViewController(animated: false)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AddAccountViewController: OAuthAccountAuthorizationOperationDelegate {
|
||||
|
||||
func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didCreate account: Account) {
|
||||
let rootViewController = view.window?.rootViewController
|
||||
|
||||
account.refreshAll { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
guard let viewController = rootViewController else {
|
||||
return
|
||||
}
|
||||
viewController.presentError(error)
|
||||
}
|
||||
}
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didFailWith error: Error) {
|
||||
presentError(error)
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
//
|
||||
// AddExtensionPointViewController.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 4/16/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol AddExtensionPointDismissDelegate: UIViewController {
|
||||
func dismiss()
|
||||
}
|
||||
|
||||
class AddExtensionPointViewController: UITableViewController, AddExtensionPointDismissDelegate {
|
||||
|
||||
private var availableExtensionPointTypes = [ExtensionPoint.Type]()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
availableExtensionPointTypes = ExtensionPointManager.shared.availableExtensionPointTypes.sorted(by: { $0.title < $1.title })
|
||||
}
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
1
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return availableExtensionPointTypes.count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "SettingsExtensionTableViewCell", for: indexPath) as! SettingsComboTableViewCell
|
||||
|
||||
let extensionPointType = availableExtensionPointTypes[indexPath.row]
|
||||
cell.comboNameLabel?.text = extensionPointType.title
|
||||
cell.comboImage?.image = extensionPointType.image
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
return NSLocalizedString("Feed Provider", comment: "Feed Provider Header")
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
|
||||
return NSLocalizedString("Feed Providers allow you to subscribe to some pages as if they were RSS feeds.", comment: "Feed Provider Footer")
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let navController = UIStoryboard.settings.instantiateViewController(withIdentifier: "EnableExtensionPointNavigationViewController") as! UINavigationController
|
||||
navController.modalPresentationStyle = .currentContext
|
||||
let enableViewController = navController.topViewController as! EnableExtensionPointViewController
|
||||
enableViewController.delegate = self
|
||||
enableViewController.extensionPointType = availableExtensionPointTypes[indexPath.row]
|
||||
present(navController, animated: true)
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
navigationController?.popViewController(animated: false)
|
||||
}
|
||||
|
||||
}
|
||||
182
iOS/Settings/Appearance/ArticleThemeManagerView.swift
Normal file
182
iOS/Settings/Appearance/ArticleThemeManagerView.swift
Normal file
@@ -0,0 +1,182 @@
|
||||
//
|
||||
// ArticleThemeManagerView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 20/12/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ArticleThemeManagerView: View {
|
||||
|
||||
@StateObject private var themeManager = ArticleThemesManager.shared
|
||||
@State private var showDeleteConfirmation: (String, Bool) = ("", false)
|
||||
@State private var showImportThemeView: Bool = false
|
||||
@State private var showImportConfirmationAlert: (ArticleTheme?, Bool) = (nil, false)
|
||||
@State private var showImportErrorAlert: (Error?, Bool) = (nil, false)
|
||||
@State private var showImportSuccessAlert: Bool = false
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section(header: Text("Built-in Themes", comment: "Section header for installed themes"), footer: Text("These themes cannot be deleted.", comment: "Section footer for installed themes.")) {
|
||||
articleThemeRow(try! themeManager.articleThemeWithThemeName(AppDefaults.defaultThemeName))
|
||||
ForEach(0..<themeManager.themesByDeveloper().builtIn.count, id: \.self) { i in
|
||||
articleThemeRow(themeManager.themesByDeveloper().0[i])
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Other Themes", comment: "Section header for third party themes")) {
|
||||
ForEach(0..<themeManager.themesByDeveloper().other.count, id: \.self) { i in
|
||||
articleThemeRow(themeManager.themesByDeveloper().1[i])
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.navigationTitle(Text("Article Themes", comment: "Navigation bar title for Article Themes"))
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
showImportThemeView = true
|
||||
} label: {
|
||||
Label {
|
||||
Text("Import Theme", comment: "Button title")
|
||||
} icon: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.fileImporter(isPresented: $showImportThemeView, allowedContentTypes: NNWThemeDocument.readableContentTypes) { result in
|
||||
switch result {
|
||||
case .success(let success):
|
||||
do {
|
||||
let url = URL(fileURLWithPath: success.path)
|
||||
if url.startAccessingSecurityScopedResource() {
|
||||
let theme = try ArticleTheme(path: success.path, isAppTheme: false)
|
||||
showImportConfirmationAlert = (theme, true)
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
} catch {
|
||||
showImportErrorAlert = (error, true)
|
||||
}
|
||||
case .failure(let failure):
|
||||
showImportErrorAlert = (failure, true)
|
||||
}
|
||||
}
|
||||
.alert(Text("Are you sure you want to delete “\(showDeleteConfirmation.0)”?", comment: "Alert title: confirm theme deletion"),
|
||||
isPresented: $showDeleteConfirmation.1, actions: {
|
||||
Button(role: .destructive) {
|
||||
themeManager.deleteTheme(themeName: showDeleteConfirmation.0)
|
||||
} label: {
|
||||
Text("Delete Theme", comment: "Button title")
|
||||
}
|
||||
|
||||
Button(role: .cancel) {
|
||||
|
||||
} label: {
|
||||
Text("Cancel", comment: "Button title")
|
||||
}
|
||||
}, message: {
|
||||
Text("This action cannot be undone.", comment: "Alert message: confirm theme deletion")
|
||||
})
|
||||
.alert(Text("Import Theme", comment: "Alert title: confirm theme import"),
|
||||
isPresented: $showImportConfirmationAlert.1,
|
||||
actions: {
|
||||
Button {
|
||||
do {
|
||||
if themeManager.themeExists(filename: showImportConfirmationAlert.0!.path!) {
|
||||
if try! themeManager.articleThemeWithThemeName(showImportConfirmationAlert.0!.name).isAppTheme {
|
||||
showImportErrorAlert = (LocalizedNetNewsWireError.duplicateDefaultTheme, true)
|
||||
} else {
|
||||
try themeManager.importTheme(filename: showImportConfirmationAlert.0!.path!)
|
||||
showImportSuccessAlert = true
|
||||
}
|
||||
} else {
|
||||
try themeManager.importTheme(filename: showImportConfirmationAlert.0!.path!)
|
||||
showImportSuccessAlert = true
|
||||
}
|
||||
} catch {
|
||||
showImportErrorAlert = (error, true)
|
||||
}
|
||||
} label: {
|
||||
let exists = themeManager.themeExists(filename: showImportConfirmationAlert.0?.path ?? "")
|
||||
if exists == true {
|
||||
Text("Overwrite Theme", comment: "Button title")
|
||||
} else {
|
||||
Text("Import Theme", comment: "Button title")
|
||||
}
|
||||
}
|
||||
|
||||
Button(role: .cancel) {
|
||||
|
||||
} label: {
|
||||
Text("Cancel", comment: "Button title")
|
||||
}
|
||||
}, message: {
|
||||
let exists = themeManager.themeExists(filename: showImportConfirmationAlert.0?.path ?? "")
|
||||
if exists {
|
||||
Text("The theme “\(showImportConfirmationAlert.0?.name ?? "")” already exists. Do you want to overwrite it?", comment: "Alert message: confirm theme import and overwrite of existing theme")
|
||||
} else {
|
||||
Text("Are you sure you want to import “\(showImportConfirmationAlert.0?.name ?? "")” by \(showImportConfirmationAlert.0?.creatorName ?? "")?", comment: "Alert message: confirm theme import")
|
||||
}
|
||||
})
|
||||
.alert(Text("Imported Successfully", comment: "Alert title: theme imported successfully"),
|
||||
isPresented: $showImportSuccessAlert,
|
||||
actions: {
|
||||
Button(role: .cancel) {
|
||||
|
||||
} label: {
|
||||
Text("Dismiss", comment: "Button title")
|
||||
}
|
||||
}, message: {
|
||||
Text("The theme “\(showImportConfirmationAlert.0?.name ?? "")” has been imported.", comment: "Alert message: theme imported successfully")
|
||||
})
|
||||
.alert(Text("Error", comment: "Alert title: Error"),
|
||||
isPresented: $showImportErrorAlert.1,
|
||||
actions: { }, message: {
|
||||
Text("\(showImportErrorAlert.0?.localizedDescription ?? "")")
|
||||
})
|
||||
}
|
||||
|
||||
func articleThemeRow(_ theme: ArticleTheme) -> some View {
|
||||
Button {
|
||||
themeManager.currentThemeName = theme.name
|
||||
} label: {
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text(theme.name)
|
||||
.foregroundColor(.primary)
|
||||
Text("Created by \(theme.creatorName)", comment: "Article theme creator byline.")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
if themeManager.currentThemeName == theme.name {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(Color(uiColor: AppAssets.primaryAccentColor))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||
if theme.isAppTheme || theme.name == themeManager.currentThemeName {
|
||||
|
||||
} else {
|
||||
Button {
|
||||
showDeleteConfirmation = (theme.name, true)
|
||||
} label: {
|
||||
Text("Delete", comment: "Button title")
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
.tint(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ArticleThemeImporterView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ArticleThemeManagerView()
|
||||
}
|
||||
}
|
||||
87
iOS/Settings/Appearance/ColorPaletteSelectorView.swift
Normal file
87
iOS/Settings/Appearance/ColorPaletteSelectorView.swift
Normal file
@@ -0,0 +1,87 @@
|
||||
//
|
||||
// ColorPaletteSelectorView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 13/11/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ColorPaletteSelectorView: View {
|
||||
|
||||
@StateObject private var appDefaults = AppDefaults.shared
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
appLightButton()
|
||||
Spacer()
|
||||
appDarkButton()
|
||||
Spacer()
|
||||
appAutomaticButton()
|
||||
}
|
||||
}
|
||||
|
||||
func appLightButton() -> some View {
|
||||
VStack(spacing: 4) {
|
||||
Image("app.appearance.light")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 40.0, height: 40.0)
|
||||
Text("Always Light", comment: "Button: always use light display mode")
|
||||
.font(.subheadline)
|
||||
if AppDefaults.userInterfaceColorPalette == .light {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(Color(uiColor: AppAssets.primaryAccentColor))
|
||||
} else {
|
||||
Image(systemName: "circle")
|
||||
}
|
||||
}.onTapGesture {
|
||||
AppDefaults.userInterfaceColorPalette = .light
|
||||
}
|
||||
}
|
||||
|
||||
func appDarkButton() -> some View {
|
||||
VStack(spacing: 4) {
|
||||
Image("app.appearance.dark")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 40.0, height: 40.0)
|
||||
Text("Always Dark", comment: "Button: always use dark display mode")
|
||||
.font(.subheadline)
|
||||
if AppDefaults.userInterfaceColorPalette == .dark {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(Color(uiColor: AppAssets.primaryAccentColor))
|
||||
} else {
|
||||
Image(systemName: "circle")
|
||||
}
|
||||
}.onTapGesture {
|
||||
AppDefaults.userInterfaceColorPalette = .dark
|
||||
}
|
||||
}
|
||||
|
||||
func appAutomaticButton() -> some View {
|
||||
VStack(spacing: 4) {
|
||||
Image("app.appearance.automatic")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 40.0, height: 40.0)
|
||||
Text("Use Device", comment: "Button: always use device display mode")
|
||||
.font(.subheadline)
|
||||
if AppDefaults.userInterfaceColorPalette == .automatic {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(Color(uiColor: AppAssets.primaryAccentColor))
|
||||
} else {
|
||||
Image(systemName: "circle")
|
||||
}
|
||||
}.onTapGesture {
|
||||
AppDefaults.userInterfaceColorPalette = .automatic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DisplayModeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ColorPaletteSelectorView()
|
||||
}
|
||||
}
|
||||
52
iOS/Settings/Appearance/DisplayAndBehaviorsView.swift
Normal file
52
iOS/Settings/Appearance/DisplayAndBehaviorsView.swift
Normal file
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// DisplayAndBehaviorsView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 12/11/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DisplayAndBehaviorsView: View {
|
||||
|
||||
@StateObject private var appDefaults = AppDefaults.shared
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section(header: Text("Application", comment: "Display & Behaviours: Application section header")) {
|
||||
ColorPaletteSelectorView()
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
|
||||
Section(header: Text("Timeline", comment: "Display & Behaviours: Timeline section header")) {
|
||||
SettingsRow.sortOldestToNewest($appDefaults.timelineSortDirectionBool)
|
||||
SettingsRow.groupByFeed($appDefaults.timelineGroupByFeed)
|
||||
SettingsRow.confirmMarkAllAsRead($appDefaults.confirmMarkAllAsRead)
|
||||
SettingsRow.markAsReadOnScroll($appDefaults.markArticlesAsReadOnScroll)
|
||||
SettingsRow.refreshToClearReadArticles($appDefaults.refreshClearsReadArticles)
|
||||
SettingsRow.timelineLayout
|
||||
}
|
||||
|
||||
Section(header: Text("Article", comment: "Display & Behaviours: Article section header")) {
|
||||
SettingsRow.themeSelection
|
||||
SettingsRow.openLinksInNetNewsWire(Binding<Bool>(
|
||||
get: { !appDefaults.useSystemBrowser },
|
||||
set: { appDefaults.useSystemBrowser = !$0 }
|
||||
))
|
||||
// TODO: Add Reader Mode Defaults here. See #3684.
|
||||
}
|
||||
}
|
||||
.navigationTitle(Text("Display & Behaviors", comment: "Navigation title for Display & Behaviours"))
|
||||
.tint(Color(uiColor: AppAssets.primaryAccentColor))
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
struct AppearanceManagementView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DisplayAndBehaviorsView()
|
||||
}
|
||||
}
|
||||
93
iOS/Settings/Appearance/TimelineCustomizerView.swift
Normal file
93
iOS/Settings/Appearance/TimelineCustomizerView.swift
Normal file
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// TimelineCustomizerView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 20/12/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
|
||||
struct TimelineCustomizerView: View {
|
||||
|
||||
@StateObject private var appDefaults = AppDefaults.shared
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section(header: Text("Icon Size", comment: "Timline Customiser: Icon size section header")) {
|
||||
ZStack {
|
||||
TickMarkSliderView(minValue: 1, maxValue: 3, currentValue: Binding(get: {
|
||||
Float(appDefaults.timelineIconSize.rawValue)
|
||||
}, set: { AppDefaults.shared.timelineIconSize = IconSize(rawValue: Int($0))! }))
|
||||
}
|
||||
.customInsetGroupedRowStyle()
|
||||
}
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 30, bottom: 0, trailing: 30))
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
|
||||
Section(header: Text("Number of Lines", comment: "Timeline customiser: Number of lines section header")) {
|
||||
ZStack {
|
||||
TickMarkSliderView(minValue: 1, maxValue: 5, currentValue: Binding(get: {
|
||||
Float(appDefaults.timelineNumberOfLines)
|
||||
}, set: { appDefaults.timelineNumberOfLines = Int($0) }))
|
||||
}
|
||||
.customInsetGroupedRowStyle()
|
||||
}
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 30, bottom: 0, trailing: 30))
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
|
||||
Section {
|
||||
timeLinePreviewRow
|
||||
.listRowInsets(EdgeInsets(top: 8, leading: 4, bottom: 4, trailing: 24))
|
||||
}
|
||||
}
|
||||
.listStyle(.grouped)
|
||||
.navigationTitle(Text("Timeline Layout", comment: "Navigation bar title for Timeline Layout"))
|
||||
|
||||
}
|
||||
|
||||
var timeLinePreviewRow: some View {
|
||||
HStack(alignment: .top, spacing: 6) {
|
||||
VStack {
|
||||
Circle()
|
||||
.foregroundColor(Color(uiColor: AppAssets.primaryAccentColor))
|
||||
.frame(width: 12, height: 12)
|
||||
Spacer()
|
||||
}.frame(width: 12)
|
||||
VStack {
|
||||
Image("faviconTemplateImage")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.frame(width: appDefaults.timelineIconSize.size.width, height: appDefaults.timelineIconSize.size.height)
|
||||
.foregroundColor(Color(uiColor: AppAssets.primaryAccentColor))
|
||||
Spacer()
|
||||
}.frame(width: appDefaults.timelineIconSize.size.width)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(verbatim: "Enim ut tellus elementum sagittis vitae et. Nibh praesent tristique magna sit amet purus gravida quis blandit. Neque volutpat ac tincidunt vitae semper quis lectus nulla. Massa id neque aliquam vestibulum morbi blandit. Ultrices vitae auctor eu augue. Enim eu turpis egestas pretium aenean pharetra magna. Eget gravida cum sociis natoque. Sit amet consectetur adipiscing elit. Auctor eu augue ut lectus arcu bibendum. Maecenas volutpat blandit aliquam etiam erat velit. Ut pharetra sit amet aliquam id diam maecenas ultricies. In hac habitasse platea dictumst quisque sagittis purus sit amet.")
|
||||
.bold()
|
||||
.lineLimit(appDefaults.timelineNumberOfLines)
|
||||
HStack {
|
||||
Text("Feed name", comment: "Feed name placeholder used in timeline preview")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
Spacer()
|
||||
Text("08:51", comment: "Sample time used in timeline preview")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
}.padding(0)
|
||||
}
|
||||
}
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.padding(.vertical, 4)
|
||||
.padding(.leading, 4)
|
||||
}
|
||||
}
|
||||
|
||||
struct TimelineCustomizerView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TimelineCustomizerView()
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
//
|
||||
// ArticleThemesTableViewController.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 9/12/21.
|
||||
// Copyright © 2021 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UniformTypeIdentifiers
|
||||
import RSCore
|
||||
import UIKit
|
||||
|
||||
class ArticleThemesTableViewController: UITableViewController, Logging {
|
||||
|
||||
override func viewDidLoad() {
|
||||
let importBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(importTheme(_:)));
|
||||
importBarButtonItem.title = NSLocalizedString("Import Theme", comment: "Import Theme");
|
||||
navigationItem.rightBarButtonItem = importBarButtonItem
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(articleThemeNamesDidChangeNotification(_:)), name: .ArticleThemeNamesDidChangeNotification, object: nil)
|
||||
}
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
@objc func articleThemeNamesDidChangeNotification(_ note: Notification) {
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
@objc func importTheme(_ sender: Any?) {
|
||||
let docPicker = UIDocumentPickerViewController(forOpeningContentTypes: [.nnwTheme], asCopy: true)
|
||||
docPicker.delegate = self
|
||||
docPicker.modalPresentationStyle = .formSheet
|
||||
self.present(docPicker, animated: true)
|
||||
}
|
||||
|
||||
// MARK: - Table view data source
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return ArticleThemesManager.shared.themeNames.count + 1
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
|
||||
|
||||
let themeName: String
|
||||
if indexPath.row == 0 {
|
||||
themeName = ArticleTheme.defaultTheme.name
|
||||
} else {
|
||||
themeName = ArticleThemesManager.shared.themeNames[indexPath.row - 1]
|
||||
}
|
||||
|
||||
cell.textLabel?.text = themeName
|
||||
if themeName == ArticleThemesManager.shared.currentTheme.name {
|
||||
cell.accessoryType = .checkmark
|
||||
} else {
|
||||
cell.accessoryType = .none
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
guard let cell = tableView.cellForRow(at: indexPath), let themeName = cell.textLabel?.text else { return }
|
||||
ArticleThemesManager.shared.currentThemeName = themeName
|
||||
navigationController?.popViewController(animated: true)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
guard let cell = tableView.cellForRow(at: indexPath),
|
||||
let themeName = cell.textLabel?.text else { return nil }
|
||||
|
||||
guard let theme = try? ArticleThemesManager.shared.articleThemeWithThemeName(themeName), !theme.isAppTheme else { return nil }
|
||||
|
||||
let deleteTitle = NSLocalizedString("Delete", comment: "Delete")
|
||||
let deleteAction = UIContextualAction(style: .normal, title: deleteTitle) { [weak self] (action, view, completion) in
|
||||
let title = NSLocalizedString("Delete Theme?", comment: "Delete Theme")
|
||||
|
||||
let localizedMessageText = NSLocalizedString("Are you sure you want to delete the theme “%@”?.", comment: "Delete Theme Message")
|
||||
let message = NSString.localizedStringWithFormat(localizedMessageText as NSString, themeName) as String
|
||||
|
||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
|
||||
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
|
||||
let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel) { action in
|
||||
completion(true)
|
||||
}
|
||||
alertController.addAction(cancelAction)
|
||||
|
||||
let deleteTitle = NSLocalizedString("Delete", comment: "Delete")
|
||||
let deleteAction = UIAlertAction(title: deleteTitle, style: .destructive) { action in
|
||||
ArticleThemesManager.shared.deleteTheme(themeName: themeName)
|
||||
completion(true)
|
||||
}
|
||||
alertController.addAction(deleteAction)
|
||||
|
||||
self?.present(alertController, animated: true)
|
||||
}
|
||||
|
||||
deleteAction.image = AppAssets.trashImage
|
||||
deleteAction.backgroundColor = UIColor.systemRed
|
||||
|
||||
return UISwipeActionsConfiguration(actions: [deleteAction])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UIDocumentPickerDelegate
|
||||
|
||||
extension ArticleThemesTableViewController: UIDocumentPickerDelegate {
|
||||
|
||||
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||
guard let url = urls.first else { return }
|
||||
ArticleThemeImporter.importTheme(controller: self, filename: url.standardizedFileURL.path)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
//
|
||||
// ColorPaletteTableViewController.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 3/15/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class ColorPaletteTableViewController: UITableViewController {
|
||||
|
||||
// MARK: - Table view data source
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return UserInterfaceColorPalette.allCases.count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
|
||||
let rowColorPalette = UserInterfaceColorPalette.allCases[indexPath.row]
|
||||
cell.textLabel?.text = String(describing: rowColorPalette)
|
||||
if rowColorPalette == AppDefaults.userInterfaceColorPalette {
|
||||
cell.accessoryType = .checkmark
|
||||
} else {
|
||||
cell.accessoryType = .none
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
if let colorPalette = UserInterfaceColorPalette(rawValue: indexPath.row) {
|
||||
AppDefaults.userInterfaceColorPalette = colorPalette
|
||||
}
|
||||
navigationController?.popViewController(animated: true)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
//
|
||||
// EnableExtensionPointViewController.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 4/16/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import AuthenticationServices
|
||||
import Account
|
||||
import OAuthSwift
|
||||
import Secrets
|
||||
|
||||
class EnableExtensionPointViewController: UITableViewController {
|
||||
|
||||
@IBOutlet weak var extensionDescription: UILabel!
|
||||
|
||||
private var callbackURL: URL? = nil
|
||||
private var oauth: OAuthSwift?
|
||||
|
||||
weak var delegate: AddExtensionPointDismissDelegate?
|
||||
var extensionPointType: ExtensionPoint.Type?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
navigationItem.title = extensionPointType?.title
|
||||
extensionDescription.attributedText = extensionPointType?.description
|
||||
tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
|
||||
}
|
||||
|
||||
@IBAction func cancel(_ sender: Any) {
|
||||
dismiss(animated: true, completion: nil)
|
||||
delegate?.dismiss()
|
||||
}
|
||||
|
||||
@IBAction func enable(_ sender: Any) {
|
||||
guard let extensionPointType = extensionPointType else { return }
|
||||
|
||||
if let oauth1 = extensionPointType as? OAuth1SwiftProvider.Type {
|
||||
enableOauth1(oauth1)
|
||||
} else if let oauth2 = extensionPointType as? OAuth2SwiftProvider.Type {
|
||||
enableOauth2(oauth2)
|
||||
} else {
|
||||
ExtensionPointManager.shared.activateExtensionPoint(extensionPointType) { result in
|
||||
if case .failure(let error) = result {
|
||||
self.presentError(error)
|
||||
}
|
||||
self.dismiss(animated: true, completion: nil)
|
||||
self.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 = extensionPointType?.image
|
||||
return headerView
|
||||
} else {
|
||||
return super.tableView(tableView, viewForHeaderInSection: section)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension EnableExtensionPointViewController: OAuthSwiftURLHandlerType {
|
||||
|
||||
public func handle(_ url: URL) {
|
||||
let session = ASWebAuthenticationSession(url: url, callbackURLScheme: callbackURL!.scheme, completionHandler: { (url, error) in
|
||||
if let callbackedURL = url {
|
||||
OAuth1Swift.handle(url: callbackedURL)
|
||||
}
|
||||
|
||||
guard let error = error else { return }
|
||||
|
||||
self.oauth?.cancel()
|
||||
self.oauth = nil
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.dismiss(animated: true, completion: nil)
|
||||
self.delegate?.dismiss()
|
||||
}
|
||||
|
||||
if case ASWebAuthenticationSessionError.canceledLogin = error {
|
||||
print("Login cancelled.")
|
||||
} else {
|
||||
self.presentError(error)
|
||||
}
|
||||
})
|
||||
|
||||
session.presentationContextProvider = self
|
||||
if !session.start() {
|
||||
print("Session failed to start!!!")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension EnableExtensionPointViewController: ASWebAuthenticationPresentationContextProviding {
|
||||
|
||||
public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
|
||||
return view.window!
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension EnableExtensionPointViewController {
|
||||
|
||||
func enableOauth1(_ provider: OAuth1SwiftProvider.Type) {
|
||||
callbackURL = provider.callbackURL
|
||||
|
||||
let oauth1 = provider.oauth1Swift
|
||||
self.oauth = oauth1
|
||||
oauth1.authorizeURLHandler = self
|
||||
|
||||
oauth1.authorize(withCallbackURL: callbackURL!) { [weak self] result in
|
||||
guard let self = self, let extensionPointType = self.extensionPointType else { return }
|
||||
|
||||
switch result {
|
||||
case .success(let tokenSuccess):
|
||||
ExtensionPointManager.shared.activateExtensionPoint(extensionPointType, tokenSuccess: tokenSuccess) { result in
|
||||
if case .failure(let error) = result {
|
||||
self.presentError(error)
|
||||
}
|
||||
self.dismiss(animated: true, completion: nil)
|
||||
self.delegate?.dismiss()
|
||||
}
|
||||
case .failure(let oauthSwiftError):
|
||||
self.presentError(oauthSwiftError)
|
||||
}
|
||||
|
||||
self.oauth?.cancel()
|
||||
self.oauth = nil
|
||||
}
|
||||
}
|
||||
|
||||
func enableOauth2(_ provider: OAuth2SwiftProvider.Type) {
|
||||
callbackURL = provider.callbackURL
|
||||
|
||||
let oauth2 = provider.oauth2Swift
|
||||
self.oauth = oauth2
|
||||
oauth2.authorizeURLHandler = self
|
||||
|
||||
let oauth2Vars = provider.oauth2Vars
|
||||
|
||||
oauth2.authorize(withCallbackURL: callbackURL!, scope: oauth2Vars.scope, state: oauth2Vars.state, parameters: oauth2Vars.params) { [weak self] result in
|
||||
guard let self = self, let extensionPointType = self.extensionPointType else { return }
|
||||
|
||||
switch result {
|
||||
case .success(let tokenSuccess):
|
||||
ExtensionPointManager.shared.activateExtensionPoint(extensionPointType, tokenSuccess: tokenSuccess) { result in
|
||||
if case .failure(let error) = result {
|
||||
self.presentError(error)
|
||||
}
|
||||
self.dismiss(animated: true, completion: nil)
|
||||
self.delegate?.dismiss()
|
||||
}
|
||||
case .failure(let oauthSwiftError):
|
||||
self.presentError(oauthSwiftError)
|
||||
}
|
||||
|
||||
self.oauth?.cancel()
|
||||
self.oauth = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
254
iOS/Settings/General/SettingsRows.swift
Normal file
254
iOS/Settings/General/SettingsRows.swift
Normal file
@@ -0,0 +1,254 @@
|
||||
//
|
||||
// SettingsRows.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 12/11/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Account
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
|
||||
// MARK: - Rows
|
||||
|
||||
struct SettingsRow {
|
||||
|
||||
/// This row, when tapped, will open iOS System Settings.
|
||||
static var openSystemSettings: some View {
|
||||
Label {
|
||||
Text("Open System Settings", comment: "Button: opens device Settings app.")
|
||||
} icon: {
|
||||
Image("system.settings")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 25.0, height: 25.0)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
}
|
||||
.onTapGesture {
|
||||
UIApplication.shared.open(URL(string: "\(UIApplication.openSettingsURLString)")!)
|
||||
}
|
||||
}
|
||||
|
||||
/// This row, when tapped, will push the New Article Notifications
|
||||
/// screen in to view.
|
||||
static var configureNewArticleNotifications: some View {
|
||||
NavigationLink(destination: NewArticleNotificationsView()) {
|
||||
Label {
|
||||
Text("New Article Notifications", comment: "Button: opens New Article Notifications view")
|
||||
} icon: {
|
||||
Image("notifications.sounds")
|
||||
.resizable()
|
||||
.frame(width: 25.0, height: 25.0)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This row, when tapped, will push the the Add Account screen
|
||||
/// in to view.
|
||||
static var addAccount: some View {
|
||||
NavigationLink(destination: AccountsManagementView()) {
|
||||
Label {
|
||||
Text("Manage Accounts", comment: "Button: opens Accounts Management view")
|
||||
} icon: {
|
||||
Image("app.account")
|
||||
.resizable()
|
||||
.frame(width: 25.0, height: 25.0)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Toggle for determining if articles are marked as read when scrolling the timeline view.
|
||||
/// - Parameter preference: `Binding<Bool>`
|
||||
/// - Returns: `some View`
|
||||
static func markAsReadOnScroll(_ preference: Binding<Bool>) -> some View {
|
||||
Toggle(isOn: preference) {
|
||||
Text("Mark As Read on Scroll", comment: "Mark As Read on Scroll")
|
||||
}
|
||||
}
|
||||
|
||||
/// This row, when tapped, will push the the Manage Extension screen
|
||||
/// in to view.
|
||||
static var manageExtensions: some View {
|
||||
NavigationLink(destination: ExtensionsManagementView()) {
|
||||
Label {
|
||||
Text("Manage Extensions", comment: "Button: opens Extensions Management view")
|
||||
} icon: {
|
||||
Image("app.extension")
|
||||
.resizable()
|
||||
.frame(width: 25.0, height: 25.0)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This row, when tapped, will present the Import
|
||||
/// Subscriptions Action Sheet.
|
||||
static func importOPML(showImportActionSheet: Binding<Bool>) -> some View {
|
||||
Button {
|
||||
showImportActionSheet.wrappedValue.toggle()
|
||||
} label: {
|
||||
Label {
|
||||
Text("Import Subscriptions", comment: "Button: opens import subscriptions view")
|
||||
.foregroundColor(.primary)
|
||||
|
||||
} icon: {
|
||||
Image("app.import.opml")
|
||||
.resizable()
|
||||
.frame(width: 25.0, height: 25.0)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This row, when tapped, will present the Export
|
||||
/// Subscriptions Action Sheet.
|
||||
static func exportOPML(showExportActionSheet: Binding<Bool>) -> some View {
|
||||
Button {
|
||||
showExportActionSheet.wrappedValue.toggle()
|
||||
} label: {
|
||||
Label {
|
||||
Text("Export Subscriptions", comment: "Button: opens Export Subscriptions view")
|
||||
.foregroundColor(.primary)
|
||||
|
||||
} icon: {
|
||||
Image("app.export.opml")
|
||||
.resizable()
|
||||
.frame(width: 25.0, height: 25.0)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a `Toggle` which triggers changes to the user's sort order preference.
|
||||
/// - Parameter preference: `Binding<Bool>`
|
||||
/// - Returns: `Toggle`
|
||||
static func sortOldestToNewest(_ preference: Binding<Bool>) -> some View {
|
||||
Toggle(isOn: preference) {
|
||||
Text("Sort Oldest to Newest", comment: "Toggle: Sort articles from oldest to newest when enabled.")
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a `Toggle` which triggers changes to the user's grouping preference.
|
||||
/// - Parameter preference: `Binding<Bool>`
|
||||
/// - Returns: `Toggle`
|
||||
static func groupByFeed(_ preference: Binding<Bool>) -> some View {
|
||||
Toggle(isOn: preference) {
|
||||
Text("Group by Feed", comment: "Toggle: groups articles by feed when enabled.")
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a `Toggle` which triggers changes to the user's refresh to clear preferences.
|
||||
/// - Parameter preference: `Binding<Bool>`
|
||||
/// - Returns: `Toggle`
|
||||
static func refreshToClearReadArticles(_ preference: Binding<Bool>) -> some View {
|
||||
Toggle(isOn: preference) {
|
||||
Text("Refresh to Clear Read Articles", comment: "Toggle: when enabled, articles will be cleared when the timeline is refreshed")
|
||||
}
|
||||
}
|
||||
|
||||
/// This row, when tapped, will push the the Timeline Layout screen
|
||||
/// in to view.
|
||||
static var timelineLayout: some View {
|
||||
NavigationLink {
|
||||
TimelineCustomizerView()
|
||||
} label: {
|
||||
Text("Timeline Layout", comment: "Button: opens the timeline customiser")
|
||||
}
|
||||
}
|
||||
|
||||
/// This row, when tapped, will push the the Theme Selector screen
|
||||
/// in to view.
|
||||
static var themeSelection: some View {
|
||||
NavigationLink(destination: ArticleThemeManagerView()) {
|
||||
HStack {
|
||||
Text("Article Theme", comment: "Button: opens the Article Theme manager view")
|
||||
Spacer()
|
||||
Text(ArticleThemesManager.shared.currentTheme.name)
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a `Toggle` which triggers changes to the user's mark all as read preferences.
|
||||
/// - Parameter preference: `Binding<Bool>`
|
||||
/// - Returns: `Toggle`
|
||||
static func confirmMarkAllAsRead(_ preference: Binding<Bool>) -> some View {
|
||||
Toggle(isOn: preference) {
|
||||
Text("Confirm Mark All as Read", comment: "Toggle: when enabled, the app will confirm whether to mark all items as read")
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a `Toggle` which triggers changes to the user's link opening behaviour.
|
||||
/// - Parameter preference: `Binding<Bool>`
|
||||
/// - Returns: `Toggle`
|
||||
static func openLinksInNetNewsWire(_ preference: Binding<Bool>) -> some View {
|
||||
Toggle(isOn: preference) {
|
||||
Text("Open Links in NetNewsWire", comment: "Toggle: when enabled, links will open in NetNewsWire")
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add Reader Mode Defaults here. See #3684.
|
||||
|
||||
/// This row, when tapped, will push the New Article Notifications
|
||||
/// screen in to view.
|
||||
static func configureAppearance(_ isShown: Binding<Bool>) -> some View {
|
||||
NavigationLink(destination: DisplayAndBehaviorsView(), isActive: isShown) {
|
||||
Label {
|
||||
Text("Display & Behaviours", comment: "Button: opens the Display and Appearance view.")
|
||||
} icon: {
|
||||
Image("app.appearance")
|
||||
.resizable()
|
||||
.frame(width: 25.0, height: 25.0)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the help sheet the user wishes to see.
|
||||
/// - Parameters:
|
||||
/// - sheet: The sheet provided to create the view.
|
||||
/// - selectedSheet: A `Binding` to the currently selected sheet. This is set, followed by `show`.
|
||||
/// - show: A `Binding` to `Bool` which triggers the sheet to display.
|
||||
/// - Returns: `View`
|
||||
static func showHelpSheet(sheet: HelpSheet, selectedSheet: Binding<HelpSheet>, _ show: Binding<Bool>) -> some View {
|
||||
Label {
|
||||
Text(sheet.description)
|
||||
} icon: {
|
||||
Image(systemName: sheet.systemImage)
|
||||
.resizable()
|
||||
.renderingMode(.template)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.foregroundColor(Color(uiColor: AppAssets.primaryAccentColor))
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 25.0, height: 25.0)
|
||||
}
|
||||
.onTapGesture {
|
||||
selectedSheet.wrappedValue = sheet
|
||||
show.wrappedValue.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
static var aboutNetNewsWire: some View {
|
||||
NavigationLink {
|
||||
AboutView()
|
||||
} label: {
|
||||
Label {
|
||||
Text("About", comment: "Button: opens the NetNewsWire about view.")
|
||||
} icon: {
|
||||
Image(systemName: "info.circle.fill")
|
||||
.resizable()
|
||||
.renderingMode(.template)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.foregroundColor(Color(uiColor: AppAssets.primaryAccentColor))
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 25.0, height: 25.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
160
iOS/Settings/General/SettingsView.swift
Normal file
160
iOS/Settings/General/SettingsView.swift
Normal file
@@ -0,0 +1,160 @@
|
||||
//
|
||||
// SettingsView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 12/11/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Account
|
||||
import UniformTypeIdentifiers
|
||||
import UserNotifications
|
||||
|
||||
struct SettingsView: View {
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@StateObject private var appDefaults = AppDefaults.shared
|
||||
@StateObject private var viewModel = SettingsViewModel()
|
||||
|
||||
@Binding var isConfigureAppearanceShown: Bool
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
// Device Permissions
|
||||
Section(header: Text("Device Permissions", comment: "Settings: Device Permissions section header."),
|
||||
footer: Text("Configure NetNewsWire's access to Siri, background app refresh, mobile data, and more.", comment: "Settings: Device Permissions section footer.")) {
|
||||
SettingsRow.openSystemSettings
|
||||
}
|
||||
|
||||
// Account/Extensions/OPML Management
|
||||
Section(header: Text("Accounts & Extensions", comment: "Settings: Accounts and Extensions section header."),
|
||||
footer: Text("Add, delete, enable, or disable accounts and extensions.", comment: "Settings: Accounts and Extensions section footer.")) {
|
||||
SettingsRow.addAccount
|
||||
SettingsRow.manageExtensions
|
||||
SettingsRow.importOPML(showImportActionSheet: $viewModel.showImportActionSheet)
|
||||
.confirmationDialog(Text("Choose an account to receive the imported feeds and folders", comment: "Import OPML confirmation title."),
|
||||
isPresented: $viewModel.showImportActionSheet,
|
||||
titleVisibility: .visible) {
|
||||
ForEach(AccountManager.shared.sortedActiveAccounts, id: \.self) { account in
|
||||
Button(account.nameForDisplay) {
|
||||
viewModel.importAccount = account
|
||||
viewModel.showImportView = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsRow.exportOPML(showExportActionSheet: $viewModel.showExportActionSheet)
|
||||
.confirmationDialog(Text("Choose an account with the subscriptions to export", comment: "Export OPML confirmation title."),
|
||||
isPresented: $viewModel.showExportActionSheet,
|
||||
titleVisibility: .visible) {
|
||||
ForEach(AccountManager.shared.sortedAccounts, id: \.self) { account in
|
||||
Button(account.nameForDisplay) {
|
||||
do {
|
||||
let document = try OPMLDocument(account)
|
||||
viewModel.exportDocument = document
|
||||
viewModel.showExportView = true
|
||||
} catch {
|
||||
viewModel.importExportError = error
|
||||
viewModel.showImportExportError = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Appearance
|
||||
Section(header: Text("Appearance", comment: "Settings: Appearance section header."),
|
||||
footer: Text("Manage the look, feel, and behavior of NetNewsWire.", comment: "Settings: Appearance section footer.")) {
|
||||
SettingsRow.configureAppearance($isConfigureAppearanceShown)
|
||||
if viewModel.notificationPermissions == .authorized {
|
||||
SettingsRow.configureNewArticleNotifications
|
||||
}
|
||||
}
|
||||
|
||||
// Help
|
||||
Section {
|
||||
ForEach(0..<HelpSheet.allCases.count, id: \.self) { i in
|
||||
SettingsRow.showHelpSheet(sheet: HelpSheet.allCases[i], selectedSheet: $viewModel.helpSheet, $viewModel.showHelpSheet)
|
||||
}
|
||||
SettingsRow.aboutNetNewsWire
|
||||
}
|
||||
}
|
||||
.tint(Color(uiColor: AppAssets.primaryAccentColor))
|
||||
.listStyle(.insetGrouped)
|
||||
.navigationTitle(Text("Settings", comment: "Navigation bar title for Settings."))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading, content: {
|
||||
Button(action: { dismiss() }, label: { Text("Done", comment: "Button title") })
|
||||
})
|
||||
}
|
||||
.sheet(isPresented: $viewModel.showAddAccountView) {
|
||||
AddAccountListView()
|
||||
}
|
||||
.sheet(isPresented: $viewModel.showHelpSheet) {
|
||||
SafariView(url: viewModel.helpSheet.url)
|
||||
}
|
||||
.sheet(isPresented: $viewModel.showAbout) {
|
||||
AboutView()
|
||||
}
|
||||
.task {
|
||||
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||
Task { await MainActor.run { self.viewModel.notificationPermissions = settings.authorizationStatus }}
|
||||
}
|
||||
}
|
||||
.onChange(of: scenePhase, perform: { phase in
|
||||
if phase == .active {
|
||||
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||
Task { await MainActor.run { self.viewModel.notificationPermissions = settings.authorizationStatus }}
|
||||
}
|
||||
}
|
||||
})
|
||||
.dismissOnExternalContextLaunch()
|
||||
.fileImporter(isPresented: $viewModel.showImportView, allowedContentTypes: OPMLDocument.readableContentTypes) { result in
|
||||
switch result {
|
||||
case .success(let url):
|
||||
if url.startAccessingSecurityScopedResource() {
|
||||
viewModel.importAccount!.importOPML(url) { importResult in
|
||||
switch importResult {
|
||||
case .success(_):
|
||||
viewModel.showImportSuccess = true
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
case .failure(let error):
|
||||
viewModel.importExportError = error
|
||||
viewModel.showImportExportError = true
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
viewModel.importExportError = error
|
||||
viewModel.showImportExportError = true
|
||||
}
|
||||
}
|
||||
.fileExporter(isPresented: $viewModel.showExportView, document: viewModel.exportDocument, contentType: OPMLDocument.writableContentTypes.first!, onCompletion: { result in
|
||||
switch result {
|
||||
case .success(_):
|
||||
viewModel.showExportSuccess = true
|
||||
case .failure(let error):
|
||||
viewModel.importExportError = error
|
||||
viewModel.showImportExportError = true
|
||||
}
|
||||
})
|
||||
.alert(Text("Imported Successfully", comment: "Alert title: imported OPML file successfully."),
|
||||
isPresented: $viewModel.showImportSuccess,
|
||||
actions: {},
|
||||
message: { Text("Subscriptions have been imported to your \(viewModel.importAccount?.nameForDisplay ?? "") account.", comment: "Alert message: imported OPML file successfully.") })
|
||||
.alert(Text("Exported Successfully", comment: "Alert title: exported OPML file successfully."),
|
||||
isPresented: $viewModel.showExportSuccess,
|
||||
actions: {},
|
||||
message: { Text("Your OPML file has been successfully exported.", comment: "Alert message: exported OPML file successfully.") })
|
||||
.alert(Text("Error", comment: "Alert title: Error"),
|
||||
isPresented: $viewModel.showImportExportError,
|
||||
actions: {},
|
||||
message: { Text(viewModel.importExportError?.localizedDescription ?? "Import/Export Error") } )
|
||||
}.navigationViewStyle(.stack)
|
||||
}
|
||||
}
|
||||
34
iOS/Settings/General/SettingsViewModel.swift
Normal file
34
iOS/Settings/General/SettingsViewModel.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// SettingsViewModel.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 29/11/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Account
|
||||
import UniformTypeIdentifiers
|
||||
import UserNotifications
|
||||
|
||||
|
||||
public final class SettingsViewModel: ObservableObject {
|
||||
|
||||
@Published public var showAddAccountView: Bool = false
|
||||
@Published public var helpSheet: HelpSheet = .help
|
||||
@Published public var showHelpSheet: Bool = false
|
||||
@Published public var showAbout: Bool = false
|
||||
@Published public var notificationPermissions: UNAuthorizationStatus = .notDetermined
|
||||
@Published public var importAccount: Account? = nil
|
||||
@Published public var exportAccount: Account? = nil
|
||||
@Published public var showImportView: Bool = false
|
||||
@Published public var showExportView: Bool = false
|
||||
@Published public var showImportActionSheet: Bool = false
|
||||
@Published public var showExportActionSheet: Bool = false
|
||||
@Published public var showImportExportError: Bool = false
|
||||
@Published public var importExportError: Error?
|
||||
@Published public var showImportSuccess: Bool = false
|
||||
@Published public var showExportSuccess: Bool = false
|
||||
@Published public var exportDocument: OPMLDocument?
|
||||
|
||||
}
|
||||
@@ -14,21 +14,21 @@ struct AboutView: View, LoadableAboutData {
|
||||
var body: some View {
|
||||
List {
|
||||
Section(header: aboutHeaderView) {}
|
||||
Section(header: Text("Primary Contributors")) {
|
||||
Section(header: Text("Primary Contributors", comment: "About: Primary Contributors section header")) {
|
||||
ForEach(0..<about.PrimaryContributors.count, id: \.self) { i in
|
||||
contributorView(about.PrimaryContributors[i])
|
||||
}
|
||||
}
|
||||
Section(header: Text("Additional Contributors")) {
|
||||
Section(header: Text("Additional Contributors", comment: "About: Additional Contributors section header")) {
|
||||
ForEach(0..<about.AdditionalContributors.count, id: \.self) { i in
|
||||
contributorView(about.AdditionalContributors[i])
|
||||
}
|
||||
}
|
||||
Section(header: Text("Thanks"), footer: thanks, content: {})
|
||||
Section(header: Text("Thanks", comment: "About: Thanks section header"), footer: thanks, content: {})
|
||||
Section(footer: copyright, content: {})
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.navigationTitle(Text("About"))
|
||||
.navigationTitle(Text("About", comment: "Navigation title: About"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ struct AboutView: View, LoadableAboutData {
|
||||
Image(uiImage: RSImage.appIconImage!)
|
||||
.resizable()
|
||||
.frame(width: 75, height: 75)
|
||||
.cornerRadius(11)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 15, style: .continuous))
|
||||
|
||||
Text(Bundle.main.appName)
|
||||
.font(.headline)
|
||||
@@ -48,7 +48,7 @@ struct AboutView: View, LoadableAboutData {
|
||||
.foregroundColor(.secondary)
|
||||
.font(.callout)
|
||||
|
||||
Text("By Brent Simmons and the Ranchero Software team.")
|
||||
Text("By Brent Simmons and the Ranchero Software team.", comment: "NetNewsWire byline.")
|
||||
.font(.subheadline)
|
||||
|
||||
Text("[netnewswire.com](https://netnewswire.com)")
|
||||
42
iOS/Settings/Help/SettingsHelpSheets.swift
Normal file
42
iOS/Settings/Help/SettingsHelpSheets.swift
Normal file
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// SettingsHelpSheets.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 12/11/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
public enum HelpSheet: CustomStringConvertible, CaseIterable {
|
||||
|
||||
case help, website
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .help:
|
||||
return String(localized: "NetNewsWire Help", comment: "Button: opens NetNewsWire Help page")
|
||||
case .website:
|
||||
return String(localized: "NetNewsWire Website", comment: "Button: opens NetNewsWire website")
|
||||
}
|
||||
}
|
||||
|
||||
public var url: URL {
|
||||
switch self {
|
||||
case .help:
|
||||
return URL(string: "https://netnewswire.com/help/ios/6.1/en/")!
|
||||
case .website:
|
||||
return URL(string: "https://netnewswire.com/")!
|
||||
}
|
||||
}
|
||||
|
||||
public var systemImage: String {
|
||||
switch self {
|
||||
case .help:
|
||||
return "questionmark.circle.fill"
|
||||
case .website:
|
||||
return "safari.fill"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// NewArticleNotificationsView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 29/11/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Account
|
||||
import RSCore
|
||||
|
||||
|
||||
struct NewArticleNotificationsView: View, Logging {
|
||||
|
||||
@State private var activeAccounts = AccountManager.shared.sortedActiveAccounts
|
||||
|
||||
var body: some View {
|
||||
List(activeAccounts, id: \.accountID) { account in
|
||||
Section(header: Text(account.nameForDisplay)) {
|
||||
ForEach(sortedWebFeedsForAccount(account), id: \.webFeedID) { feed in
|
||||
WebFeedToggle(webfeed: feed)
|
||||
.id(feed.webFeedID)
|
||||
}
|
||||
}
|
||||
.navigationTitle(Text("New Article Notifications", comment: "Navigation title: New Article Notifications"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
|
||||
}
|
||||
.tint(Color(uiColor: AppAssets.primaryAccentColor))
|
||||
.onReceive(NotificationCenter.default.publisher(for: .FaviconDidBecomeAvailable), perform: { notification in
|
||||
guard let faviconURLString = notification.userInfo?["faviconURL"] as? String,
|
||||
let faviconHost = URL(string: faviconURLString)?.host else {
|
||||
return
|
||||
}
|
||||
activeAccounts.forEach { account in
|
||||
for feed in Array(account.flattenedWebFeeds()) {
|
||||
if let feedURLHost = URL(string: feed.url)?.host {
|
||||
if faviconHost == feedURLHost {
|
||||
feed.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.onReceive(NotificationCenter.default.publisher(for: .WebFeedIconDidBecomeAvailable), perform: { notification in
|
||||
guard let webFeed = notification.userInfo?[UserInfoKey.webFeed] as? WebFeed else { return }
|
||||
webFeed.objectWillChange.send()
|
||||
})
|
||||
}
|
||||
|
||||
private func sortedWebFeedsForAccount(_ account: Account) -> [WebFeed] {
|
||||
return Array(account.flattenedWebFeeds()).sorted(by: { $0.nameForDisplay.caseInsensitiveCompare($1.nameForDisplay) == .orderedAscending })
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
fileprivate struct WebFeedToggle: View {
|
||||
|
||||
@ObservedObject var webfeed: WebFeed
|
||||
|
||||
var body: some View {
|
||||
Toggle(isOn: Binding(
|
||||
get: { webfeed.isNotifyAboutNewArticles ?? false },
|
||||
set: { webfeed.isNotifyAboutNewArticles = $0 })) {
|
||||
Label {
|
||||
Text(webfeed.nameForDisplay)
|
||||
} icon: {
|
||||
Image(uiImage: IconImageCache.shared.imageFor(webfeed.feedID!)!.image)
|
||||
.resizable()
|
||||
.frame(width: 25, height: 25)
|
||||
.cornerRadius(4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
struct NewArticleNotificationsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NewArticleNotificationsView()
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
//
|
||||
// NotificationsTableViewCell.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 26/01/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Account
|
||||
import UserNotifications
|
||||
|
||||
|
||||
class NotificationsTableViewCell: VibrantBasicTableViewCell {
|
||||
|
||||
@IBOutlet weak var notificationsSwitch: UISwitch!
|
||||
@IBOutlet weak var notificationsLabel: UILabel!
|
||||
@IBOutlet weak var notificationsImageView: UIImageView!
|
||||
weak var feed: WebFeed?
|
||||
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
// Initialization code
|
||||
}
|
||||
|
||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||
super.setSelected(selected, animated: animated)
|
||||
|
||||
// Configure the view for the selected state
|
||||
}
|
||||
|
||||
func configure(_ webFeed: WebFeed) {
|
||||
self.feed = webFeed
|
||||
var isOn = false
|
||||
if webFeed.isNotifyAboutNewArticles == nil {
|
||||
isOn = false
|
||||
} else {
|
||||
isOn = webFeed.isNotifyAboutNewArticles!
|
||||
}
|
||||
notificationsSwitch.isOn = isOn
|
||||
notificationsSwitch.addTarget(self, action: #selector(toggleWebFeedNotification(_:)), for: .touchUpInside)
|
||||
notificationsLabel.text = webFeed.nameForDisplay
|
||||
notificationsImageView.image = IconImageCache.shared.imageFor(webFeed.feedID!)?.image
|
||||
notificationsImageView.layer.cornerRadius = 4
|
||||
}
|
||||
|
||||
@objc
|
||||
private func toggleWebFeedNotification(_ sender: Any) {
|
||||
guard let feed = feed else {
|
||||
return
|
||||
}
|
||||
if feed.isNotifyAboutNewArticles == nil {
|
||||
feed.isNotifyAboutNewArticles = true
|
||||
}
|
||||
else {
|
||||
feed.isNotifyAboutNewArticles!.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
//
|
||||
// NotificationsViewController.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 26/01/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Account
|
||||
import UserNotifications
|
||||
|
||||
class NotificationsViewController: UIViewController {
|
||||
|
||||
@IBOutlet weak var notificationsTableView: UITableView!
|
||||
|
||||
private lazy var searchController: UISearchController = {
|
||||
let searchController = UISearchController(searchResultsController: nil)
|
||||
searchController.searchBar.placeholder = NSLocalizedString("Find a feed", comment: "Find a feed")
|
||||
searchController.searchBar.searchBarStyle = .minimal
|
||||
searchController.delegate = self
|
||||
searchController.searchBar.delegate = self
|
||||
searchController.searchBar.sizeToFit()
|
||||
searchController.obscuresBackgroundDuringPresentation = false
|
||||
searchController.hidesNavigationBarDuringPresentation = false
|
||||
self.definesPresentationContext = true
|
||||
return searchController
|
||||
}()
|
||||
private var status: UNAuthorizationStatus = .notDetermined
|
||||
private var newArticleNotificationFilter: Bool = false {
|
||||
didSet {
|
||||
filterButton.menu = notificationFilterMenu()
|
||||
}
|
||||
}
|
||||
private var filterButton: UIBarButtonItem!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = NSLocalizedString("New Article Notifications", comment: "Notifications")
|
||||
|
||||
navigationItem.searchController = searchController
|
||||
notificationsTableView.isPrefetchingEnabled = false
|
||||
|
||||
filterButton = UIBarButtonItem(
|
||||
title: nil,
|
||||
image: AppAssets.moreImage,
|
||||
primaryAction: nil,
|
||||
menu: notificationFilterMenu())
|
||||
|
||||
navigationItem.rightBarButtonItem = filterButton
|
||||
|
||||
reloadNotificationTableView()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(updateCellsFrom(_:)), name: .WebFeedIconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(reloadVisibleCells(_:)), name: .FaviconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(reloadNotificationTableView(_:)), name: UIScene.willEnterForegroundNotification, object: nil)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func reloadNotificationTableView(_ sender: Any? = nil) {
|
||||
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||
DispatchQueue.main.async {
|
||||
self.status = settings.authorizationStatus
|
||||
if self.status != .authorized {
|
||||
self.filterButton.isEnabled = false
|
||||
self.newArticleNotificationFilter = false
|
||||
}
|
||||
self.notificationsTableView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
private func updateCellsFrom(_ notification: Notification) {
|
||||
guard let webFeed = notification.userInfo?[UserInfoKey.webFeed] as? WebFeed else { return }
|
||||
if let visibleIndexPaths = notificationsTableView.indexPathsForVisibleRows {
|
||||
for path in visibleIndexPaths {
|
||||
if let cell = notificationsTableView.cellForRow(at: path) as? NotificationsTableViewCell {
|
||||
if cell.feed! == webFeed {
|
||||
cell.configure(webFeed)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
private func reloadVisibleCells(_ notification: Notification) {
|
||||
guard let faviconURLString = notification.userInfo?["faviconURL"] as? String,
|
||||
let faviconHost = URL(string: faviconURLString)?.host else {
|
||||
return
|
||||
}
|
||||
|
||||
for cell in notificationsTableView.visibleCells {
|
||||
if let notificationCell = cell as? NotificationsTableViewCell {
|
||||
if let feedURLHost = URL(string: notificationCell.feed!.url)?.host {
|
||||
if faviconHost == feedURLHost {
|
||||
notificationCell.configure(notificationCell.feed!)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func notificationFilterMenu() -> UIMenu {
|
||||
|
||||
if filterButton != nil {
|
||||
if newArticleNotificationFilter {
|
||||
filterButton.image = AppAssets.moreImageFill
|
||||
} else {
|
||||
filterButton.image = AppAssets.moreImage
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let filterMenu = UIMenu(title: "",
|
||||
image: nil,
|
||||
identifier: nil,
|
||||
options: [.displayInline],
|
||||
children: [
|
||||
UIAction(
|
||||
title: NSLocalizedString("Show Feeds with Notifications Enabled", comment: "Feeds with Notifications"),
|
||||
image: nil,
|
||||
identifier: nil,
|
||||
discoverabilityTitle: nil,
|
||||
attributes: [],
|
||||
state: newArticleNotificationFilter ? .on : .off,
|
||||
handler: { [weak self] _ in
|
||||
self?.newArticleNotificationFilter.toggle()
|
||||
self?.notificationsTableView.reloadData()
|
||||
})])
|
||||
|
||||
|
||||
var menus = [UIMenuElement]()
|
||||
menus.append(filterMenu)
|
||||
|
||||
for account in AccountManager.shared.sortedActiveAccounts {
|
||||
let accountMenu = UIMenu(title: account.nameForDisplay, image: nil, identifier: nil, options: .singleSelection, children: [enableAllAction(for: account), disableAllAction(for: account)])
|
||||
menus.append(accountMenu)
|
||||
}
|
||||
|
||||
let combinedMenu = UIMenu(title: "",
|
||||
image: nil,
|
||||
identifier: nil,
|
||||
options: .displayInline,
|
||||
children: menus)
|
||||
|
||||
return combinedMenu
|
||||
}
|
||||
|
||||
private func enableAllAction(for account: Account) -> UIAction {
|
||||
let action = UIAction(title: NSLocalizedString("Enable All Notifications", comment: "Enable All"),
|
||||
image: nil,
|
||||
identifier: nil,
|
||||
discoverabilityTitle: nil,
|
||||
attributes: [],
|
||||
state: .off) { [weak self] _ in
|
||||
for feed in account.flattenedWebFeeds() {
|
||||
feed.isNotifyAboutNewArticles = true
|
||||
}
|
||||
self?.notificationsTableView.reloadData()
|
||||
self?.filterButton.menu = self?.notificationFilterMenu()
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
private func disableAllAction(for account: Account) -> UIAction {
|
||||
let action = UIAction(title: NSLocalizedString("Disable All Notifications", comment: "Disable All"),
|
||||
image: nil,
|
||||
identifier: nil,
|
||||
discoverabilityTitle: nil,
|
||||
attributes: [],
|
||||
state: .off) { [weak self] _ in
|
||||
for feed in account.flattenedWebFeeds() {
|
||||
feed.isNotifyAboutNewArticles = false
|
||||
}
|
||||
self?.notificationsTableView.reloadData()
|
||||
self?.filterButton.menu = self?.notificationFilterMenu()
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
// MARK: - Feed Filtering
|
||||
|
||||
private func sortedWebFeedsForAccount(_ account: Account) -> [WebFeed] {
|
||||
return Array(account.flattenedWebFeeds()).sorted(by: { $0.nameForDisplay.caseInsensitiveCompare($1.nameForDisplay) == .orderedAscending })
|
||||
}
|
||||
|
||||
private func filteredWebFeeds(_ searchText: String? = "", account: Account) -> [WebFeed] {
|
||||
sortedWebFeedsForAccount(account).filter { feed in
|
||||
return feed.nameForDisplay.lowercased().contains(searchText!.lowercased())
|
||||
}
|
||||
}
|
||||
|
||||
private func feedsWithNotificationsEnabled(_ account: Account) -> [WebFeed] {
|
||||
sortedWebFeedsForAccount(account).filter { feed in
|
||||
return feed.isNotifyAboutNewArticles == true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
extension NotificationsViewController: UITableViewDataSource {
|
||||
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
if status == .denied { return 1 }
|
||||
return 1 + AccountManager.shared.activeAccounts.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
if section == 0 {
|
||||
if status == .denied { return 1 }
|
||||
return 0
|
||||
}
|
||||
if searchController.isActive {
|
||||
return filteredWebFeeds(searchController.searchBar.text, account: AccountManager.shared.sortedActiveAccounts[section - 1]).count
|
||||
} else if newArticleNotificationFilter == true {
|
||||
return feedsWithNotificationsEnabled(AccountManager.shared.sortedActiveAccounts[section - 1]).count
|
||||
} else {
|
||||
return AccountManager.shared.sortedActiveAccounts[section - 1].flattenedWebFeeds().count
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
if indexPath.section == 0 {
|
||||
let openSettingsCell = tableView.dequeueReusableCell(withIdentifier: "OpenSettingsCell") as! VibrantBasicTableViewCell
|
||||
return openSettingsCell
|
||||
} else {
|
||||
if searchController.isActive {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "NotificationsCell") as! NotificationsTableViewCell
|
||||
let account = AccountManager.shared.sortedActiveAccounts[indexPath.section - 1]
|
||||
cell.configure(filteredWebFeeds(searchController.searchBar.text, account: account)[indexPath.row])
|
||||
return cell
|
||||
} else if newArticleNotificationFilter == true {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "NotificationsCell") as! NotificationsTableViewCell
|
||||
let account = AccountManager.shared.sortedActiveAccounts[indexPath.section - 1]
|
||||
cell.configure(feedsWithNotificationsEnabled(account)[indexPath.row])
|
||||
return cell
|
||||
} else {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "NotificationsCell") as! NotificationsTableViewCell
|
||||
let account = AccountManager.shared.sortedActiveAccounts[indexPath.section - 1]
|
||||
cell.configure(sortedWebFeedsForAccount(account)[indexPath.row])
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
if section == 0 { return nil }
|
||||
return AccountManager.shared.sortedActiveAccounts[section - 1].nameForDisplay
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
|
||||
if section == 0 {
|
||||
if status == .denied {
|
||||
return NSLocalizedString("Notification permissions are currently denied. Enable notifications in the Settings app.", comment: "Notifications denied.")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
extension NotificationsViewController: UITableViewDelegate {
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
if indexPath.section == 0 {
|
||||
UIApplication.shared.open(URL(string: "\(UIApplication.openSettingsURLString)")!)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// MARK: - UISearchControllerDelegate
|
||||
extension NotificationsViewController: UISearchControllerDelegate {
|
||||
|
||||
func didDismissSearchController(_ searchController: UISearchController) {
|
||||
print(#function)
|
||||
searchController.isActive = false
|
||||
notificationsTableView.reloadData()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UISearchBarDelegate
|
||||
extension NotificationsViewController: UISearchBarDelegate {
|
||||
|
||||
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
|
||||
searchController.isActive = true
|
||||
newArticleNotificationFilter = false
|
||||
notificationsTableView.reloadData()
|
||||
}
|
||||
|
||||
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
|
||||
notificationsTableView.reloadData()
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,30 +0,0 @@
|
||||
//
|
||||
// SettingsAccountTableViewCell.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 10/23/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SettingsComboTableViewCell: VibrantTableViewCell {
|
||||
|
||||
@IBOutlet weak var comboImage: UIImageView!
|
||||
@IBOutlet weak var comboNameLabel: UILabel!
|
||||
|
||||
override func updateVibrancy(animated: Bool) {
|
||||
super.updateVibrancy(animated: animated)
|
||||
updateLabelVibrancy(comboNameLabel, color: labelColor, animated: animated)
|
||||
|
||||
let tintColor = isHighlighted || isSelected ? AppAssets.vibrantTextColor : UIColor.label
|
||||
if animated {
|
||||
UIView.animate(withDuration: Self.duration) {
|
||||
self.comboImage?.tintColor = tintColor
|
||||
}
|
||||
} else {
|
||||
self.comboImage?.tintColor = tintColor
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" id="JCb-QB-CrO" customClass="SettingsComboTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="JCb-QB-CrO" id="FzD-t2-JGy">
|
||||
<rect key="frame" x="0.0" y="0.0" width="383" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="yiw-9t-gil">
|
||||
<rect key="frame" x="12" y="11" width="22" height="22"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="22" id="43E-Em-Z6O"/>
|
||||
<constraint firstAttribute="height" constant="22" id="mTY-cQ-1R1"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="TRx-RV-za8">
|
||||
<rect key="frame" x="42" y="14" width="42" height="16"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="TRx-RV-za8" firstAttribute="leading" secondItem="yiw-9t-gil" secondAttribute="trailing" constant="8" symbolic="YES" id="RUN-Ol-xSl"/>
|
||||
<constraint firstItem="TRx-RV-za8" firstAttribute="top" secondItem="FzD-t2-JGy" secondAttribute="top" constant="14" id="cze-hi-8Uh"/>
|
||||
<constraint firstItem="yiw-9t-gil" firstAttribute="leading" secondItem="FzD-t2-JGy" secondAttribute="leading" constant="12" id="oU9-E3-lEt"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="TRx-RV-za8" secondAttribute="trailing" constant="8" id="sJ6-wr-JIw"/>
|
||||
<constraint firstItem="yiw-9t-gil" firstAttribute="centerY" secondItem="FzD-t2-JGy" secondAttribute="centerY" id="tUD-tI-dgr"/>
|
||||
<constraint firstAttribute="bottom" secondItem="TRx-RV-za8" secondAttribute="bottom" constant="14" id="zls-MW-Ffp"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="comboImage" destination="yiw-9t-gil" id="WqT-gf-Pwq"/>
|
||||
<outlet property="comboNameLabel" destination="TRx-RV-za8" id="CX9-Cp-qZP"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="7" y="-9"/>
|
||||
</tableViewCell>
|
||||
</objects>
|
||||
</document>
|
||||
@@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="SettingsTableViewCell" id="JCb-QB-CrO" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="JCb-QB-CrO" id="FzD-t2-JGy">
|
||||
<rect key="frame" x="0.0" y="0.0" width="385.5" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</tableViewCellContentView>
|
||||
<point key="canvasLocation" x="7" y="-9"/>
|
||||
</tableViewCell>
|
||||
</objects>
|
||||
</document>
|
||||
@@ -1,529 +0,0 @@
|
||||
//
|
||||
// SettingsViewController.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 4/24/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Account
|
||||
import CoreServices
|
||||
import SafariServices
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
import UserNotifications
|
||||
import RSCore
|
||||
|
||||
class SettingsViewController: UITableViewController, Logging {
|
||||
|
||||
private weak var opmlAccount: Account?
|
||||
|
||||
@IBOutlet weak var timelineSortOrderSwitch: UISwitch!
|
||||
@IBOutlet weak var groupByFeedSwitch: UISwitch!
|
||||
@IBOutlet weak var refreshClearsReadArticlesSwitch: UISwitch!
|
||||
@IBOutlet weak var markArticlesAsReadOnScrollSwitch: UISwitch!
|
||||
@IBOutlet weak var articleThemeDetailLabel: UILabel!
|
||||
@IBOutlet weak var confirmMarkAllAsReadSwitch: UISwitch!
|
||||
@IBOutlet weak var colorPaletteDetailLabel: UILabel!
|
||||
@IBOutlet weak var openLinksInNetNewsWire: UISwitch!
|
||||
|
||||
var scrollToArticlesSection = false
|
||||
weak var presentingParentController: UIViewController?
|
||||
|
||||
var notificationStatus: UNAuthorizationStatus = .notDetermined
|
||||
|
||||
override func viewDidLoad() {
|
||||
// This hack mostly works around a bug in static tables with dynamic type. See: https://spin.atomicobject.com/2018/10/15/dynamic-type-static-uitableview/
|
||||
NotificationCenter.default.removeObserver(tableView!, name: UIContentSizeCategory.didChangeNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange), name: .UserDidAddAccount, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange), name: .UserDidDeleteAccount, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange), name: .DisplayNameDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(activeExtensionPointsDidChange), name: .ActiveExtensionPointsDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(refreshNotificationStatus(_:)), name: UIScene.willEnterForegroundNotification, object: nil)
|
||||
|
||||
|
||||
tableView.register(UINib(nibName: "SettingsComboTableViewCell", bundle: nil), forCellReuseIdentifier: "SettingsComboTableViewCell")
|
||||
tableView.register(UINib(nibName: "SettingsTableViewCell", bundle: nil), forCellReuseIdentifier: "SettingsTableViewCell")
|
||||
|
||||
refreshNotificationStatus()
|
||||
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.estimatedRowHeight = 44
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
if AppDefaults.shared.timelineSortDirection == .orderedAscending {
|
||||
timelineSortOrderSwitch.isOn = true
|
||||
} else {
|
||||
timelineSortOrderSwitch.isOn = false
|
||||
}
|
||||
|
||||
if AppDefaults.shared.timelineGroupByFeed {
|
||||
groupByFeedSwitch.isOn = true
|
||||
} else {
|
||||
groupByFeedSwitch.isOn = false
|
||||
}
|
||||
|
||||
if AppDefaults.shared.refreshClearsReadArticles {
|
||||
refreshClearsReadArticlesSwitch.isOn = true
|
||||
} else {
|
||||
refreshClearsReadArticlesSwitch.isOn = false
|
||||
}
|
||||
|
||||
if AppDefaults.shared.markArticlesAsReadOnScroll {
|
||||
markArticlesAsReadOnScrollSwitch.isOn = true
|
||||
} else {
|
||||
markArticlesAsReadOnScrollSwitch.isOn = false
|
||||
}
|
||||
|
||||
articleThemeDetailLabel.text = ArticleThemesManager.shared.currentTheme.name
|
||||
|
||||
if AppDefaults.shared.confirmMarkAllAsRead {
|
||||
confirmMarkAllAsReadSwitch.isOn = true
|
||||
} else {
|
||||
confirmMarkAllAsReadSwitch.isOn = false
|
||||
}
|
||||
|
||||
colorPaletteDetailLabel.text = String(describing: AppDefaults.userInterfaceColorPalette)
|
||||
openLinksInNetNewsWire.isOn = !AppDefaults.shared.useSystemBrowser
|
||||
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
self.tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
|
||||
if scrollToArticlesSection {
|
||||
tableView.scrollToRow(at: IndexPath(row: 0, section: 4), at: .top, animated: true)
|
||||
scrollToArticlesSection = false
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
func refreshNotificationStatus(_ sender: Any? = nil) {
|
||||
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||
DispatchQueue.main.async {
|
||||
self.notificationStatus = settings.authorizationStatus
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UITableView
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
|
||||
switch section {
|
||||
case 0:
|
||||
if notificationStatus == .authorized { return 2 }
|
||||
return 1
|
||||
case 1:
|
||||
return AccountManager.shared.accounts.count + 1
|
||||
case 2:
|
||||
return ExtensionPointManager.shared.activeExtensionPoints.count + 1
|
||||
case 3:
|
||||
let defaultNumberOfRows = super.tableView(tableView, numberOfRowsInSection: section)
|
||||
if AccountManager.shared.activeAccounts.isEmpty || AccountManager.shared.anyAccountHasNetNewsWireNewsSubscription() {
|
||||
return defaultNumberOfRows - 1
|
||||
}
|
||||
return defaultNumberOfRows
|
||||
case 5:
|
||||
return 3
|
||||
default:
|
||||
return super.tableView(tableView, numberOfRowsInSection: section)
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
|
||||
let cell: UITableViewCell
|
||||
switch indexPath.section {
|
||||
case 1:
|
||||
|
||||
let sortedAccounts = AccountManager.shared.sortedAccounts
|
||||
if indexPath.row == sortedAccounts.count {
|
||||
cell = tableView.dequeueReusableCell(withIdentifier: "SettingsTableViewCell", for: indexPath)
|
||||
cell.textLabel?.text = NSLocalizedString("Add Account", comment: "Accounts")
|
||||
} else {
|
||||
let acctCell = tableView.dequeueReusableCell(withIdentifier: "SettingsComboTableViewCell", for: indexPath) as! SettingsComboTableViewCell
|
||||
acctCell.applyThemeProperties()
|
||||
let account = sortedAccounts[indexPath.row]
|
||||
acctCell.comboImage?.image = AppAssets.image(for: account.type)
|
||||
acctCell.comboNameLabel?.text = account.nameForDisplay
|
||||
cell = acctCell
|
||||
}
|
||||
|
||||
case 2:
|
||||
|
||||
let extensionPoints = Array(ExtensionPointManager.shared.activeExtensionPoints.values)
|
||||
if indexPath.row == extensionPoints.count {
|
||||
cell = tableView.dequeueReusableCell(withIdentifier: "SettingsTableViewCell", for: indexPath)
|
||||
cell.textLabel?.text = NSLocalizedString("Add Extension", comment: "Extensions")
|
||||
} else {
|
||||
let acctCell = tableView.dequeueReusableCell(withIdentifier: "SettingsComboTableViewCell", for: indexPath) as! SettingsComboTableViewCell
|
||||
acctCell.applyThemeProperties()
|
||||
let extensionPoint = extensionPoints[indexPath.row]
|
||||
acctCell.comboImage?.image = extensionPoint.image
|
||||
acctCell.comboNameLabel?.text = extensionPoint.title
|
||||
cell = acctCell
|
||||
}
|
||||
default:
|
||||
cell = super.tableView(tableView, cellForRowAt: indexPath)
|
||||
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
|
||||
switch indexPath.section {
|
||||
case 0:
|
||||
if indexPath.row == 0 {
|
||||
UIApplication.shared.open(URL(string: "\(UIApplication.openSettingsURLString)")!)
|
||||
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
} else {
|
||||
let controller = UIStoryboard.settings.instantiateController(ofType: NotificationsViewController.self)
|
||||
self.navigationController?.pushViewController(controller, animated: true)
|
||||
}
|
||||
case 1:
|
||||
let sortedAccounts = AccountManager.shared.sortedAccounts
|
||||
if indexPath.row == sortedAccounts.count {
|
||||
let controller = UIStoryboard.settings.instantiateController(ofType: AddAccountViewController.self)
|
||||
self.navigationController?.pushViewController(controller, animated: true)
|
||||
} else {
|
||||
let controller = UIStoryboard.inspector.instantiateController(ofType: AccountInspectorViewController.self)
|
||||
controller.account = sortedAccounts[indexPath.row]
|
||||
self.navigationController?.pushViewController(controller, animated: true)
|
||||
}
|
||||
case 2:
|
||||
let extensionPoints = Array(ExtensionPointManager.shared.activeExtensionPoints.values)
|
||||
if indexPath.row == extensionPoints.count {
|
||||
let controller = UIStoryboard.settings.instantiateController(ofType: AddExtensionPointViewController.self)
|
||||
self.navigationController?.pushViewController(controller, animated: true)
|
||||
} else {
|
||||
let controller = UIStoryboard.inspector.instantiateController(ofType: ExtensionPointInspectorViewController.self)
|
||||
controller.extensionPoint = extensionPoints[indexPath.row]
|
||||
self.navigationController?.pushViewController(controller, animated: true)
|
||||
}
|
||||
case 3:
|
||||
switch indexPath.row {
|
||||
case 0:
|
||||
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
if let sourceView = tableView.cellForRow(at: indexPath) {
|
||||
let sourceRect = tableView.rectForRow(at: indexPath)
|
||||
importOPML(sourceView: sourceView, sourceRect: sourceRect)
|
||||
}
|
||||
case 1:
|
||||
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
if let sourceView = tableView.cellForRow(at: indexPath) {
|
||||
let sourceRect = tableView.rectForRow(at: indexPath)
|
||||
exportOPML(sourceView: sourceView, sourceRect: sourceRect)
|
||||
}
|
||||
case 2:
|
||||
addFeed()
|
||||
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
default:
|
||||
break
|
||||
}
|
||||
case 4:
|
||||
switch indexPath.row {
|
||||
case 4:
|
||||
let timeline = UIStoryboard.settings.instantiateController(ofType: TimelineCustomizerViewController.self)
|
||||
self.navigationController?.pushViewController(timeline, animated: true)
|
||||
default:
|
||||
break
|
||||
}
|
||||
case 5:
|
||||
switch indexPath.row {
|
||||
case 0:
|
||||
let articleThemes = UIStoryboard.settings.instantiateController(ofType: ArticleThemesTableViewController.self)
|
||||
self.navigationController?.pushViewController(articleThemes, animated: true)
|
||||
default:
|
||||
break
|
||||
}
|
||||
case 6:
|
||||
let colorPalette = UIStoryboard.settings.instantiateController(ofType: ColorPaletteTableViewController.self)
|
||||
self.navigationController?.pushViewController(colorPalette, animated: true)
|
||||
case 7:
|
||||
switch indexPath.row {
|
||||
case 0:
|
||||
openURL("https://netnewswire.com/help/ios/6.1/en/")
|
||||
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
case 1:
|
||||
openURL("https://netnewswire.com/")
|
||||
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
case 2:
|
||||
openURL(URL.releaseNotes.absoluteString)
|
||||
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
case 3:
|
||||
openURL("https://github.com/brentsimmons/NetNewsWire/blob/main/Technotes/HowToSupportNetNewsWire.markdown")
|
||||
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
case 4:
|
||||
openURL("https://github.com/brentsimmons/NetNewsWire")
|
||||
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
case 5:
|
||||
openURL("https://github.com/brentsimmons/NetNewsWire/issues")
|
||||
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
case 6:
|
||||
openURL("https://github.com/brentsimmons/NetNewsWire/tree/main/Technotes")
|
||||
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
case 7:
|
||||
openURL("https://netnewswire.com/slack")
|
||||
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
case 8:
|
||||
let hosting = UIHostingController(rootView: AboutView())
|
||||
self.navigationController?.pushViewController(hosting, animated: true)
|
||||
default:
|
||||
break
|
||||
}
|
||||
default:
|
||||
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
|
||||
return .none
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
return UITableView.automaticDimension
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, indentationLevelForRowAt indexPath: IndexPath) -> Int {
|
||||
return super.tableView(tableView, indentationLevelForRowAt: IndexPath(row: 0, section: 1))
|
||||
}
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
@IBAction func done(_ sender: Any) {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
@IBAction func switchTimelineOrder(_ sender: Any) {
|
||||
if timelineSortOrderSwitch.isOn {
|
||||
AppDefaults.shared.timelineSortDirection = .orderedAscending
|
||||
} else {
|
||||
AppDefaults.shared.timelineSortDirection = .orderedDescending
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func switchGroupByFeed(_ sender: Any) {
|
||||
if groupByFeedSwitch.isOn {
|
||||
AppDefaults.shared.timelineGroupByFeed = true
|
||||
} else {
|
||||
AppDefaults.shared.timelineGroupByFeed = false
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func switchClearsReadArticles(_ sender: Any) {
|
||||
if refreshClearsReadArticlesSwitch.isOn {
|
||||
AppDefaults.shared.refreshClearsReadArticles = true
|
||||
} else {
|
||||
AppDefaults.shared.refreshClearsReadArticles = false
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func switchMarkArticlesAsReadOnScroll(_ sender: Any) {
|
||||
if markArticlesAsReadOnScrollSwitch.isOn {
|
||||
AppDefaults.shared.markArticlesAsReadOnScroll = true
|
||||
} else {
|
||||
AppDefaults.shared.markArticlesAsReadOnScroll = false
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func switchConfirmMarkAllAsRead(_ sender: Any) {
|
||||
if confirmMarkAllAsReadSwitch.isOn {
|
||||
AppDefaults.shared.confirmMarkAllAsRead = true
|
||||
} else {
|
||||
AppDefaults.shared.confirmMarkAllAsRead = false
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func switchBrowserPreference(_ sender: Any) {
|
||||
if openLinksInNetNewsWire.isOn {
|
||||
AppDefaults.shared.useSystemBrowser = false
|
||||
} else {
|
||||
AppDefaults.shared.useSystemBrowser = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
@objc func contentSizeCategoryDidChange() {
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
@objc func accountsDidChange() {
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
@objc func displayNameDidChange() {
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
@objc func activeExtensionPointsDidChange() {
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
@objc func browserPreferenceDidChange() {
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: OPML Document Picker
|
||||
|
||||
extension SettingsViewController: UIDocumentPickerDelegate {
|
||||
|
||||
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||
for url in urls {
|
||||
opmlAccount?.importOPML(url) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure:
|
||||
let title = NSLocalizedString("Import Failed", comment: "Import Failed")
|
||||
let message = NSLocalizedString("We were unable to process the selected file. Please ensure that it is a properly formatted OPML file.", comment: "Import Failed Message")
|
||||
self.presentError(title: title, message: message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private extension SettingsViewController {
|
||||
|
||||
func addFeed() {
|
||||
self.dismiss(animated: true)
|
||||
|
||||
let addNavViewController = UIStoryboard.add.instantiateViewController(withIdentifier: "AddWebFeedViewControllerNav") as! UINavigationController
|
||||
let addViewController = addNavViewController.topViewController as! AddFeedViewController
|
||||
addViewController.initialFeed = AccountManager.netNewsWireNewsURL
|
||||
addViewController.initialFeedName = NSLocalizedString("NetNewsWire News", comment: "NetNewsWire News")
|
||||
addNavViewController.modalPresentationStyle = .formSheet
|
||||
addNavViewController.preferredContentSize = AddFeedViewController.preferredContentSizeForFormSheetDisplay
|
||||
|
||||
presentingParentController?.present(addNavViewController, animated: true)
|
||||
}
|
||||
|
||||
func importOPML(sourceView: UIView, sourceRect: CGRect) {
|
||||
switch AccountManager.shared.activeAccounts.count {
|
||||
case 0:
|
||||
presentError(title: "Error", message: NSLocalizedString("You must have at least one active account.", comment: "Missing active account"))
|
||||
case 1:
|
||||
opmlAccount = AccountManager.shared.activeAccounts.first
|
||||
importOPMLDocumentPicker()
|
||||
default:
|
||||
importOPMLAccountPicker(sourceView: sourceView, sourceRect: sourceRect)
|
||||
}
|
||||
}
|
||||
|
||||
func importOPMLAccountPicker(sourceView: UIView, sourceRect: CGRect) {
|
||||
let title = NSLocalizedString("Choose an account to receive the imported feeds and folders", comment: "Import Account")
|
||||
let alert = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet)
|
||||
|
||||
if let popoverController = alert.popoverPresentationController {
|
||||
popoverController.sourceView = view
|
||||
popoverController.sourceRect = sourceRect
|
||||
}
|
||||
|
||||
for account in AccountManager.shared.sortedActiveAccounts {
|
||||
let action = UIAlertAction(title: account.nameForDisplay, style: .default) { [weak self] action in
|
||||
self?.opmlAccount = account
|
||||
self?.importOPMLDocumentPicker()
|
||||
}
|
||||
alert.addAction(action)
|
||||
}
|
||||
|
||||
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
|
||||
alert.addAction(UIAlertAction(title: cancelTitle, style: .cancel))
|
||||
|
||||
self.present(alert, animated: true)
|
||||
}
|
||||
|
||||
func importOPMLDocumentPicker() {
|
||||
|
||||
let utiArray = UTType.types(tag: "opml", tagClass: .filenameExtension, conformingTo: nil)
|
||||
|
||||
let docPicker = UIDocumentPickerViewController(forOpeningContentTypes: utiArray, asCopy: true)
|
||||
docPicker.delegate = self
|
||||
docPicker.modalPresentationStyle = .formSheet
|
||||
self.present(docPicker, animated: true)
|
||||
}
|
||||
|
||||
func exportOPML(sourceView: UIView, sourceRect: CGRect) {
|
||||
if AccountManager.shared.accounts.count == 1 {
|
||||
opmlAccount = AccountManager.shared.accounts.first!
|
||||
exportOPMLDocumentPicker()
|
||||
} else {
|
||||
exportOPMLAccountPicker(sourceView: sourceView, sourceRect: sourceRect)
|
||||
}
|
||||
}
|
||||
|
||||
func exportOPMLAccountPicker(sourceView: UIView, sourceRect: CGRect) {
|
||||
let title = NSLocalizedString("Choose an account with the subscriptions to export", comment: "Export Account")
|
||||
let alert = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet)
|
||||
|
||||
if let popoverController = alert.popoverPresentationController {
|
||||
popoverController.sourceView = view
|
||||
popoverController.sourceRect = sourceRect
|
||||
}
|
||||
|
||||
for account in AccountManager.shared.sortedAccounts {
|
||||
let action = UIAlertAction(title: account.nameForDisplay, style: .default) { [weak self] action in
|
||||
self?.opmlAccount = account
|
||||
self?.exportOPMLDocumentPicker()
|
||||
}
|
||||
alert.addAction(action)
|
||||
}
|
||||
|
||||
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
|
||||
alert.addAction(UIAlertAction(title: cancelTitle, style: .cancel))
|
||||
|
||||
self.present(alert, animated: true)
|
||||
}
|
||||
|
||||
func exportOPMLDocumentPicker() {
|
||||
guard let account = opmlAccount else { return }
|
||||
|
||||
let accountName = account.nameForDisplay.replacingOccurrences(of: " ", with: "").trimmingCharacters(in: .whitespaces)
|
||||
let filename = "Subscriptions-\(accountName).opml"
|
||||
let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent(filename)
|
||||
let opmlString = OPMLExporter.OPMLString(with: account, title: filename)
|
||||
do {
|
||||
try opmlString.write(to: tempFile, atomically: true, encoding: String.Encoding.utf8)
|
||||
} catch {
|
||||
self.presentError(title: "OPML Export Error", message: error.localizedDescription)
|
||||
logger.error("OPML Export Error: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
|
||||
let docPicker = UIDocumentPickerViewController(forExporting: [tempFile], asCopy: true)
|
||||
docPicker.modalPresentationStyle = .formSheet
|
||||
self.present(docPicker, animated: true)
|
||||
}
|
||||
|
||||
func openURL(_ urlString: String) {
|
||||
let vc = SFSafariViewController(url: URL(string: urlString)!)
|
||||
vc.modalPresentationStyle = .pageSheet
|
||||
present(vc, animated: true)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
//
|
||||
// TimelineCustomizerViewController.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 11/8/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class TimelineCustomizerViewController: UIViewController {
|
||||
|
||||
@IBOutlet weak var iconSizeSliderContainerView: UIView!
|
||||
@IBOutlet weak var iconSizeSlider: TickMarkSlider!
|
||||
@IBOutlet weak var numberOfLinesSliderContainerView: UIView!
|
||||
@IBOutlet weak var numberOfLinesSlider: TickMarkSlider!
|
||||
|
||||
@IBOutlet weak var previewWidthConstraint: NSLayoutConstraint!
|
||||
@IBOutlet weak var previewHeightConstraint: NSLayoutConstraint!
|
||||
|
||||
@IBOutlet weak var previewContainerView: UIView!
|
||||
var previewController: TimelinePreviewTableViewController {
|
||||
return children.first as! TimelinePreviewTableViewController
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
iconSizeSliderContainerView.layer.cornerRadius = 10
|
||||
iconSizeSlider.value = Float(AppDefaults.shared.timelineIconSize.rawValue)
|
||||
iconSizeSlider.addTickMarks()
|
||||
|
||||
numberOfLinesSliderContainerView.layer.cornerRadius = 10
|
||||
numberOfLinesSlider.value = Float(AppDefaults.shared.timelineNumberOfLines)
|
||||
numberOfLinesSlider.addTickMarks()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
updatePreviewBorder()
|
||||
updatePreview()
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
updatePreviewBorder()
|
||||
updatePreview()
|
||||
}
|
||||
|
||||
@IBAction func iconSizeChanged(_ sender: Any) {
|
||||
guard let iconSize = IconSize(rawValue: Int(iconSizeSlider.value.rounded())) else { return }
|
||||
AppDefaults.shared.timelineIconSize = iconSize
|
||||
updatePreview()
|
||||
}
|
||||
|
||||
@IBAction func numberOfLinesChanged(_ sender: Any) {
|
||||
AppDefaults.shared.timelineNumberOfLines = Int(numberOfLinesSlider.value.rounded())
|
||||
updatePreview()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private extension TimelineCustomizerViewController {
|
||||
|
||||
func updatePreview() {
|
||||
let previewWidth: CGFloat = {
|
||||
if traitCollection.userInterfaceIdiom == .phone {
|
||||
return view.bounds.width
|
||||
} else {
|
||||
return view.bounds.width / 1.5
|
||||
}
|
||||
}()
|
||||
|
||||
previewWidthConstraint.constant = previewWidth
|
||||
previewHeightConstraint.constant = previewController.heightFor(width: previewWidth)
|
||||
|
||||
previewController.reload()
|
||||
}
|
||||
|
||||
func updatePreviewBorder() {
|
||||
if traitCollection.userInterfaceStyle == .dark {
|
||||
previewContainerView.layer.borderColor = UIColor.black.cgColor
|
||||
previewContainerView.layer.borderWidth = 1
|
||||
} else {
|
||||
previewContainerView.layer.borderWidth = 0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
//
|
||||
// TimelinePreviewTableViewController.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 11/8/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Articles
|
||||
|
||||
class TimelinePreviewTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
|
||||
|
||||
@IBOutlet weak var tableView: UITableView!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
|
||||
}
|
||||
|
||||
func heightFor(width: CGFloat) -> CGFloat {
|
||||
if UIApplication.shared.preferredContentSizeCategory.isAccessibilityCategory {
|
||||
let layout = MasterTimelineAccessibilityCellLayout(width: width, insets: tableView.safeAreaInsets, cellData: prototypeCellData)
|
||||
return layout.height
|
||||
} else {
|
||||
let layout = MasterTimelineDefaultCellLayout(width: width, insets: tableView.safeAreaInsets, cellData: prototypeCellData)
|
||||
return layout.height
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Table view data source
|
||||
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterTimelineTableViewCell
|
||||
cell.cellData = prototypeCellData
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
}
|
||||
// MARK: API
|
||||
|
||||
func reload() {
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private extension TimelinePreviewTableViewController {
|
||||
|
||||
var prototypeCellData: MasterTimelineCellData {
|
||||
let longTitle = "Enim ut tellus elementum sagittis vitae et. Nibh praesent tristique magna sit amet purus gravida quis blandit. Neque volutpat ac tincidunt vitae semper quis lectus nulla. Massa id neque aliquam vestibulum morbi blandit. Ultrices vitae auctor eu augue. Enim eu turpis egestas pretium aenean pharetra magna. Eget gravida cum sociis natoque. Sit amet consectetur adipiscing elit. Auctor eu augue ut lectus arcu bibendum. Maecenas volutpat blandit aliquam etiam erat velit. Ut pharetra sit amet aliquam id diam maecenas ultricies. In hac habitasse platea dictumst quisque sagittis purus sit amet."
|
||||
|
||||
let prototypeID = "prototype"
|
||||
let status = ArticleStatus(articleID: prototypeID, read: false, starred: false, dateArrived: Date())
|
||||
let prototypeArticle = Article(accountID: prototypeID, articleID: prototypeID, webFeedID: prototypeID, uniqueID: prototypeID, title: longTitle, contentHTML: nil, contentText: nil, url: nil, externalURL: nil, summary: nil, imageURL: nil, datePublished: nil, dateModified: nil, authors: nil, status: status)
|
||||
|
||||
let iconImage = IconImage(AppAssets.faviconTemplateImage.withTintColor(AppAssets.secondaryAccentColor))
|
||||
|
||||
return MasterTimelineCellData(article: prototypeArticle, showFeedName: .feed, feedName: "Feed Name", byline: nil, iconImage: iconImage, showIcon: true, featuredImage: nil, numberOfLines: AppDefaults.shared.timelineNumberOfLines, iconSize: AppDefaults.shared.timelineIconSize, hideSeparator: false)
|
||||
}
|
||||
|
||||
}
|
||||
61
iOS/SwiftUI Extensions/AccountSectionHeader.swift
Normal file
61
iOS/SwiftUI Extensions/AccountSectionHeader.swift
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
34
iOS/SwiftUI Extensions/CustomInsetGroupedRowStyle.swift
Normal file
34
iOS/SwiftUI Extensions/CustomInsetGroupedRowStyle.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// CustomInsetGroupedRowStyle.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 22/12/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CustomInsetGroupedRowStyle: ViewModifier {
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 8)
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 15, bottom: 0, trailing: 15))
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.foregroundColor(Color(uiColor: UIColor.secondarySystemGroupedBackground))
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension View {
|
||||
|
||||
/// This function dismisses a view when the user launches from
|
||||
/// an external action, for example, opening the app from the widget.
|
||||
/// - Returns: `View`
|
||||
func customInsetGroupedRowStyle() -> some View {
|
||||
modifier(CustomInsetGroupedRowStyle())
|
||||
}
|
||||
}
|
||||
30
iOS/SwiftUI Extensions/ExtensionSectionHeader.swift
Normal file
30
iOS/SwiftUI Extensions/ExtensionSectionHeader.swift
Normal file
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// ExtensionSectionHeader.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 19/12/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Account
|
||||
|
||||
struct ExtensionSectionHeader: View {
|
||||
|
||||
var extensionPoint: ExtensionPoint.Type
|
||||
|
||||
var body: some View {
|
||||
Section(header: headerImage) {}
|
||||
}
|
||||
|
||||
var headerImage: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
Image(uiImage: extensionPoint.image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 48, height: 48)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
30
iOS/SwiftUI Extensions/InjectedNavigationView.swift
Normal file
30
iOS/SwiftUI Extensions/InjectedNavigationView.swift
Normal file
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// InjectedNavigationView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 15/12/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct InjectedNavigationView: View {
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
var injectedView: any View
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
AnyView(injectedView)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button(role: .cancel) {
|
||||
dismiss()
|
||||
} label: {
|
||||
Text("Done", comment: "Button title")
|
||||
}
|
||||
}
|
||||
}
|
||||
}.navigationViewStyle(.stack)
|
||||
}
|
||||
}
|
||||
31
iOS/SwiftUI Extensions/View+DismissOnAccountAdd.swift
Normal file
31
iOS/SwiftUI Extensions/View+DismissOnAccountAdd.swift
Normal file
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// 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 {
|
||||
|
||||
/// Convenience modifier to dismiss a view when an account has been added.
|
||||
/// - Returns: `View`
|
||||
func dismissOnAccountAdd() -> some View {
|
||||
modifier(DismissOnAccountAdd())
|
||||
}
|
||||
}
|
||||
33
iOS/SwiftUI Extensions/View+DismissOnExternalContext.swift
Normal file
33
iOS/SwiftUI Extensions/View+DismissOnExternalContext.swift
Normal file
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// 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 {
|
||||
|
||||
/// This function dismisses a view when the user launches from
|
||||
/// an external action, for example, opening the app from the widget.
|
||||
/// - Returns: `View`
|
||||
func dismissOnExternalContextLaunch() -> some View {
|
||||
modifier(DismissOnExternalContext())
|
||||
}
|
||||
}
|
||||
24
iOS/UIKit Extensions/SafariView.swift
Normal file
24
iOS/UIKit Extensions/SafariView.swift
Normal file
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// SafariView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 12/11/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SafariServices
|
||||
|
||||
struct SafariView: UIViewControllerRepresentable {
|
||||
|
||||
let url: URL
|
||||
|
||||
func makeUIViewController(context: UIViewControllerRepresentableContext<SafariView>) -> SFSafariViewController {
|
||||
return SFSafariViewController(url: url)
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: SFSafariViewController, context: UIViewControllerRepresentableContext<SafariView>) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,6 +7,53 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
struct TickMarkSliderView: UIViewRepresentable {
|
||||
|
||||
var minValue: Int
|
||||
var maxValue: Int
|
||||
@Binding var currentValue: Float
|
||||
|
||||
func makeUIView(context: Context) -> TickMarkSlider {
|
||||
let slider = TickMarkSlider()
|
||||
slider.minimumValue = Float(minValue)
|
||||
slider.maximumValue = Float(maxValue)
|
||||
slider.value = currentValue
|
||||
slider.addTickMarks()
|
||||
return slider
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: TickMarkSlider, context: Context) {
|
||||
uiView.addTarget(
|
||||
context.coordinator,
|
||||
action: #selector(Coordinator.valueChanged(_:)),
|
||||
for: .valueChanged
|
||||
)
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(value: $currentValue)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject {
|
||||
var value: Binding<Float>
|
||||
|
||||
init(value: Binding<Float>) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
// Create a valueChanged(_:) action
|
||||
@objc func valueChanged(_ sender: Any) {
|
||||
if let slider = sender as? UISlider {
|
||||
self.value.wrappedValue = Float(slider.value.rounded())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
typealias UIViewType = TickMarkSlider
|
||||
}
|
||||
|
||||
class TickMarkSlider: UISlider {
|
||||
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RSCore
|
||||
|
||||
extension UIStoryboard {
|
||||
extension UIStoryboard: Logging {
|
||||
|
||||
static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 400.0)
|
||||
|
||||
@@ -40,7 +41,7 @@ extension UIStoryboard {
|
||||
|
||||
let storyboardId = String(describing: type)
|
||||
guard let viewController = instantiateViewController(withIdentifier: storyboardId) as? T else {
|
||||
print("Unable to load view with Scene Identifier: \(storyboardId)")
|
||||
logger.error("Unable to load view with Scene Identifier: \(storyboardId)")
|
||||
fatalError()
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user