Merge branch 'extension-point'

This commit is contained in:
Maurice Parker
2020-04-24 13:34:13 -05:00
163 changed files with 6165 additions and 480 deletions

2
.gitignore vendored
View File

@@ -70,6 +70,6 @@ fastlane/screenshots
fastlane/test_output
/Shared/Secrets.swift
/Frameworks/Secrets/Secrets.swift
*.py[cod]

3
.gitmodules vendored
View File

@@ -17,3 +17,6 @@
path = submodules/Sparkle
url = https://github.com/brentsimmons/Sparkle
branch = ui-separation-and-xpc
[submodule "submodules/OAuthSwift"]
path = submodules/OAuthSwift
url = https://github.com/Ranchero-Software/OAuthSwift

View File

@@ -18,6 +18,7 @@ import RSDatabase
import ArticlesDatabase
import RSWeb
import os.log
import Secrets
// Main thread only.
@@ -749,9 +750,9 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
database.update(with: parsedItems, webFeedID: webFeedID) { updateArticlesResult in
switch updateArticlesResult {
case .success(let newAndUpdatedArticles):
self.sendNotificationAbout(newAndUpdatedArticles)
completion(.success(newAndUpdatedArticles))
case .success(let articleChanges):
self.sendNotificationAbout(articleChanges)
completion(.success(articleChanges))
case .failure(let databaseError):
completion(.failure(databaseError))
}

View File

@@ -17,7 +17,6 @@
179DBE829FDF48E102F73244 /* NewsBlurAccountDelegate+Internal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB78C47256A122A281942 /* NewsBlurAccountDelegate+Internal.swift */; };
179DBED55C9B4D6A413486C1 /* NewsBlurStoryHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB818180A51098A9816B2 /* NewsBlurStoryHash.swift */; };
179DBF4DE2562D4C532F6008 /* NewsBlurFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB1B909672E0E807B5E8C /* NewsBlurFeed.swift */; };
3B3A33E7238D3D6800314204 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B3A33E6238D3D6800314204 /* Secrets.swift */; };
3B826DA72385C81C00FC1ADB /* FeedWranglerAuthorizationResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B826D9E2385C81C00FC1ADB /* FeedWranglerAuthorizationResult.swift */; };
3B826DA82385C81C00FC1ADB /* FeedWranglerFeedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B826D9F2385C81C00FC1ADB /* FeedWranglerFeedItem.swift */; };
3B826DA92385C81C00FC1ADB /* FeedWranglerAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B826DA02385C81C00FC1ADB /* FeedWranglerAPICaller.swift */; };
@@ -28,14 +27,20 @@
3B826DAE2385C81C00FC1ADB /* FeedWranglerSubscriptionsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B826DA52385C81C00FC1ADB /* FeedWranglerSubscriptionsRequest.swift */; };
3B826DAF2385C81C00FC1ADB /* FeedWranglerGenericResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B826DA62385C81C00FC1ADB /* FeedWranglerGenericResult.swift */; };
3BC23AB92385ECB100371CBA /* FeedWranglerSubscriptionResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BC23AB82385ECB100371CBA /* FeedWranglerSubscriptionResult.swift */; };
5102FD80244009E000534F17 /* Secrets.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5102FD7F244009E000534F17 /* Secrets.framework */; };
5103A9D92422546800410853 /* CloudKitAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5103A9D82422546800410853 /* CloudKitAccountDelegate.swift */; };
5107A09B227DE49500C7C3C5 /* TestAccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */; };
5107A09D227DE77700C7C3C5 /* TestTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09C227DE77700C7C3C5 /* TestTransport.swift */; };
510BD111232C3801002692E4 /* AccountMetadataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510BD110232C3801002692E4 /* AccountMetadataFile.swift */; };
510BD113232C3E9D002692E4 /* WebFeedMetadataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510BD112232C3E9D002692E4 /* WebFeedMetadataFile.swift */; };
510E3317244E0CED00E7A6AF /* TwitterMedia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510E3316244E0CED00E7A6AF /* TwitterMedia.swift */; };
511B9804237CD4270028BCAA /* FeedIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511B9803237CD4270028BCAA /* FeedIdentifier.swift */; };
512DD4CB2431000600C17B1F /* CKRecord+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 512DD4CA2431000600C17B1F /* CKRecord+Extensions.swift */; };
512DD4CD2431098700C17B1F /* CloudKitAccountZoneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 512DD4CC2431098700C17B1F /* CloudKitAccountZoneDelegate.swift */; };
5132AAC42448BAD90077840A /* FeedProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5132AAC12448BAD90077840A /* FeedProvider.swift */; };
5132AAC52448BAD90077840A /* TwitterFeedProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5132AAC32448BAD90077840A /* TwitterFeedProvider.swift */; };
5132DE812449159100806ADE /* TwitterUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5132DE802449159100806ADE /* TwitterUser.swift */; };
5132DE832449306F00806ADE /* TwitterStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5132DE822449306F00806ADE /* TwitterStatus.swift */; };
513323082281070D00C30F19 /* AccountFeedbinSyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513323072281070C00C30F19 /* AccountFeedbinSyncTest.swift */; };
5133230A2281082F00C30F19 /* subscriptions_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 513323092281082F00C30F19 /* subscriptions_initial.json */; };
5133230C2281088A00C30F19 /* subscriptions_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 5133230B2281088A00C30F19 /* subscriptions_add.json */; };
@@ -44,9 +49,7 @@
514BF5202391B0DB00902FE8 /* SingleArticleFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514BF51F2391B0DB00902FE8 /* SingleArticleFetcher.swift */; };
5150FFFE243823B800C1A442 /* CloudKitError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5150FFFD243823B800C1A442 /* CloudKitError.swift */; };
5154367B228EEB28005E1CDF /* FeedbinImportResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5154367A228EEB28005E1CDF /* FeedbinImportResult.swift */; };
515E4EB52324FF8C0057B0E7 /* CredentialsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515E4EB22324FF8C0057B0E7 /* CredentialsManager.swift */; };
515E4EB62324FF8C0057B0E7 /* URLRequest+RSWeb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515E4EB32324FF8C0057B0E7 /* URLRequest+RSWeb.swift */; };
515E4EB72324FF8C0057B0E7 /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515E4EB42324FF8C0057B0E7 /* Credentials.swift */; };
5165D7122282080C00D9D53D /* AccountFeedbinFolderContentsSyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165D7112282080C00D9D53D /* AccountFeedbinFolderContentsSyncTest.swift */; };
5165D71622821C2400D9D53D /* taggings_delete.json in Resources */ = {isa = PBXBuildFile; fileRef = 5165D71322821C2400D9D53D /* taggings_delete.json */; };
5165D71722821C2400D9D53D /* taggings_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 5165D71422821C2400D9D53D /* taggings_add.json */; };
@@ -55,10 +58,20 @@
5165D72922835F7A00D9D53D /* FeedSpecifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165D71D22835E9800D9D53D /* FeedSpecifier.swift */; };
5165D72A22835F7D00D9D53D /* HTMLFeedFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165D71E22835E9800D9D53D /* HTMLFeedFinder.swift */; };
5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165D73022837F3400D9D53D /* InitialFeedDownloader.swift */; };
516896352448EBEA00185AC5 /* FeedProviderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516896342448EBEA00185AC5 /* FeedProviderManager.swift */; };
5170743C232AEDB500A461A3 /* OPMLFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5170743B232AEDB500A461A3 /* OPMLFile.swift */; };
519E84A62433D49000D238B0 /* OPMLNormalizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519E84A52433D49000D238B0 /* OPMLNormalizer.swift */; };
519E84A82434C5EF00D238B0 /* CloudKitArticlesZone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519E84A72434C5EF00D238B0 /* CloudKitArticlesZone.swift */; };
519E84AC2435019100D238B0 /* CloudKitArticlesZoneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519E84AB2435019100D238B0 /* CloudKitArticlesZoneDelegate.swift */; };
51B36305244B6135000DEF2A /* TwitterEntities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B36304244B6135000DEF2A /* TwitterEntities.swift */; };
51B36307244B6234000DEF2A /* TwitterHashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B36306244B6234000DEF2A /* TwitterHashtag.swift */; };
51B36309244B62A5000DEF2A /* TwitterURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B36308244B62A5000DEF2A /* TwitterURL.swift */; };
51B3630B244B634A000DEF2A /* TwitterMention.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B3630A244B634A000DEF2A /* TwitterMention.swift */; };
51B3630D244B6428000DEF2A /* TwitterSymbol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B3630C244B6428000DEF2A /* TwitterSymbol.swift */; };
51B3630F244B6CB9000DEF2A /* TwitterExtendedEntities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B3630E244B6CB9000DEF2A /* TwitterExtendedEntities.swift */; };
51B36311244B6CFB000DEF2A /* TwitterExtendedMedia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B36310244B6CFA000DEF2A /* TwitterExtendedMedia.swift */; };
51B36313244B8B5E000DEF2A /* TwitterVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B36312244B8B5E000DEF2A /* TwitterVideo.swift */; };
51B36315244BCCA4000DEF2A /* TwitterSearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B36314244BCCA4000DEF2A /* TwitterSearchResult.swift */; };
51BB7B84233531BC008E8144 /* AccountBehaviors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB7B83233531BC008E8144 /* AccountBehaviors.swift */; };
51BC8FCC237EC055004F8B56 /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC8FCB237EC055004F8B56 /* Feed.swift */; };
51BFDECE238B508D00216323 /* ContainerIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BFDECD238B508D00216323 /* ContainerIdentifier.swift */; };
@@ -251,7 +264,6 @@
179DB818180A51098A9816B2 /* NewsBlurStoryHash.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurStoryHash.swift; sourceTree = "<group>"; };
179DBBF346CF712AB2F0E9E6 /* NewsBlurAPICaller+Internal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NewsBlurAPICaller+Internal.swift"; sourceTree = "<group>"; };
179DBDDC00B68411AA28941F /* NewsBlurFolderChange.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurFolderChange.swift; sourceTree = "<group>"; };
3B3A33E6238D3D6800314204 /* Secrets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Secrets.swift; path = ../../Shared/Secrets.swift; sourceTree = "<group>"; };
3B826D9E2385C81C00FC1ADB /* FeedWranglerAuthorizationResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedWranglerAuthorizationResult.swift; sourceTree = "<group>"; };
3B826D9F2385C81C00FC1ADB /* FeedWranglerFeedItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedWranglerFeedItem.swift; sourceTree = "<group>"; };
3B826DA02385C81C00FC1ADB /* FeedWranglerAPICaller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedWranglerAPICaller.swift; sourceTree = "<group>"; };
@@ -262,15 +274,23 @@
3B826DA52385C81C00FC1ADB /* FeedWranglerSubscriptionsRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedWranglerSubscriptionsRequest.swift; sourceTree = "<group>"; };
3B826DA62385C81C00FC1ADB /* FeedWranglerGenericResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedWranglerGenericResult.swift; sourceTree = "<group>"; };
3BC23AB82385ECB100371CBA /* FeedWranglerSubscriptionResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedWranglerSubscriptionResult.swift; sourceTree = "<group>"; };
5102FD7F244009E000534F17 /* Secrets.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Secrets.framework; sourceTree = BUILT_PRODUCTS_DIR; };
5103A9D82422546800410853 /* CloudKitAccountDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitAccountDelegate.swift; sourceTree = "<group>"; };
5107A098227DE42E00C7C3C5 /* AccountCredentialsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCredentialsTest.swift; sourceTree = "<group>"; };
5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAccountManager.swift; sourceTree = "<group>"; };
5107A09C227DE77700C7C3C5 /* TestTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTransport.swift; sourceTree = "<group>"; };
510BD110232C3801002692E4 /* AccountMetadataFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMetadataFile.swift; sourceTree = "<group>"; };
510BD112232C3E9D002692E4 /* WebFeedMetadataFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFeedMetadataFile.swift; sourceTree = "<group>"; };
510E3316244E0CED00E7A6AF /* TwitterMedia.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterMedia.swift; sourceTree = "<group>"; };
511076A3243BD33100D97C8C /* .framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = .framework; sourceTree = BUILT_PRODUCTS_DIR; };
511076F4243BD96D00D97C8C /* FeedProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FeedProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; };
511B9803237CD4270028BCAA /* FeedIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedIdentifier.swift; sourceTree = "<group>"; };
512DD4CA2431000600C17B1F /* CKRecord+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CKRecord+Extensions.swift"; sourceTree = "<group>"; };
512DD4CC2431098700C17B1F /* CloudKitAccountZoneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitAccountZoneDelegate.swift; sourceTree = "<group>"; };
5132AAC12448BAD90077840A /* FeedProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedProvider.swift; sourceTree = "<group>"; };
5132AAC32448BAD90077840A /* TwitterFeedProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TwitterFeedProvider.swift; sourceTree = "<group>"; };
5132DE802449159100806ADE /* TwitterUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterUser.swift; sourceTree = "<group>"; };
5132DE822449306F00806ADE /* TwitterStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterStatus.swift; sourceTree = "<group>"; };
513323072281070C00C30F19 /* AccountFeedbinSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFeedbinSyncTest.swift; sourceTree = "<group>"; };
513323092281082F00C30F19 /* subscriptions_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscriptions_initial.json; sourceTree = "<group>"; };
5133230B2281088A00C30F19 /* subscriptions_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscriptions_add.json; sourceTree = "<group>"; };
@@ -279,9 +299,7 @@
514BF51F2391B0DB00902FE8 /* SingleArticleFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleArticleFetcher.swift; sourceTree = "<group>"; };
5150FFFD243823B800C1A442 /* CloudKitError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudKitError.swift; sourceTree = "<group>"; };
5154367A228EEB28005E1CDF /* FeedbinImportResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinImportResult.swift; sourceTree = "<group>"; };
515E4EB22324FF8C0057B0E7 /* CredentialsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CredentialsManager.swift; sourceTree = "<group>"; };
515E4EB32324FF8C0057B0E7 /* URLRequest+RSWeb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URLRequest+RSWeb.swift"; sourceTree = "<group>"; };
515E4EB42324FF8C0057B0E7 /* Credentials.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Credentials.swift; sourceTree = "<group>"; };
5165D7112282080C00D9D53D /* AccountFeedbinFolderContentsSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFeedbinFolderContentsSyncTest.swift; sourceTree = "<group>"; };
5165D71322821C2400D9D53D /* taggings_delete.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = taggings_delete.json; sourceTree = "<group>"; };
5165D71422821C2400D9D53D /* taggings_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = taggings_add.json; sourceTree = "<group>"; };
@@ -290,11 +308,21 @@
5165D71D22835E9800D9D53D /* FeedSpecifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedSpecifier.swift; sourceTree = "<group>"; };
5165D71E22835E9800D9D53D /* HTMLFeedFinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLFeedFinder.swift; sourceTree = "<group>"; };
5165D73022837F3400D9D53D /* InitialFeedDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InitialFeedDownloader.swift; sourceTree = "<group>"; };
516896342448EBEA00185AC5 /* FeedProviderManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedProviderManager.swift; sourceTree = "<group>"; };
5170743B232AEDB500A461A3 /* OPMLFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPMLFile.swift; sourceTree = "<group>"; };
518B2EA52351306200400001 /* Account_project_test.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_project_test.xcconfig; sourceTree = "<group>"; };
519E84A52433D49000D238B0 /* OPMLNormalizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPMLNormalizer.swift; sourceTree = "<group>"; };
519E84A72434C5EF00D238B0 /* CloudKitArticlesZone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitArticlesZone.swift; sourceTree = "<group>"; };
519E84AB2435019100D238B0 /* CloudKitArticlesZoneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudKitArticlesZoneDelegate.swift; sourceTree = "<group>"; };
51B36304244B6135000DEF2A /* TwitterEntities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterEntities.swift; sourceTree = "<group>"; };
51B36306244B6234000DEF2A /* TwitterHashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterHashtag.swift; sourceTree = "<group>"; };
51B36308244B62A5000DEF2A /* TwitterURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterURL.swift; sourceTree = "<group>"; };
51B3630A244B634A000DEF2A /* TwitterMention.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterMention.swift; sourceTree = "<group>"; };
51B3630C244B6428000DEF2A /* TwitterSymbol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterSymbol.swift; sourceTree = "<group>"; };
51B3630E244B6CB9000DEF2A /* TwitterExtendedEntities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterExtendedEntities.swift; sourceTree = "<group>"; };
51B36310244B6CFA000DEF2A /* TwitterExtendedMedia.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterExtendedMedia.swift; sourceTree = "<group>"; };
51B36312244B8B5E000DEF2A /* TwitterVideo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterVideo.swift; sourceTree = "<group>"; };
51B36314244BCCA4000DEF2A /* TwitterSearchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterSearchResult.swift; sourceTree = "<group>"; };
51BB7B83233531BC008E8144 /* AccountBehaviors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountBehaviors.swift; sourceTree = "<group>"; };
51BC8FCB237EC055004F8B56 /* Feed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = "<group>"; };
51BFDECD238B508D00216323 /* ContainerIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerIdentifier.swift; sourceTree = "<group>"; };
@@ -463,6 +491,7 @@
51E148EC234B8FFC0004F7A5 /* SyncDatabase.framework in Frameworks */,
841973FE1F6DD1BC006346C4 /* RSCore.framework in Frameworks */,
841973FF1F6DD1C5006346C4 /* RSParser.framework in Frameworks */,
5102FD80244009E000534F17 /* Secrets.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -544,14 +573,34 @@
path = Feedbin;
sourceTree = "<group>";
};
515E4EB12324FF7D0057B0E7 /* Credentials */ = {
5132AABB2448BA5B0077840A /* FeedProvider */ = {
isa = PBXGroup;
children = (
515E4EB42324FF8C0057B0E7 /* Credentials.swift */,
515E4EB22324FF8C0057B0E7 /* CredentialsManager.swift */,
515E4EB32324FF8C0057B0E7 /* URLRequest+RSWeb.swift */,
5132AAC12448BAD90077840A /* FeedProvider.swift */,
516896342448EBEA00185AC5 /* FeedProviderManager.swift */,
5132AAC22448BAD90077840A /* Twitter */,
);
path = Credentials;
path = FeedProvider;
sourceTree = "<group>";
};
5132AAC22448BAD90077840A /* Twitter */ = {
isa = PBXGroup;
children = (
51B36304244B6135000DEF2A /* TwitterEntities.swift */,
51B3630E244B6CB9000DEF2A /* TwitterExtendedEntities.swift */,
5132AAC32448BAD90077840A /* TwitterFeedProvider.swift */,
51B36306244B6234000DEF2A /* TwitterHashtag.swift */,
51B36310244B6CFA000DEF2A /* TwitterExtendedMedia.swift */,
51B3630A244B634A000DEF2A /* TwitterMention.swift */,
51B36314244BCCA4000DEF2A /* TwitterSearchResult.swift */,
5132DE822449306F00806ADE /* TwitterStatus.swift */,
51B3630C244B6428000DEF2A /* TwitterSymbol.swift */,
51B36308244B62A5000DEF2A /* TwitterURL.swift */,
5132DE802449159100806ADE /* TwitterUser.swift */,
51B36312244B8B5E000DEF2A /* TwitterVideo.swift */,
510E3316244E0CED00E7A6AF /* TwitterMedia.swift */,
);
path = Twitter;
sourceTree = "<group>";
};
5165D71F22835E9800D9D53D /* FeedFinder */ = {
@@ -657,6 +706,9 @@
8469F80F1F6DC3C10084783E /* Frameworks */ = {
isa = PBXGroup;
children = (
5102FD7F244009E000534F17 /* Secrets.framework */,
511076F4243BD96D00D97C8C /* FeedProvider.framework */,
511076A3243BD33100D97C8C /* .framework */,
51E148EB234B8FFC0004F7A5 /* SyncDatabase.framework */,
84EAC4812148CC6300F154AB /* RSDatabase.framework */,
844B2980210CE3BF004020B3 /* RSWeb.framework */,
@@ -671,7 +723,6 @@
848934EC1F62484F00CEBD24 = {
isa = PBXGroup;
children = (
3B3A33E6238D3D6800314204 /* Secrets.swift */,
848935101F62486800CEBD24 /* Account.swift */,
841974241F6DDCE4006346C4 /* AccountDelegate.swift */,
51BB7B83233531BC008E8144 /* AccountBehaviors.swift */,
@@ -692,23 +743,24 @@
511B9803237CD4270028BCAA /* FeedIdentifier.swift */,
841974001F6DD1EC006346C4 /* Folder.swift */,
844B297E210CE37E004020B3 /* UnreadCountProvider.swift */,
515E4EB32324FF8C0057B0E7 /* URLRequest+RSWeb.swift */,
844B297C2106C7EC004020B3 /* WebFeed.swift */,
84B2D4CE2238C13D00498ADA /* WebFeedMetadata.swift */,
510BD112232C3E9D002692E4 /* WebFeedMetadataFile.swift */,
5165D71F22835E9800D9D53D /* FeedFinder */,
515E4EB12324FF7D0057B0E7 /* Credentials */,
5132AABB2448BA5B0077840A /* FeedProvider */,
8419742B1F6DDE84006346C4 /* LocalAccount */,
84245C7D1FDDD2580074AFBB /* Feedbin */,
3B826D9D2385C81C00FC1ADB /* FeedWrangler */,
552032EA229D5D5A009559E0 /* ReaderAPI */,
9EA31339231E368100268BA0 /* Feedly */,
5103A9D7242253DC00410853 /* CloudKit */,
84245C7D1FDDD2580074AFBB /* Feedbin */,
9EA31339231E368100268BA0 /* Feedly */,
3B826D9D2385C81C00FC1ADB /* FeedWrangler */,
769F2630AF8DC873D4A73567 /* NewsBlur */,
552032EA229D5D5A009559E0 /* ReaderAPI */,
848935031F62484F00CEBD24 /* AccountTests */,
848934F71F62484F00CEBD24 /* Products */,
8469F80F1F6DC3C10084783E /* Frameworks */,
D511EEB4202422BB00712EC3 /* xcconfig */,
848934FA1F62484F00CEBD24 /* Info.plist */,
769F2630AF8DC873D4A73567 /* NewsBlur */,
);
sourceTree = "<group>";
usesTabs = 1;
@@ -1068,7 +1120,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "xcrun -sdk macosx swiftc -target x86_64-macosx10.11 ../../buildscripts/VerifyNoBuildSettings.swift -o $CONFIGURATION_TEMP_DIR/VerifyNoBS\n$CONFIGURATION_TEMP_DIR/VerifyNoBS ${PROJECT_NAME}.xcodeproj/project.pbxproj\n";
shellScript = "xcrun -sdk macosx swiftc -target x86_64-macosx10.11 ../../buildscripts/VerifyNoBuildSettings.swift -o $CONFIGURATION_TEMP_DIR/VerifyNoBS\n$CONFIGURATION_TEMP_DIR/VerifyNoBS ${PROJECT_NAME}.xcodeproj/project.pbxproj\n\nif [ $? -ne 0 ]\nthen\n echo \"error: Build Setting were found in the project.pbxproj file. Most likely you didn't intend to change this file and should revert it.\"\n exit 1\nfi\n";
};
/* End PBXShellScriptBuildPhase section */
@@ -1091,7 +1143,9 @@
9EA643D5239306AC0018A28C /* FeedlyFeedsSearchResponse.swift in Sources */,
9EAEC60E2332FEC20085D7C9 /* FeedlyFeed.swift in Sources */,
5144EA4E227B829A00D19003 /* FeedbinAccountDelegate.swift in Sources */,
5132AAC52448BAD90077840A /* TwitterFeedProvider.swift in Sources */,
519E84AC2435019100D238B0 /* CloudKitArticlesZoneDelegate.swift in Sources */,
51B3630B244B634A000DEF2A /* TwitterMention.swift in Sources */,
512DD4CB2431000600C17B1F /* CKRecord+Extensions.swift in Sources */,
3B826DAF2385C81C00FC1ADB /* FeedWranglerGenericResult.swift in Sources */,
9ECC9A85234DC16E009B5144 /* FeedlyAccountDelegateError.swift in Sources */,
@@ -1100,16 +1154,20 @@
846E77451F6EF9B900A165E2 /* Container.swift in Sources */,
9EA643D3239305680018A28C /* FeedlySearchOperation.swift in Sources */,
5150FFFE243823B800C1A442 /* CloudKitError.swift in Sources */,
516896352448EBEA00185AC5 /* FeedProviderManager.swift in Sources */,
9E5EC15D23E0D58500A4E503 /* FeedlyFeedParser.swift in Sources */,
9E1D15532334304B00F4944C /* FeedlyGetStreamContentsOperation.swift in Sources */,
9E12B0202334696A00ADE5A0 /* FeedlyCreateFeedsForCollectionFoldersOperation.swift in Sources */,
552032FD229D5D5A009559E0 /* ReaderAPITagging.swift in Sources */,
9EAEC62A23331EE70085D7C9 /* FeedlyOrigin.swift in Sources */,
9E5EC15B23E01DEF00A4E503 /* FeedlyRTLTextSanitizer.swift in Sources */,
51B36305244B6135000DEF2A /* TwitterEntities.swift in Sources */,
5132DE832449306F00806ADE /* TwitterStatus.swift in Sources */,
511B9804237CD4270028BCAA /* FeedIdentifier.swift in Sources */,
84F73CF1202788D90000BCEF /* ArticleFetcher.swift in Sources */,
841974251F6DDCE4006346C4 /* AccountDelegate.swift in Sources */,
510BD113232C3E9D002692E4 /* WebFeedMetadataFile.swift in Sources */,
51B36309244B62A5000DEF2A /* TwitterURL.swift in Sources */,
5103A9D92422546800410853 /* CloudKitAccountDelegate.swift in Sources */,
5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */,
9E784EBE237E890600099B1B /* FeedlyLogoutOperation.swift in Sources */,
@@ -1117,7 +1175,6 @@
9EEEF71F23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift in Sources */,
9EBD49C223C67784005AD5CD /* FeedlyEntryIdentifierProviding.swift in Sources */,
846E77541F6F00E300A165E2 /* AccountManager.swift in Sources */,
515E4EB72324FF8C0057B0E7 /* Credentials.swift in Sources */,
51E490362288C37100C791F0 /* FeedbinDate.swift in Sources */,
9EEAE06E235D002D00E3FEE4 /* FeedlyGetCollectionsService.swift in Sources */,
5165D72922835F7A00D9D53D /* FeedSpecifier.swift in Sources */,
@@ -1135,6 +1192,7 @@
9E1773D7234575AB0056A5A8 /* FeedlyTag.swift in Sources */,
3B826DAB2385C81C00FC1ADB /* FeedWranglerConfig.swift in Sources */,
515E4EB62324FF8C0057B0E7 /* URLRequest+RSWeb.swift in Sources */,
51B36315244BCCA4000DEF2A /* TwitterSearchResult.swift in Sources */,
9EB1D576238E6A3900A753D7 /* FeedlyAddNewFeedOperation.swift in Sources */,
3B826DA82385C81C00FC1ADB /* FeedWranglerFeedItem.swift in Sources */,
9E672396236F7E68000BE141 /* OAuthAcessTokenRefreshing.swift in Sources */,
@@ -1149,21 +1207,25 @@
9EEEF7212355277F009E9D80 /* FeedlyIngestStarredArticleIdsOperation.swift in Sources */,
3BC23AB92385ECB100371CBA /* FeedWranglerSubscriptionResult.swift in Sources */,
5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */,
51B3630F244B6CB9000DEF2A /* TwitterExtendedEntities.swift in Sources */,
84B99C9F1FAE8D3200ECDEDB /* ContainerPath.swift in Sources */,
51BC8FCC237EC055004F8B56 /* Feed.swift in Sources */,
846E77501F6EF9C400A165E2 /* LocalAccountRefresher.swift in Sources */,
9EA643CF2391D3560018A28C /* FeedlyAddExistingFeedOperation.swift in Sources */,
55203300229D5D5A009559E0 /* ReaderAPICaller.swift in Sources */,
5132AAC42448BAD90077840A /* FeedProvider.swift in Sources */,
9E1D154F233371DD00F4944C /* FeedlyGetCollectionsOperation.swift in Sources */,
9EAEC626233318400085D7C9 /* FeedlyStream.swift in Sources */,
9E5DE60E23C3F4B70064DA30 /* FeedlyFetchIdsForMissingArticlesOperation.swift in Sources */,
3B826DA92385C81C00FC1ADB /* FeedWranglerAPICaller.swift in Sources */,
9EAEC60C2332FE830085D7C9 /* FeedlyCollection.swift in Sources */,
51B36307244B6234000DEF2A /* TwitterHashtag.swift in Sources */,
51E3EB41229AF61B00645299 /* AccountError.swift in Sources */,
9E1D155D233447F000F4944C /* FeedlyUpdateAccountFeedsWithItemsOperation.swift in Sources */,
51E59599228C77BC00FCC42B /* FeedbinUnreadEntry.swift in Sources */,
552032F8229D5D5A009559E0 /* ReaderAPIEntry.swift in Sources */,
552032FB229D5D5A009559E0 /* ReaderAPITag.swift in Sources */,
51B36313244B8B5E000DEF2A /* TwitterVideo.swift in Sources */,
5165D72822835F7800D9D53D /* FeedFinder.swift in Sources */,
9EBD49C023C67602005AD5CD /* FeedlyDownloadArticlesOperation.swift in Sources */,
51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */,
@@ -1180,7 +1242,6 @@
9EAEC62823331C350085D7C9 /* FeedlyCategory.swift in Sources */,
3B826DAE2385C81C00FC1ADB /* FeedWranglerSubscriptionsRequest.swift in Sources */,
9E964EB823754AC400A7AF2E /* OAuthAuthorizationClient+Feedly.swift in Sources */,
3B3A33E7238D3D6800314204 /* Secrets.swift in Sources */,
9EF1B10923590E93000A486A /* FeedlyStreamIds.swift in Sources */,
84D09623217418DC00D77525 /* FeedbinTagging.swift in Sources */,
84CAD7161FDF2E22000F0755 /* FeedbinEntry.swift in Sources */,
@@ -1190,7 +1251,6 @@
51E4DB302426353D0091EB5B /* CloudKitAccountZone.swift in Sources */,
3B826DAD2385C81C00FC1ADB /* FeedWranglerFeedItemsRequest.swift in Sources */,
846E774F1F6EF9C000A165E2 /* LocalAccountDelegate.swift in Sources */,
515E4EB52324FF8C0057B0E7 /* CredentialsManager.swift in Sources */,
844B297F210CE37E004020B3 /* UnreadCountProvider.swift in Sources */,
9E1773D5234570E30056A5A8 /* FeedlyEntryParser.swift in Sources */,
512DD4CD2431098700C17B1F /* CloudKitAccountZoneDelegate.swift in Sources */,
@@ -1199,12 +1259,16 @@
84F1F06E2243524700DA0616 /* AccountMetadata.swift in Sources */,
9EF1B10723590D61000A486A /* FeedlyGetStreamIdsOperation.swift in Sources */,
84245C851FDDD8CB0074AFBB /* FeedbinSubscription.swift in Sources */,
510E3317244E0CED00E7A6AF /* TwitterMedia.swift in Sources */,
9EF2602C23C91FFE006D160C /* FeedlyGetUpdatedArticleIdsOperation.swift in Sources */,
3B826DAA2385C81C00FC1ADB /* FeedWranglerSubscription.swift in Sources */,
5132DE812449159100806ADE /* TwitterUser.swift in Sources */,
3B826DAC2385C81C00FC1ADB /* FeedWranglerAccountDelegate.swift in Sources */,
769F295938E5A30D03DFF88F /* NewsBlurAccountDelegate.swift in Sources */,
51B3630D244B6428000DEF2A /* TwitterSymbol.swift in Sources */,
769F2BA02EF5F329CDE45F5A /* NewsBlurAPICaller.swift in Sources */,
51C034DF242D65D20014DC71 /* CloudKitZoneResult.swift in Sources */,
51B36311244B6CFB000DEF2A /* TwitterExtendedMedia.swift in Sources */,
179DB28CF49F73A945EBF5DB /* NewsBlurLoginResponse.swift in Sources */,
179DBF4DE2562D4C532F6008 /* NewsBlurFeed.swift in Sources */,
179DB02FFBC17AC9798F0EBC /* NewsBlurStory.swift in Sources */,

View File

@@ -9,6 +9,7 @@
import Foundation
import Articles
import RSWeb
import Secrets
protocol AccountDelegate {

View File

@@ -16,6 +16,7 @@ import RSParser
import Articles
import ArticlesDatabase
import RSWeb
import Secrets
public enum CloudKitAccountDelegateError: String, Error {
case invalidParameter = "An invalid parameter was used."
@@ -211,72 +212,20 @@ final class CloudKitAccountDelegate: AccountDelegate {
}
func createWebFeed(for account: Account, url urlString: String, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void) {
let editedName = name == nil || name!.isEmpty ? nil : name
guard let url = URL(string: urlString) else {
guard let url = URL(string: urlString), let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
completion(.failure(LocalAccountDelegateError.invalidParameter))
return
}
BatchUpdate.shared.start()
refreshProgress.addToNumberOfTasksAndRemaining(3)
FeedFinder.find(url: url) { result in
self.refreshProgress.completeTask()
switch result {
case .success(let feedSpecifiers):
guard let bestFeedSpecifier = FeedSpecifier.bestFeed(in: feedSpecifiers), let url = URL(string: bestFeedSpecifier.urlString) else {
BatchUpdate.shared.end()
self.refreshProgress.clear()
completion(.failure(AccountError.createErrorNotFound))
return
}
if account.hasWebFeed(withURL: bestFeedSpecifier.urlString) {
BatchUpdate.shared.end()
self.refreshProgress.clear()
completion(.failure(AccountError.createErrorAlreadySubscribed))
return
}
self.accountZone.createWebFeed(url: bestFeedSpecifier.urlString, editedName: editedName, container: container) { result in
let editedName = name == nil || name!.isEmpty ? nil : name
self.refreshProgress.completeTask()
switch result {
case .success(let externalID):
let feed = account.createWebFeed(with: nil, url: url.absoluteString, webFeedID: url.absoluteString, homePageURL: nil)
feed.editedName = editedName
feed.externalID = externalID
container.addWebFeed(feed)
InitialFeedDownloader.download(url) { parsedFeed in
self.refreshProgress.completeTask()
if let parsedFeed = parsedFeed {
account.update(feed, with: parsedFeed, {_ in
BatchUpdate.shared.end()
completion(.success(feed))
})
}
}
case .failure(let error):
BatchUpdate.shared.end()
self.refreshProgress.clear()
completion(.failure(error))
}
}
case .failure:
BatchUpdate.shared.end()
self.refreshProgress.clear()
completion(.failure(AccountError.createErrorNotFound))
}
// Username should be part of the URL on new feed adds
if let feedProvider = FeedProviderManager.shared.best(for: urlComponents) {
createProviderWebFeed(for: account, urlComponents: urlComponents, editedName: editedName, container: container, feedProvider: feedProvider, completion: completion)
} else {
createRSSWebFeed(for: account, url: url, editedName: editedName, container: container, completion: completion)
}
}
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
@@ -545,7 +494,7 @@ private extension CloudKitAccountDelegate {
self.sendArticleStatus(for: account) { result in
switch result {
case .success:
self.refreshProgress.completeTask()
self.refreshProgress.clear()
completion(.success(()))
case .failure(let error):
fail(error)
@@ -593,7 +542,8 @@ private extension CloudKitAccountDelegate {
self.refreshProgress.completeTask()
self.refresher.refreshFeeds(webFeeds) {
self.refreshWebFeeds(account, webFeeds) {
self.refreshProgress.clear()
account.metadata.lastArticleFetchEndTime = Date()
}
@@ -614,6 +564,224 @@ private extension CloudKitAccountDelegate {
}
}
func refreshWebFeeds(_ account: Account, _ webFeeds: Set<WebFeed>, completion: @escaping () -> Void) {
var newArticles = Set<Article>()
var deletedArticles = Set<Article>()
var refresherWebFeeds = Set<WebFeed>()
let group = DispatchGroup()
refreshProgress.addToNumberOfTasksAndRemaining(2)
for webFeed in webFeeds {
if let components = URLComponents(string: webFeed.url), let feedProvider = FeedProviderManager.shared.best(for: components) {
group.enter()
feedProvider.refresh(webFeed) { result in
switch result {
case .success(let parsedItems):
account.update(webFeed.webFeedID, with: parsedItems) { result in
switch result {
case .success(let articleChanges):
newArticles.formUnion(articleChanges.newArticles ?? Set<Article>())
deletedArticles.formUnion(articleChanges.deletedArticles ?? Set<Article>())
self.refreshProgress.completeTask()
group.leave()
case .failure(let error):
os_log(.error, log: self.log, "Feed Provider refresh update error: %@.", error.localizedDescription)
self.refreshProgress.completeTask()
group.leave()
}
}
case .failure(let error):
os_log(.error, log: self.log, "Feed Provider refresh error: %@.", error.localizedDescription)
self.refreshProgress.completeTask()
group.leave()
}
}
} else {
refresherWebFeeds.insert(webFeed)
}
}
group.enter()
refresher.refreshFeeds(refresherWebFeeds) {
group.leave()
}
group.notify(queue: DispatchQueue.main) {
self.articlesZone.deleteArticles(deletedArticles) { _ in
self.refreshProgress.completeTask()
self.articlesZone.sendNewArticles(newArticles) { _ in
self.refreshProgress.completeTask()
completion()
}
}
}
}
func createProviderWebFeed(for account: Account, urlComponents: URLComponents, editedName: String?, container: Container, feedProvider: FeedProvider, completion: @escaping (Result<WebFeed, Error>) -> Void) {
refreshProgress.addToNumberOfTasksAndRemaining(5)
feedProvider.assignName(urlComponents) { result in
self.refreshProgress.completeTask()
switch result {
case .success(let name):
guard let urlString = urlComponents.url?.absoluteString else {
completion(.failure(AccountError.createErrorNotFound))
return
}
self.accountZone.createWebFeed(url: urlString, editedName: editedName, container: container) { result in
self.refreshProgress.completeTask()
switch result {
case .success(let externalID):
let feed = account.createWebFeed(with: name, url: urlString, webFeedID: urlString, homePageURL: nil)
feed.editedName = editedName
feed.externalID = externalID
container.addWebFeed(feed)
feedProvider.refresh(feed) { result in
self.refreshProgress.completeTask()
switch result {
case .success(let parsedItems):
account.update(urlString, with: parsedItems) { result in
switch result {
case .success(let articleChanges):
let newArticles = articleChanges.newArticles ?? Set<Article>()
let deletedArticles = articleChanges.deletedArticles ?? Set<Article>()
self.articlesZone.deleteArticles(deletedArticles) { _ in
self.refreshProgress.completeTask()
self.articlesZone.sendNewArticles(newArticles) { _ in
self.refreshProgress.clear()
completion(.success(feed))
}
}
case .failure(let error):
self.refreshProgress.clear()
completion(.failure(error))
}
}
case .failure:
self.refreshProgress.clear()
completion(.failure(AccountError.createErrorNotFound))
}
}
case .failure(let error):
self.refreshProgress.clear()
completion(.failure(error))
}
}
case .failure(let error):
self.refreshProgress.clear()
completion(.failure(error))
}
}
}
func createRSSWebFeed(for account: Account, url: URL, editedName: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void) {
BatchUpdate.shared.start()
refreshProgress.addToNumberOfTasksAndRemaining(5)
FeedFinder.find(url: url) { result in
self.refreshProgress.completeTask()
switch result {
case .success(let feedSpecifiers):
guard let bestFeedSpecifier = FeedSpecifier.bestFeed(in: feedSpecifiers), let url = URL(string: bestFeedSpecifier.urlString) else {
BatchUpdate.shared.end()
self.refreshProgress.clear()
completion(.failure(AccountError.createErrorNotFound))
return
}
if account.hasWebFeed(withURL: bestFeedSpecifier.urlString) {
BatchUpdate.shared.end()
self.refreshProgress.clear()
completion(.failure(AccountError.createErrorAlreadySubscribed))
return
}
self.accountZone.createWebFeed(url: bestFeedSpecifier.urlString, editedName: editedName, container: container) { result in
self.refreshProgress.completeTask()
switch result {
case .success(let externalID):
let feed = account.createWebFeed(with: nil, url: url.absoluteString, webFeedID: url.absoluteString, homePageURL: nil)
feed.editedName = editedName
feed.externalID = externalID
container.addWebFeed(feed)
InitialFeedDownloader.download(url) { parsedFeed in
self.refreshProgress.completeTask()
if let parsedFeed = parsedFeed {
account.update(feed, with: parsedFeed) { result in
switch result {
case .success(let articleChanges):
BatchUpdate.shared.end()
let newArticles = articleChanges.newArticles ?? Set<Article>()
let deletedArticles = articleChanges.deletedArticles ?? Set<Article>()
self.articlesZone.deleteArticles(deletedArticles) { _ in
self.refreshProgress.completeTask()
self.articlesZone.sendNewArticles(newArticles) { _ in
self.refreshProgress.clear()
completion(.success(feed))
}
}
case .failure(let error):
self.refreshProgress.clear()
completion(.failure(error))
}
}
} else {
self.refreshProgress.clear()
completion(.success(feed))
}
}
case .failure(let error):
BatchUpdate.shared.end()
self.refreshProgress.clear()
completion(.failure(error))
}
}
case .failure:
BatchUpdate.shared.end()
self.refreshProgress.clear()
completion(.failure(AccountError.createErrorNotFound))
}
}
}
func processAccountError(_ account: Account, _ error: Error) {
if case CloudKitZoneError.userDeletedZone = error {
account.removeFeeds(account.topLevelWebFeeds)
@@ -643,7 +811,6 @@ extension CloudKitAccountDelegate: LocalAccountRefresherDelegate {
}
func localAccountRefresherDidFinish(_ refresher: LocalAccountRefresher) {
refreshProgress.clear()
}
}

View File

@@ -72,7 +72,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
}
let editedName = record[CloudKitAccountZone.CloudKitWebFeed.Fields.editedName] as? String
if let webFeed = account.existingWebFeed(withExternalID: record.externalID) {
updateWebFeed(webFeed, editedName: editedName, containerExternalIDs: containerExternalIDs)
@@ -180,28 +180,60 @@ private extension CloudKitAcountZoneDelegate {
}
func createWebFeedIfNecessary(url: URL, editedName: String?, webFeedExternalID: String, container: Container, completion: @escaping (WebFeed) -> Void) {
guard let account = account else { return }
guard let account = account, let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return }
if let webFeed = account.existingWebFeed(withExternalID: webFeedExternalID) {
completion(webFeed)
return
}
let webFeed = account.createWebFeed(with: editedName, url: url.absoluteString, webFeedID: url.absoluteString, homePageURL: nil)
let webFeed = account.createWebFeed(with: nil, url: url.absoluteString, webFeedID: url.absoluteString, homePageURL: nil)
webFeed.editedName = editedName
webFeed.externalID = webFeedExternalID
container.addWebFeed(webFeed)
refreshProgress?.addToNumberOfTasksAndRemaining(1)
InitialFeedDownloader.download(url) { parsedFeed in
self.refreshProgress?.completeTask()
if let parsedFeed = parsedFeed {
account.update(webFeed, with: parsedFeed, { _ in
if let feedProvider = FeedProviderManager.shared.best(for: urlComponents) {
refreshProgress?.addToNumberOfTasksAndRemaining(2)
feedProvider.assignName(urlComponents) { result in
self.refreshProgress?.completeTask()
switch result {
case .success(let name):
webFeed.name = name
container.addWebFeed(webFeed)
feedProvider.refresh(webFeed) { result in
self.refreshProgress?.completeTask()
switch result {
case .success(let parsedItems):
account.update(url.absoluteString, with: parsedItems) { _ in
completion(webFeed)
}
case .failure:
completion(webFeed)
}
}
case .failure:
completion(webFeed)
})
} else {
completion(webFeed)
}
}
} else {
refreshProgress?.addToNumberOfTasksAndRemaining(1)
InitialFeedDownloader.download(url) { parsedFeed in
self.refreshProgress?.completeTask()
if let parsedFeed = parsedFeed {
account.update(webFeed, with: parsedFeed, { _ in
container.addWebFeed(webFeed)
completion(webFeed)
})
} else {
completion(webFeed)
}
}
}
}

View File

@@ -0,0 +1,33 @@
//
// FeedProvider.swift
// FeedProvider
//
// Created by Maurice Parker on 4/6/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSCore
import RSParser
public enum FeedProviderAbility {
case owner
case available
case none
}
public protocol FeedProvider {
/// Informs the caller of the ability for this feed provider to service the given URL
func ability(_ urlComponents: URLComponents) -> FeedProviderAbility
/// Provide the iconURL of the given URL
func iconURL(_ urlComponents: URLComponents, completion: @escaping (Result<String, Error>) -> Void)
/// Construct a Name for the new feed
func assignName(_ urlComponents: URLComponents, completion: @escaping (Result<String, Error>) -> Void)
/// Refresh all the article entries (ParsedItems)
func refresh(_ webFeed: WebFeed, completion: @escaping (Result<Set<ParsedItem>, Error>) -> Void)
}

View File

@@ -0,0 +1,42 @@
//
// FeedProviderManager.swift
// Account
//
// Created by Maurice Parker on 4/16/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public protocol FeedProviderManagerDelegate: class {
var activeFeedProviders: [FeedProvider] { get }
}
public final class FeedProviderManager {
public static let shared = FeedProviderManager()
public weak var delegate: FeedProviderManagerDelegate?
public func best(for offered: URLComponents) -> FeedProvider? {
if let owner = feedProviderMatching(offered, ability: .owner) {
return owner
}
return feedProviderMatching(offered, ability: .available)
}
}
private extension FeedProviderManager {
func feedProviderMatching(_ offered: URLComponents, ability: FeedProviderAbility) -> FeedProvider? {
if let delegate = delegate {
for feedProvider in delegate.activeFeedProviders {
if feedProvider.ability(offered) == ability {
return feedProvider
}
}
}
return nil
}
}

View File

@@ -0,0 +1,70 @@
//
// TwitterEntities.swift
// Account
//
// Created by Maurice Parker on 4/18/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
protocol TwitterEntity {
var indices: [Int]? { get }
func renderAsHTML() -> String
}
extension TwitterEntity {
var startIndex: Int {
if let indices = indices, indices.count > 0 {
return indices[0]
}
return 0
}
var endIndex: Int {
if let indices = indices, indices.count > 1 {
return indices[1]
}
return 0
}
}
struct TwitterEntities: Codable {
let hashtags: [TwitterHashtag]?
let urls: [TwitterURL]?
let userMentions: [TwitterMention]?
let symbols: [TwitterSymbol]?
let media: [TwitterMedia]?
enum CodingKeys: String, CodingKey {
case hashtags = "hashtags"
case urls = "urls"
case userMentions = "user_mentions"
case symbols = "symbols"
case media = "media"
}
func combineAndSort() -> [TwitterEntity] {
var entities = [TwitterEntity]()
if let hashtags = hashtags {
entities.append(contentsOf: hashtags)
}
if let urls = urls {
entities.append(contentsOf: urls)
}
if let userMentions = userMentions {
entities.append(contentsOf: userMentions)
}
if let symbols = symbols {
entities.append(contentsOf: symbols)
}
if let media = media {
entities.append(contentsOf: media)
}
return entities.sorted(by: { $0.startIndex < $1.startIndex })
}
}

View File

@@ -0,0 +1,28 @@
//
// TwitterExtendedEntities.swift
// Account
//
// Created by Maurice Parker on 4/18/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
struct TwitterExtendedEntities: Codable {
let medias: [TwitterExtendedMedia]?
enum CodingKeys: String, CodingKey {
case medias = "media"
}
func renderAsHTML() -> String {
var html = String()
if let medias = medias {
for media in medias {
html += media.renderAsHTML()
}
}
return html
}
}

View File

@@ -0,0 +1,94 @@
//
// TwitterExtendedMedia.swift
// Account
//
// Created by Maurice Parker on 4/18/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
struct TwitterExtendedMedia: Codable {
let idStr: String?
let indices: [Int]?
let mediaURL: String?
let httpsMediaURL: String?
let url: String?
let displayURL: String?
let type: String?
let video: TwitterVideo?
enum CodingKeys: String, CodingKey {
case idStr = "idStr"
case indices = "indices"
case mediaURL = "media_url"
case httpsMediaURL = "media_url_https"
case url = "url"
case displayURL = "display_url"
case type = "type"
case video = "video_info"
}
func renderAsHTML() -> String {
var html = String()
switch type {
case "photo":
html += renderPhotoAsHTML()
case "video", "animated_gif":
html += renderVideoAsHTML()
default:
break
}
return html
}
}
private extension TwitterExtendedMedia {
func renderPhotoAsHTML() -> String {
if let httpsMediaURL = httpsMediaURL {
return "<figure><img src=\"\(httpsMediaURL)\"></figure>"
}
if let mediaURL = mediaURL {
return "<figure><img src=\"\(mediaURL)\"></figure>"
}
return ""
}
func renderVideoAsHTML() -> String {
guard let bestVariantURL = findBestVariant()?.url else { return "" }
var html = "<video "
if let httpsMediaURL = httpsMediaURL {
html += "poster=\"\(httpsMediaURL)\" "
} else if let mediaURL = mediaURL {
html += "poster=\"\(mediaURL)\" "
}
html += "src=\"\(bestVariantURL)\"></video>"
return html
}
func findBestVariant() -> TwitterVideo.Variant? {
var best: TwitterVideo.Variant? = nil
if let variants = video?.variants {
for variant in variants {
if let currentBest = best {
if variant.bitrate ?? 0 > currentBest.bitrate ?? 0 {
best = variant
}
} else {
best = variant
}
}
}
return best
}
// <video poster="https://pbs.twimg.com/ext_tw_video_thumb/1251578276709109764/pu/img/fHFdxWFL3nQE9L0I.jpg:large" width="10" height="7" src="https://video.twimg.com/ext_tw_video/1251578276709109764/pu/vid/1028x720/lHpEeJekcIZAod2B.mp4?tag=10" playsinline="true" controls="true"></video>
}

View File

@@ -0,0 +1,389 @@
//
// TwitterFeedProvider.swift
// FeedProvider
//
// Created by Maurice Parker on 4/7/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import Secrets
import OAuthSwift
import RSParser
public enum TwitterFeedProviderError: LocalizedError {
case screenNameNotFound
case unknown
public var localizedDescription: String {
switch self {
case .screenNameNotFound:
return NSLocalizedString("Unable to determine screen name.", comment: "Screen name")
case .unknown:
return NSLocalizedString("An unknown Twitter Feed Provider error has occurred.", comment: "Screen name")
}
}
}
public enum TwitterFeedType: Int {
case homeTimeline = 0
case mentions = 1
case screenName = 2
case search = 3
}
public struct TwitterFeedProvider: FeedProvider {
private static let server = "api.twitter.com"
private static let apiBase = "https://api.twitter.com/1.1/"
private static let dateFormat = "EEE MMM dd HH:mm:ss Z yyyy"
private static let userPaths = ["/home", "/notifications"]
private static let reservedPaths = ["/search", "/explore", "/messages", "/i", "/compose"]
public var screenName: String
private var oauthToken: String
private var oauthTokenSecret: String
private var client: OAuthSwiftClient
public init?(tokenSuccess: OAuthSwift.TokenSuccess) {
guard let screenName = tokenSuccess.parameters["screen_name"] as? String else {
return nil
}
self.screenName = screenName
self.oauthToken = tokenSuccess.credential.oauthToken
self.oauthTokenSecret = tokenSuccess.credential.oauthTokenSecret
let tokenCredentials = Credentials(type: .oauthAccessToken, username: screenName, secret: oauthToken)
try? CredentialsManager.storeCredentials(tokenCredentials, server: Self.server)
let tokenSecretCredentials = Credentials(type: .oauthAccessTokenSecret, username: screenName, secret: oauthTokenSecret)
try? CredentialsManager.storeCredentials(tokenSecretCredentials, server: Self.server)
client = OAuthSwiftClient(consumerKey: Secrets.twitterConsumerKey,
consumerSecret: Secrets.twitterConsumerSecret,
oauthToken: oauthToken,
oauthTokenSecret: oauthTokenSecret,
version: .oauth1)
}
public init?(screenName: String) {
self.screenName = screenName
guard let tokenCredentials = try? CredentialsManager.retrieveCredentials(type: .oauthAccessToken, server: Self.server, username: screenName),
let tokenSecretCredentials = try? CredentialsManager.retrieveCredentials(type: .oauthAccessTokenSecret, server: Self.server, username: screenName) else {
return nil
}
self.oauthToken = tokenCredentials.secret
self.oauthTokenSecret = tokenSecretCredentials.secret
client = OAuthSwiftClient(consumerKey: Secrets.twitterConsumerKey,
consumerSecret: Secrets.twitterConsumerSecret,
oauthToken: oauthToken,
oauthTokenSecret: oauthTokenSecret,
version: .oauth1)
}
public func ability(_ urlComponents: URLComponents) -> FeedProviderAbility {
guard urlComponents.host?.hasSuffix("twitter.com") ?? false else {
return .none
}
if let username = urlComponents.user {
if username == screenName {
return .owner
} else {
return .none
}
}
return .available
}
public func iconURL(_ urlComponents: URLComponents, completion: @escaping (Result<String, Error>) -> Void) {
if let screenName = deriveScreenName(urlComponents) {
retrieveUser(screenName: screenName) { result in
switch result {
case .success(let user):
if let avatarURL = user.avatarURL {
completion(.success(avatarURL))
} else {
completion(.failure(TwitterFeedProviderError.screenNameNotFound))
}
case .failure(let error):
completion(.failure(error))
}
}
} else {
completion(.failure(TwitterFeedProviderError.screenNameNotFound))
}
}
public func assignName(_ urlComponents: URLComponents, completion: @escaping (Result<String, Error>) -> Void) {
switch urlComponents.path {
case "", "/", "/home":
let name = NSLocalizedString("Twitter Timeline", comment: "Twitter Timeline")
completion(.success(name))
case "/notifications/mentions":
let name = NSLocalizedString("Twitter Mentions", comment: "Twitter Mentions")
completion(.success(name))
case "/search":
if let query = urlComponents.queryItems?.first(where: { $0.name == "q" })?.value {
let localized = NSLocalizedString("Twitter Search: %@", comment: "Twitter Search")
let searchName = NSString.localizedStringWithFormat(localized as NSString, query) as String
completion(.success(searchName))
} else {
let name = NSLocalizedString("Twitter Search", comment: "Twitter Search")
completion(.success(name))
}
default:
if let hashtag = deriveHashtag(urlComponents) {
completion(.success("#\(hashtag)"))
} else if let screenName = deriveScreenName(urlComponents) {
retrieveUser(screenName: screenName) { result in
switch result {
case .success(let user):
if let userName = user.name {
completion(.success(userName))
} else {
completion(.failure(TwitterFeedProviderError.screenNameNotFound))
}
case .failure(let error):
completion(.failure(error))
}
}
} else {
completion(.failure(TwitterFeedProviderError.unknown))
}
}
}
public func refresh(_ webFeed: WebFeed, completion: @escaping (Result<Set<ParsedItem>, Error>) -> Void) {
guard let urlComponents = URLComponents(string: webFeed.url) else {
completion(.failure(TwitterFeedProviderError.unknown))
return
}
let api: String
var parameters = [String: Any]()
var isSearch = false
switch urlComponents.path {
case "", "/", "/home":
parameters["count"] = 100
api = "statuses/home_timeline.json"
case "/notifications/mentions":
api = "statuses/mentions_timeline.json"
case "/search":
api = "search/tweets.json"
if let query = urlComponents.queryItems?.first(where: { $0.name == "q" })?.value {
parameters["q"] = query
}
isSearch = true
default:
if let hashtag = deriveHashtag(urlComponents) {
api = "search/tweets.json"
parameters["q"] = "#\(hashtag)"
isSearch = true
} else {
api = "statuses/user_timeline.json"
parameters["exclude_replies"] = true
if let screenName = deriveScreenName(urlComponents) {
parameters["screen_name"] = screenName
} else {
completion(.failure(TwitterFeedProviderError.unknown))
return
}
}
}
retrieveTweets(api: api, parameters: parameters, isSearch: isSearch) { result in
switch result {
case .success(let tweets):
let parsedItems = self.makeParsedItems(webFeed.url, tweets)
completion(.success(parsedItems))
case .failure(let error):
completion(.failure(error))
}
}
}
public static func buildURL(_ type: TwitterFeedType, username: String?, screenName: String?, searchField: String?) -> URL? {
var components = URLComponents()
components.scheme = "https"
components.host = "twitter.com"
switch type {
case .homeTimeline:
guard let username = username else {
return nil
}
components.user = username
case .mentions:
guard let username = username else {
return nil
}
components.user = username
components.path = "/notifications/mentions"
case .screenName:
guard let screenName = screenName else {
return nil
}
components.path = "/\(screenName)"
case .search:
guard let searchField = searchField else {
return nil
}
components.path = "/search"
components.queryItems = [URLQueryItem(name: "q", value: searchField)]
}
return components.url
}
}
// MARK: OAuth1SwiftProvider
extension TwitterFeedProvider: OAuth1SwiftProvider {
public static var oauth1Swift: OAuth1Swift {
return OAuth1Swift(
consumerKey: Secrets.twitterConsumerKey,
consumerSecret: Secrets.twitterConsumerSecret,
requestTokenUrl: "https://api.twitter.com/oauth/request_token",
authorizeUrl: "https://api.twitter.com/oauth/authorize",
accessTokenUrl: "https://api.twitter.com/oauth/access_token"
)
}
}
// MARK: Private
private extension TwitterFeedProvider {
func deriveHashtag(_ urlComponents: URLComponents) -> String? {
let path = urlComponents.path
if path.starts(with: "/hashtag/"), let startIndex = path.index(path.startIndex, offsetBy: 9, limitedBy: path.endIndex), startIndex < path.endIndex {
return String(path[startIndex..<path.endIndex])
}
return nil
}
func deriveScreenName(_ urlComponents: URLComponents) -> String? {
let path = urlComponents.path
guard !Self.reservedPaths.contains(path) else { return nil }
if path.isEmpty || Self.userPaths.contains(path) {
return screenName
} else {
return String(path.suffix(from: path.index(path.startIndex, offsetBy: 1)))
}
}
func retrieveUser(screenName: String, completion: @escaping (Result<TwitterUser, Error>) -> Void) {
let url = "\(Self.apiBase)users/show.json"
let parameters = ["screen_name": screenName]
client.get(url, parameters: parameters) { result in
switch result {
case .success(let response):
let decoder = JSONDecoder()
do {
let user = try decoder.decode(TwitterUser.self, from: response.data)
completion(.success(user))
} catch {
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
func retrieveTweets(api: String, parameters: [String: Any], isSearch: Bool, completion: @escaping (Result<[TwitterStatus], Error>) -> Void) {
let url = "\(Self.apiBase)\(api)"
var expandedParameters = parameters
expandedParameters["tweet_mode"] = "extended"
client.get(url, parameters: expandedParameters) { result in
switch result {
case .success(let response):
let decoder = JSONDecoder()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = Self.dateFormat
decoder.dateDecodingStrategy = .formatted(dateFormatter)
do {
let tweets: [TwitterStatus]
if isSearch {
let searchResult = try decoder.decode(TwitterSearchResult.self, from: response.data)
if let statuses = searchResult.statuses {
tweets = statuses
} else {
tweets = [TwitterStatus]()
}
} else {
tweets = try decoder.decode([TwitterStatus].self, from: response.data)
}
completion(.success(tweets))
} catch {
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
func makeParsedItems(_ webFeedURL: String, _ statuses: [TwitterStatus]) -> Set<ParsedItem> {
var parsedItems = Set<ParsedItem>()
for status in statuses {
guard let idStr = status.idStr, let statusURL = status.url else { continue }
let parsedItem = ParsedItem(syncServiceID: nil,
uniqueID: idStr,
feedURL: webFeedURL,
url: statusURL,
externalURL: nil,
title: nil,
language: nil,
contentHTML: status.renderAsHTML(),
contentText: status.renderAsText(),
summary: nil,
imageURL: nil,
bannerImageURL: nil,
datePublished: status.createdAt,
dateModified: nil,
authors: makeParsedAuthors(status.user),
tags: nil,
attachments: nil)
parsedItems.insert(parsedItem)
}
return parsedItems
}
func makeUserURL(_ screenName: String) -> String {
return "https://twitter.com/\(screenName)"
}
func makeParsedAuthors(_ user: TwitterUser?) -> Set<ParsedAuthor>? {
guard let user = user else { return nil }
return Set([ParsedAuthor(name: user.name, url: user.url, avatarURL: user.avatarURL, emailAddress: nil)])
}
}

View File

@@ -0,0 +1,28 @@
//
// TwitterHashtag.swift
// Account
//
// Created by Maurice Parker on 4/18/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
struct TwitterHashtag: Codable, TwitterEntity {
let text: String?
let indices: [Int]?
enum CodingKeys: String, CodingKey {
case text = "text"
case indices = "indices"
}
func renderAsHTML() -> String {
var html = String()
if let text = text {
html += "<a href=\"https://twitter.com/search?q=%23\(text)\">#\(text)</a>"
}
return html
}
}

View File

@@ -0,0 +1,22 @@
//
// TwitterMedia.swift
// Account
//
// Created by Maurice Parker on 4/20/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
struct TwitterMedia: Codable, TwitterEntity {
let indices: [Int]?
enum CodingKeys: String, CodingKey {
case indices = "indices"
}
func renderAsHTML() -> String {
return String()
}
}

View File

@@ -0,0 +1,33 @@
//
// TwitterMention.swift
// Account
//
// Created by Maurice Parker on 4/18/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
struct TwitterMention: Codable, TwitterEntity {
let name: String?
let indices: [Int]?
let screenName: String?
let idStr: String?
enum CodingKeys: String, CodingKey {
case name = "url"
case indices = "indices"
case screenName = "screen_name"
case idStr = "idStr"
}
func renderAsHTML() -> String {
var html = String()
if let screenName = screenName {
html += "<a href=\"https://twitter.com/\(screenName)\">@\(screenName)</a>"
}
return html
}
}

View File

@@ -0,0 +1,19 @@
//
// TwitterSearchResult.swift
// Account
//
// Created by Maurice Parker on 4/18/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
struct TwitterSearchResult: Codable {
let statuses: [TwitterStatus]?
enum CodingKeys: String, CodingKey {
case statuses = "statuses"
}
}

View File

@@ -0,0 +1,182 @@
//
// TwitterStatus.swift
// Account
//
// Created by Maurice Parker on 4/16/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
final class TwitterStatus: Codable {
let createdAt: Date?
let idStr: String?
let fullText: String?
let displayTextRange: [Int]?
let user: TwitterUser?
let truncated: Bool?
let retweeted: Bool?
let retweetedStatus: TwitterStatus?
let quotedStatus: TwitterStatus?
let entities: TwitterEntities?
let extendedEntities: TwitterExtendedEntities?
enum CodingKeys: String, CodingKey {
case createdAt = "created_at"
case idStr = "id_str"
case fullText = "full_text"
case displayTextRange = "display_text_range"
case user = "user"
case truncated = "truncated"
case retweeted = "retweeted"
case retweetedStatus = "retweeted_status"
case quotedStatus = "quoted_status"
case entities = "entities"
case extendedEntities = "extended_entities"
}
var url: String? {
guard let userURL = user?.url, let idStr = idStr else { return nil }
return "\(userURL)/status/\(idStr)"
}
func renderAsText() -> String? {
let statusToRender = retweetedStatus != nil ? retweetedStatus! : self
return statusToRender.displayText
}
func renderAsHTML(topLevel: Bool = true) -> String {
if let retweetedStatus = retweetedStatus {
return renderAsRetweetHTML(retweetedStatus, topLevel: topLevel)
}
if let quotedStatus = quotedStatus {
return renderAsQuoteHTML(quotedStatus, topLevel: topLevel)
}
return renderAsOriginalHTML(topLevel: topLevel)
}
}
private extension TwitterStatus {
var displayText: String? {
if let text = fullText, let displayRange = displayTextRange, displayRange.count > 1,
let startIndex = text.index(text.startIndex, offsetBy: displayRange[0], limitedBy: text.endIndex),
let endIndex = text.index(text.startIndex, offsetBy: displayRange[1], limitedBy: text.endIndex) {
return String(text[startIndex..<endIndex])
} else {
return fullText
}
}
var displayHTML: String? {
if let text = fullText, let displayRange = displayTextRange, displayRange.count > 1, let entities = entities?.combineAndSort() {
let displayStartIndex = text.index(text.startIndex, offsetBy: displayRange[0], limitedBy: text.endIndex) ?? text.startIndex
let displayEndIndex = text.index(text.startIndex, offsetBy: displayRange[1], limitedBy: text.endIndex) ?? text.endIndex
var html = String()
var prevIndex = displayStartIndex
var emojiOffset = 0
for entity in entities {
// The twitter indices are messed up by emoji with more than one scalar, we are going to adjust for that here.
let emojiEndIndex = text.index(text.startIndex, offsetBy: entity.endIndex, limitedBy: text.endIndex) ?? text.endIndex
if prevIndex < emojiEndIndex {
let emojis = String(text[prevIndex..<emojiEndIndex]).emojis
for emoji in emojis {
emojiOffset += emoji.unicodeScalars.count - 1
}
}
let offsetStartIndex = entity.startIndex - emojiOffset
let offsetEndIndex = entity.endIndex - emojiOffset
let entityStartIndex = text.index(text.startIndex, offsetBy: offsetStartIndex, limitedBy: text.endIndex) ?? text.startIndex
let entityEndIndex = text.index(text.startIndex, offsetBy: offsetEndIndex, limitedBy: text.endIndex) ?? text.endIndex
if prevIndex < entityStartIndex {
html += String(text[prevIndex..<entityStartIndex]).replacingOccurrences(of: "\n", with: "<br>")
}
// We drop off any URL which is just pointing to the quoted status. It is redundant.
if let twitterURL = entity as? TwitterURL, let expandedURL = twitterURL.expandedURL, let quotedURL = quotedStatus?.url {
if expandedURL.caseInsensitiveCompare(quotedURL) != .orderedSame {
html += entity.renderAsHTML()
}
} else {
html += entity.renderAsHTML()
}
prevIndex = entityEndIndex
}
if prevIndex < displayEndIndex {
html += String(text[prevIndex..<displayEndIndex])
}
return html
} else {
return displayText
}
}
func renderAsTweetHTML(_ status: TwitterStatus, topLevel: Bool) -> String {
var html = "<div>\(status.displayHTML ?? "")</div>"
if !topLevel, let createdAt = status.createdAt, let url = status.url {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .short
html += "<a href=\"\(url)\" class=\"twitterTimestamp\">\(dateFormatter.string(from: createdAt))</a>"
}
return html
}
func renderAsOriginalHTML(topLevel: Bool) -> String {
var html = renderAsTweetHTML(self, topLevel: topLevel)
if topLevel {
html += extendedEntities?.renderAsHTML() ?? ""
html += retweetedStatus?.extendedEntities?.renderAsHTML() ?? ""
html += quotedStatus?.extendedEntities?.renderAsHTML() ?? ""
}
return html
}
func renderAsRetweetHTML(_ status: TwitterStatus, topLevel: Bool) -> String {
var html = "<blockquote>"
if let userHTML = status.user?.renderAsHTML() {
html += userHTML
}
html += status.renderAsHTML(topLevel: false)
html += "</blockquote>"
if topLevel {
html += status.extendedEntities?.renderAsHTML() ?? ""
html += status.retweetedStatus?.extendedEntities?.renderAsHTML() ?? ""
html += status.quotedStatus?.extendedEntities?.renderAsHTML() ?? ""
}
return html
}
func renderAsQuoteHTML(_ quotedStatus: TwitterStatus, topLevel: Bool) -> String {
var html = String()
html += renderAsTweetHTML(self, topLevel: topLevel)
html += "<blockquote>"
if let userHTML = quotedStatus.user?.renderAsHTML() {
html += userHTML
}
html += quotedStatus.renderAsHTML(topLevel: false)
html += "</blockquote>"
if topLevel {
html += quotedStatus.extendedEntities?.renderAsHTML() ?? ""
html += quotedStatus.retweetedStatus?.extendedEntities?.renderAsHTML() ?? ""
html += quotedStatus.quotedStatus?.extendedEntities?.renderAsHTML() ?? ""
}
return html
}
}

View File

@@ -0,0 +1,29 @@
//
// TwitterSymbol.swift
// Account
//
// Created by Maurice Parker on 4/18/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
struct TwitterSymbol: Codable, TwitterEntity {
let name: String?
let indices: [Int]?
enum CodingKeys: String, CodingKey {
case name = "name"
case indices = "indices"
}
func renderAsHTML() -> String {
var html = String()
if let name = name {
html += "<a href=\"https://twitter.com/search?q=%24\(name)\">$\(name)</a>"
}
return html
}
}

View File

@@ -0,0 +1,33 @@
//
// TwitterURL.swift
// Account
//
// Created by Maurice Parker on 4/18/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
struct TwitterURL: Codable, TwitterEntity {
let url: String?
let indices: [Int]?
let displayURL: String?
let expandedURL: String?
enum CodingKeys: String, CodingKey {
case url = "url"
case indices = "indices"
case displayURL = "display_url"
case expandedURL = "expanded_url"
}
func renderAsHTML() -> String {
var html = String()
if let expandedURL = expandedURL, let displayURL = displayURL {
html += "<a href=\"\(expandedURL)\">\(displayURL)</a>"
}
return html
}
}

View File

@@ -0,0 +1,44 @@
//
// TwitterUser.swift
// Account
//
// Created by Maurice Parker on 4/16/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
struct TwitterUser: Codable {
let name: String?
let screenName: String?
let avatarURL: String?
enum CodingKeys: String, CodingKey {
case name = "name"
case screenName = "screen_name"
case avatarURL = "profile_image_url_https"
}
var url: String {
return "https://twitter.com/\(screenName ?? "")"
}
func renderAsHTML() -> String? {
var html = String()
html += "<div><a href=\"\(url)\">"
if let avatarURL = avatarURL {
html += "<img class=\"twitterAvatar\" src=\"\(avatarURL)\">"
}
html += "<span class=\"twitterUsername\">"
if let name = name {
html += " \(name)"
}
if let screenName = screenName {
html += " @\(screenName)"
}
html += "</span></a></div>"
return html
}
}

View File

@@ -0,0 +1,34 @@
//
// TwitterVideoInfo.swift
// Account
//
// Created by Maurice Parker on 4/18/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
struct TwitterVideo: Codable {
let variants: [Variant]?
enum CodingKeys: String, CodingKey {
case variants = "variants"
}
struct Variant: Codable {
let bitrate: Int?
let contentType: String?
let url: String?
enum CodingKeys: String, CodingKey {
case bitrate = "bitrate"
case contentType = "content_type"
case url = "url"
}
}
}

View File

@@ -11,6 +11,7 @@ import Foundation
import Foundation
import SyncDatabase
import RSWeb
import Secrets
enum FeedWranglerError : Error {
case general(message: String)

View File

@@ -12,6 +12,7 @@ import RSParser
import RSWeb
import SyncDatabase
import os.log
import Secrets
final class FeedWranglerAccountDelegate: AccountDelegate {

View File

@@ -7,6 +7,7 @@
//
import Foundation
import Secrets
enum FeedWranglerConfig {
static let pageSize = 100

View File

@@ -12,6 +12,7 @@
import Foundation
import RSWeb
import Secrets
enum CreateSubscriptionResult {
case created(FeedbinSubscription)

View File

@@ -13,6 +13,7 @@ import RSParser
import RSWeb
import SyncDatabase
import os.log
import Secrets
public enum FeedbinAccountDelegateError: String, Error {
case invalidParameter = "There was an invalid parameter passed."

View File

@@ -8,6 +8,7 @@
import Foundation
import RSWeb
import Secrets
final class FeedlyAPICaller {

View File

@@ -8,6 +8,7 @@
import Foundation
import RSWeb
import Secrets
/// Models the access token response from Feedly.
/// https://developer.feedly.com/v3/auth/#exchanging-an-auth-code-for-a-refresh-token-and-an-access-token

View File

@@ -12,6 +12,7 @@ import RSParser
import RSWeb
import SyncDatabase
import os.log
import Secrets
final class FeedlyAccountDelegate: AccountDelegate {

View File

@@ -7,6 +7,7 @@
//
import Foundation
import Secrets
extension OAuthAuthorizationClient {

View File

@@ -8,6 +8,7 @@
import Foundation
import RSWeb
import Secrets
/// Client-specific information for requesting an authorization code grant.
/// Accounts are responsible for the scope.

View File

@@ -10,6 +10,7 @@ import Foundation
import os.log
import RSWeb
import RSCore
import Secrets
class FeedlyAddExistingFeedOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlyCheckpointOperationDelegate {

View File

@@ -11,6 +11,7 @@ import os.log
import SyncDatabase
import RSWeb
import RSCore
import Secrets
class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlySearchOperationDelegate, FeedlyCheckpointOperationDelegate {

View File

@@ -8,6 +8,7 @@
import Foundation
import os.log
import Secrets
/// Single responsibility is to identify articles that have changed since a particular date.
///

View File

@@ -9,6 +9,7 @@
import Foundation
import os.log
import SyncDatabase
import Secrets
/// Clone locally the remote starred article state.
///

View File

@@ -8,6 +8,7 @@
import Foundation
import os.log
import Secrets
/// Ensure a status exists for every article id the user might be interested in.
///

View File

@@ -10,6 +10,7 @@ import Foundation
import os.log
import RSParser
import SyncDatabase
import Secrets
/// Clone locally the remote unread article state.
///

View File

@@ -9,6 +9,7 @@
import Foundation
import os.log
import RSWeb
import Secrets
final class FeedlyRefreshAccessTokenOperation: FeedlyOperation {

View File

@@ -11,6 +11,7 @@ import os.log
import SyncDatabase
import RSWeb
import RSCore
import Secrets
/// Compose the operations necessary to get the entire set of articles, feeds and folders with the statuses the user expects between now and a certain date in the past.
final class FeedlySyncAllOperation: FeedlyOperation {

View File

@@ -11,6 +11,7 @@ import os.log
import RSParser
import RSCore
import RSWeb
import Secrets
final class FeedlySyncStreamContentsOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlyGetStreamContentsOperationDelegate, FeedlyCheckpointOperationDelegate {

View File

@@ -7,11 +7,13 @@
//
import Foundation
import os.log
import RSCore
import RSParser
import Articles
import ArticlesDatabase
import RSWeb
import Secrets
public enum LocalAccountDelegateError: String, Error {
case invalidParameter = "An invalid parameter was used."
@@ -19,6 +21,8 @@ public enum LocalAccountDelegateError: String, Error {
final class LocalAccountDelegate: AccountDelegate {
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "LocalAccount")
weak var account: Account?
private lazy var refresher: LocalAccountRefresher? = {
@@ -35,23 +39,55 @@ final class LocalAccountDelegate: AccountDelegate {
var accountMetadata: AccountMetadata?
let refreshProgress = DownloadProgress(numberOfTasks: 0)
var refreshAllCompletion: ((Result<Void, Error>) -> Void)? = nil
func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any], completion: @escaping () -> Void) {
completion()
}
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
guard refreshAllCompletion == nil else {
guard refreshProgress.isComplete else {
completion(.success(()))
return
}
refreshAllCompletion = completion
var refresherWebFeeds = Set<WebFeed>()
let webFeeds = account.flattenedWebFeeds()
refreshProgress.addToNumberOfTasksAndRemaining(webFeeds.count)
refresher?.refreshFeeds(webFeeds)
let group = DispatchGroup()
for webFeed in webFeeds {
if let components = URLComponents(string: webFeed.url), let feedProvider = FeedProviderManager.shared.best(for: components) {
refreshProgress.addToNumberOfTasksAndRemaining(1)
group.enter()
feedProvider.refresh(webFeed) { result in
switch result {
case .success(let parsedItems):
account.update(webFeed.webFeedID, with: parsedItems) { _ in
self.refreshProgress.completeTask()
group.leave()
}
case .failure(let error):
os_log(.error, log: self.log, "Feed Provider refresh error: %@.", error.localizedDescription)
self.refreshProgress.completeTask()
group.leave()
}
}
} else {
refresherWebFeeds.insert(webFeed)
}
}
refreshProgress.addToNumberOfTasksAndRemaining(refresherWebFeeds.count)
group.enter()
refresher?.refreshFeeds(refresherWebFeeds) {
group.leave()
}
group.notify(queue: DispatchQueue.main) {
account.metadata.lastArticleFetchEndTime = Date()
completion(.success(()))
}
}
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
@@ -105,52 +141,17 @@ final class LocalAccountDelegate: AccountDelegate {
}
func createWebFeed(for account: Account, url urlString: String, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void) {
guard let url = URL(string: urlString) else {
guard let url = URL(string: urlString), let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
completion(.failure(LocalAccountDelegateError.invalidParameter))
return
}
refreshProgress.addToNumberOfTasksAndRemaining(1)
FeedFinder.find(url: url) { result in
switch result {
case .success(let feedSpecifiers):
guard let bestFeedSpecifier = FeedSpecifier.bestFeed(in: feedSpecifiers),
let url = URL(string: bestFeedSpecifier.urlString) else {
self.refreshProgress.completeTask()
completion(.failure(AccountError.createErrorNotFound))
return
}
if account.hasWebFeed(withURL: bestFeedSpecifier.urlString) {
self.refreshProgress.completeTask()
completion(.failure(AccountError.createErrorAlreadySubscribed))
return
}
let feed = account.createWebFeed(with: nil, url: url.absoluteString, webFeedID: url.absoluteString, homePageURL: nil)
InitialFeedDownloader.download(url) { parsedFeed in
self.refreshProgress.completeTask()
if let parsedFeed = parsedFeed {
account.update(feed, with: parsedFeed, {_ in})
}
feed.editedName = name
container.addWebFeed(feed)
completion(.success(feed))
}
case .failure:
self.refreshProgress.completeTask()
completion(.failure(AccountError.createErrorNotFound))
}
// Username should be part of the URL on new feed adds
if let feedProvider = FeedProviderManager.shared.best(for: urlComponents) {
createProviderWebFeed(for: account, urlComponents: urlComponents, editedName: name, container: container, feedProvider: feedProvider, completion: completion)
} else {
createRSSWebFeed(for: account, url: url, editedName: name, container: container, completion: completion)
}
}
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
@@ -244,9 +245,101 @@ extension LocalAccountDelegate: LocalAccountRefresherDelegate {
func localAccountRefresherDidFinish(_ refresher: LocalAccountRefresher) {
self.refreshProgress.clear()
account?.metadata.lastArticleFetchEndTime = Date()
refreshAllCompletion?(.success(()))
refreshAllCompletion = nil
}
}
private extension LocalAccountDelegate {
func createProviderWebFeed(for account: Account, urlComponents: URLComponents, editedName: String?, container: Container, feedProvider: FeedProvider, completion: @escaping (Result<WebFeed, Error>) -> Void) {
refreshProgress.addToNumberOfTasksAndRemaining(2)
feedProvider.assignName(urlComponents) { result in
self.refreshProgress.completeTask()
switch result {
case .success(let name):
guard let urlString = urlComponents.url?.absoluteString else {
completion(.failure(AccountError.createErrorNotFound))
return
}
let feed = account.createWebFeed(with: name, url: urlString, webFeedID: urlString, homePageURL: nil)
feed.editedName = editedName
container.addWebFeed(feed)
feedProvider.refresh(feed) { result in
self.refreshProgress.completeTask()
switch result {
case .success(let parsedItems):
account.update(urlString, with: parsedItems) { _ in
completion(.success(feed))
}
case .failure:
completion(.failure(AccountError.createErrorNotFound))
}
}
case .failure(let error):
completion(.failure(error))
}
}
}
func createRSSWebFeed(for account: Account, url: URL, editedName: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void) {
// We need to use a batch update here because we need to assign add the feed to the
// container before the name has been downloaded. This will put it in the sidebar
// with an Untitled name if we don't delay it being added to the sidebar.
BatchUpdate.shared.start()
refreshProgress.addToNumberOfTasksAndRemaining(1)
FeedFinder.find(url: url) { result in
switch result {
case .success(let feedSpecifiers):
guard let bestFeedSpecifier = FeedSpecifier.bestFeed(in: feedSpecifiers),
let url = URL(string: bestFeedSpecifier.urlString) else {
self.refreshProgress.completeTask()
BatchUpdate.shared.end()
completion(.failure(AccountError.createErrorNotFound))
return
}
if account.hasWebFeed(withURL: bestFeedSpecifier.urlString) {
self.refreshProgress.completeTask()
BatchUpdate.shared.end()
completion(.failure(AccountError.createErrorAlreadySubscribed))
return
}
let feed = account.createWebFeed(with: nil, url: url.absoluteString, webFeedID: url.absoluteString, homePageURL: nil)
feed.editedName = editedName
container.addWebFeed(feed)
InitialFeedDownloader.download(url) { parsedFeed in
self.refreshProgress.completeTask()
if let parsedFeed = parsedFeed {
account.update(feed, with: parsedFeed, {_ in
BatchUpdate.shared.end()
completion(.success(feed))
})
} else {
BatchUpdate.shared.end()
completion(.failure(AccountError.createErrorNotFound))
}
}
case .failure:
BatchUpdate.shared.end()
self.refreshProgress.completeTask()
completion(.failure(AccountError.createErrorNotFound))
}
}
}
}

View File

@@ -30,6 +30,10 @@ final class LocalAccountRefresher {
}()
public func refreshFeeds(_ feeds: Set<WebFeed>, completion: (() -> Void)? = nil) {
guard !feeds.isEmpty else {
completion?()
return
}
if let completion = completion {
completions.append(completion)
}
@@ -100,8 +104,8 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
}
account.update(feed, with: parsedFeed) { result in
if case .success(let newAndUpdatedArticles) = result {
self.delegate?.localAccountRefresher(self, didProcess: newAndUpdatedArticles) {
if case .success(let articleChanges) = result {
self.delegate?.localAccountRefresher(self, didProcess: articleChanges) {
if let httpResponse = response as? HTTPURLResponse {
feed.conditionalGetInfo = HTTPConditionalGetInfo(urlResponse: httpResponse)
}

View File

@@ -8,6 +8,7 @@
import Foundation
import RSWeb
import Secrets
final class NewsBlurAPICaller: NSObject {
static let SessionIdCookie = "newsblur_sessionid"

View File

@@ -13,6 +13,7 @@ import RSParser
import RSWeb
import SyncDatabase
import os.log
import Secrets
final class NewsBlurAccountDelegate: AccountDelegate {

View File

@@ -12,6 +12,7 @@ import RSParser
import RSWeb
import SyncDatabase
import os.log
import Secrets
public enum ReaderAPIAccountDelegateError: String, Error {
case invalidParameter = "There was an invalid parameter passed."

View File

@@ -8,6 +8,7 @@
import Foundation
import RSWeb
import Secrets
enum CreateReaderAPISubscriptionResult {
case created(ReaderAPISubscription)

View File

@@ -8,6 +8,7 @@
import Foundation
import RSWeb
import Secrets
public extension URLRequest {
@@ -60,6 +61,9 @@ public extension URLRequest {
case .oauthAccessToken:
let auth = "OAuth \(credentials.secret)"
setValue(auth, forHTTPHeaderField: "Authorization")
case .oauthAccessTokenSecret:
assertionFailure("Token secrets are used by OAuth1. Did you mean to use `OAuthSwift` instead of a URLRequest?")
break
case .oauthRefreshToken:
// While both access and refresh tokens are credentials, it seems the `Credentials` cases
// enumerates how the identity of the user can be proved rather than

View File

@@ -311,7 +311,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "xcrun -sdk macosx swiftc -target x86_64-macosx10.11 ../../buildscripts/VerifyNoBuildSettings.swift -o $CONFIGURATION_TEMP_DIR/VerifyNoBS\n$CONFIGURATION_TEMP_DIR/VerifyNoBS ${PROJECT_NAME}.xcodeproj/project.pbxproj\n";
shellScript = "xcrun -sdk macosx swiftc -target x86_64-macosx10.11 ../../buildscripts/VerifyNoBuildSettings.swift -o $CONFIGURATION_TEMP_DIR/VerifyNoBS\n$CONFIGURATION_TEMP_DIR/VerifyNoBS ${PROJECT_NAME}.xcodeproj/project.pbxproj\n\nif [ $? -ne 0 ]\nthen\n echo \"error: Build Setting were found in the project.pbxproj file. Most likely you didn't intend to change this file and should revert it.\"\n exit 1\nfi\n";
};
/* End PBXShellScriptBuildPhase section */

View File

@@ -364,14 +364,14 @@
TargetAttributes = {
844BEE361F0AB3AA004AB7CD = {
CreatedOnToolsVersion = 8.3.2;
DevelopmentTeam = M8L2WTLA8W;
DevelopmentTeam = SHJK2V3AJG;
LastSwiftMigration = 0830;
ProvisioningStyle = Manual;
ProvisioningStyle = Automatic;
};
844BEE3F1F0AB3AB004AB7CD = {
CreatedOnToolsVersion = 8.3.2;
DevelopmentTeam = M8L2WTLA8W;
ProvisioningStyle = Manual;
DevelopmentTeam = SHJK2V3AJG;
ProvisioningStyle = Automatic;
};
};
};
@@ -519,7 +519,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "xcrun -sdk macosx swiftc -target x86_64-macosx10.11 ../../buildscripts/VerifyNoBuildSettings.swift -o $CONFIGURATION_TEMP_DIR/VerifyNoBS\n$CONFIGURATION_TEMP_DIR/VerifyNoBS ${PROJECT_NAME}.xcodeproj/project.pbxproj\n";
shellScript = "xcrun -sdk macosx swiftc -target x86_64-macosx10.11 ../../buildscripts/VerifyNoBuildSettings.swift -o $CONFIGURATION_TEMP_DIR/VerifyNoBS\n$CONFIGURATION_TEMP_DIR/VerifyNoBS ${PROJECT_NAME}.xcodeproj/project.pbxproj\n\nif [ $? -ne 0 ]\nthen\n echo \"error: Build Setting were found in the project.pbxproj file. Most likely you didn't intend to change this file and should revert it.\"\n exit 1\nfi\n";
};
/* End PBXShellScriptBuildPhase section */

View File

@@ -22,6 +22,7 @@ public enum CredentialsType: String {
case readerBasic = "readerBasic"
case readerAPIKey = "readerAPIKey"
case oauthAccessToken = "oauthAccessToken"
case oauthAccessTokenSecret = "oauthAccessTokenSecret"
case oauthRefreshToken = "oauthRefreshToken"
}

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2020 Ranchero Software, LLC. All rights reserved.</string>
</dict>
</plist>

View File

@@ -0,0 +1,16 @@
//
// OAuth1SwiftProvider.swift
// Secrets
//
// Created by Maurice Parker on 4/14/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import OAuthSwift
public protocol OAuth1SwiftProvider {
static var oauth1Swift: OAuth1Swift { get }
}

View File

@@ -2,7 +2,7 @@
%{
import os
secrets = ['FEED_WRANGLER_KEY', 'MERCURY_CLIENT_ID', 'MERCURY_CLIENT_SECRET', 'FEEDLY_CLIENT_ID', 'FEEDLY_CLIENT_SECRET']
secrets = ['FEED_WRANGLER_KEY', 'MERCURY_CLIENT_ID', 'MERCURY_CLIENT_SECRET', 'FEEDLY_CLIENT_ID', 'FEEDLY_CLIENT_SECRET', 'TWITTER_CONSUMER_KEY', 'TWITTER_CONSUMER_SECRET']
def chunks(seq, size):
return (seq[i:(i + size)] for i in range(0, len(seq), size))

View File

@@ -0,0 +1,277 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objects = {
/* Begin PBXBuildFile section */
514446BE243FFF0300EE752D /* Secrets_project_release.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 514446B9243FFF0200EE752D /* Secrets_project_release.xcconfig */; };
514446BF243FFF0300EE752D /* Secrets_project_test.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 514446BA243FFF0200EE752D /* Secrets_project_test.xcconfig */; };
514446C0243FFF0300EE752D /* Secrets_project_debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 514446BB243FFF0200EE752D /* Secrets_project_debug.xcconfig */; };
514446C1243FFF0300EE752D /* Secrets_project.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 514446BC243FFF0200EE752D /* Secrets_project.xcconfig */; };
514446C2243FFF0300EE752D /* Secrets_target.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 514446BD243FFF0300EE752D /* Secrets_target.xcconfig */; };
514446ED2440030900EE752D /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514446EC2440030900EE752D /* Secrets.swift */; };
514BB43B243FFBFF0023B621 /* CredentialsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514BB439243FFBFF0023B621 /* CredentialsManager.swift */; };
514BB43C243FFBFF0023B621 /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514BB43A243FFBFF0023B621 /* Credentials.swift */; };
5152BEF2244633FA00138380 /* OAuth1SwiftProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5152BEF1244633FA00138380 /* OAuth1SwiftProvider.swift */; };
51C99ABD2447DD730027D5F6 /* OAuthSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51C99ABC2447DD730027D5F6 /* OAuthSwift.framework */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
514446B9243FFF0200EE752D /* Secrets_project_release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Secrets_project_release.xcconfig; sourceTree = "<group>"; };
514446BA243FFF0200EE752D /* Secrets_project_test.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Secrets_project_test.xcconfig; sourceTree = "<group>"; };
514446BB243FFF0200EE752D /* Secrets_project_debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Secrets_project_debug.xcconfig; sourceTree = "<group>"; };
514446BC243FFF0200EE752D /* Secrets_project.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Secrets_project.xcconfig; sourceTree = "<group>"; };
514446BD243FFF0300EE752D /* Secrets_target.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Secrets_target.xcconfig; sourceTree = "<group>"; };
514446EC2440030900EE752D /* Secrets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Secrets.swift; sourceTree = "<group>"; };
514BB41A243FFA640023B621 /* Secrets.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Secrets.framework; sourceTree = BUILT_PRODUCTS_DIR; };
514BB41E243FFA640023B621 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
514BB439243FFBFF0023B621 /* CredentialsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CredentialsManager.swift; sourceTree = "<group>"; };
514BB43A243FFBFF0023B621 /* Credentials.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Credentials.swift; sourceTree = "<group>"; };
5152BEF1244633FA00138380 /* OAuth1SwiftProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuth1SwiftProvider.swift; sourceTree = "<group>"; };
51C99ABC2447DD730027D5F6 /* OAuthSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OAuthSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
514BB417243FFA640023B621 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
51C99ABD2447DD730027D5F6 /* OAuthSwift.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
514BB410243FFA640023B621 = {
isa = PBXGroup;
children = (
514BB43A243FFBFF0023B621 /* Credentials.swift */,
514BB439243FFBFF0023B621 /* CredentialsManager.swift */,
5152BEF1244633FA00138380 /* OAuth1SwiftProvider.swift */,
514BB41E243FFA640023B621 /* Info.plist */,
514BB41B243FFA640023B621 /* Products */,
514446EC2440030900EE752D /* Secrets.swift */,
514BB42B243FFAF50023B621 /* xcconfig */,
51C99ABB2447DD730027D5F6 /* Frameworks */,
);
sourceTree = "<group>";
};
514BB41B243FFA640023B621 /* Products */ = {
isa = PBXGroup;
children = (
514BB41A243FFA640023B621 /* Secrets.framework */,
);
name = Products;
sourceTree = "<group>";
};
514BB42B243FFAF50023B621 /* xcconfig */ = {
isa = PBXGroup;
children = (
514446BB243FFF0200EE752D /* Secrets_project_debug.xcconfig */,
514446B9243FFF0200EE752D /* Secrets_project_release.xcconfig */,
514446BA243FFF0200EE752D /* Secrets_project_test.xcconfig */,
514446BC243FFF0200EE752D /* Secrets_project.xcconfig */,
514446BD243FFF0300EE752D /* Secrets_target.xcconfig */,
);
path = xcconfig;
sourceTree = "<group>";
};
51C99ABB2447DD730027D5F6 /* Frameworks */ = {
isa = PBXGroup;
children = (
51C99ABC2447DD730027D5F6 /* OAuthSwift.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
514BB415243FFA640023B621 /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
514BB419243FFA640023B621 /* Secrets */ = {
isa = PBXNativeTarget;
buildConfigurationList = 514BB422243FFA640023B621 /* Build configuration list for PBXNativeTarget "Secrets" */;
buildPhases = (
514BB415243FFA640023B621 /* Headers */,
514BB416243FFA640023B621 /* Sources */,
514BB417243FFA640023B621 /* Frameworks */,
514BB418243FFA640023B621 /* Resources */,
514BB438243FFBB30023B621 /* Run Script: Verfiy No Build Settings */,
);
buildRules = (
);
dependencies = (
);
name = Secrets;
productName = Credentials;
productReference = 514BB41A243FFA640023B621 /* Secrets.framework */;
productType = "com.apple.product-type.framework";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
514BB411243FFA640023B621 /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1140;
ORGANIZATIONNAME = "Ranchero Software, LLC";
TargetAttributes = {
514BB419243FFA640023B621 = {
CreatedOnToolsVersion = 11.4;
};
};
};
buildConfigurationList = 514BB414243FFA640023B621 /* Build configuration list for PBXProject "Secrets" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 514BB410243FFA640023B621;
productRefGroup = 514BB41B243FFA640023B621 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
514BB419243FFA640023B621 /* Secrets */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
514BB418243FFA640023B621 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
514446C0243FFF0300EE752D /* Secrets_project_debug.xcconfig in Resources */,
514446BF243FFF0300EE752D /* Secrets_project_test.xcconfig in Resources */,
514446C2243FFF0300EE752D /* Secrets_target.xcconfig in Resources */,
514446C1243FFF0300EE752D /* Secrets_project.xcconfig in Resources */,
514446BE243FFF0300EE752D /* Secrets_project_release.xcconfig in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
514BB438243FFBB30023B621 /* Run Script: Verfiy No Build Settings */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Run Script: Verfiy No Build Settings";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "xcrun -sdk macosx swiftc -target x86_64-macosx10.11 ../../buildscripts/VerifyNoBuildSettings.swift -o $CONFIGURATION_TEMP_DIR/VerifyNoBS\n$CONFIGURATION_TEMP_DIR/VerifyNoBS ${PROJECT_NAME}.xcodeproj/project.pbxproj\n\nif [ $? -ne 0 ]\nthen\n echo \"error: Build Setting were found in the project.pbxproj file. Most likely you didn't intend to change this file and should revert it.\"\n exit 1\nfi\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
514BB416243FFA640023B621 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
514BB43C243FFBFF0023B621 /* Credentials.swift in Sources */,
514446ED2440030900EE752D /* Secrets.swift in Sources */,
5152BEF2244633FA00138380 /* OAuth1SwiftProvider.swift in Sources */,
514BB43B243FFBFF0023B621 /* CredentialsManager.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
514BB420243FFA640023B621 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 514446BB243FFF0200EE752D /* Secrets_project_debug.xcconfig */;
buildSettings = {
};
name = Debug;
};
514BB421243FFA640023B621 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 514446B9243FFF0200EE752D /* Secrets_project_release.xcconfig */;
buildSettings = {
};
name = Release;
};
514BB423243FFA640023B621 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 514446BD243FFF0300EE752D /* Secrets_target.xcconfig */;
buildSettings = {
PRODUCT_NAME = Secrets;
};
name = Debug;
};
514BB424243FFA640023B621 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 514446BD243FFF0300EE752D /* Secrets_target.xcconfig */;
buildSettings = {
PRODUCT_NAME = Secrets;
};
name = Release;
};
514BB436243FFB800023B621 /* Test */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 514446BA243FFF0200EE752D /* Secrets_project_test.xcconfig */;
buildSettings = {
};
name = Test;
};
514BB437243FFB800023B621 /* Test */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 514446BD243FFF0300EE752D /* Secrets_target.xcconfig */;
buildSettings = {
PRODUCT_NAME = Secrets;
};
name = Test;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
514BB414243FFA640023B621 /* Build configuration list for PBXProject "Secrets" */ = {
isa = XCConfigurationList;
buildConfigurations = (
514BB420243FFA640023B621 /* Debug */,
514BB436243FFB800023B621 /* Test */,
514BB421243FFA640023B621 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
514BB422243FFA640023B621 /* Build configuration list for PBXNativeTarget "Secrets" */ = {
isa = XCConfigurationList;
buildConfigurations = (
514BB423243FFA640023B621 /* Debug */,
514BB437243FFB800023B621 /* Test */,
514BB424243FFA640023B621 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 514BB411243FFA640023B621 /* Project object */;
}

View File

@@ -0,0 +1,61 @@
CODE_SIGN_IDENTITY = Developer ID Application
DEVELOPMENT_TEAM = M8L2WTLA8W
CODE_SIGN_STYLE = Manual
PROVISIONING_PROFILE_SPECIFIER =
// See the notes in NetNewsWire_target.xcconfig on why the
// DeveloperSettings.xcconfig is #included here
#include? "../../../SharedXcodeSettings/DeveloperSettings.xcconfig"
SDKROOT = macosx
MACOSX_DEPLOYMENT_TARGET = 10.14
IPHONEOS_DEPLOYMENT_TARGET = 13.0
SUPPORTED_PLATFORMS = macosx iphoneos iphonesimulator
CLANG_ENABLE_OBJC_WEAK = YES
SWIFT_VERSION = 5.1
COMBINE_HIDPI_IMAGES = YES
COPY_PHASE_STRIP = NO
ALWAYS_SEARCH_USER_PATHS = NO
CURRENT_PROJECT_VERSION = 1
VERSION_INFO_PREFIX =
VERSIONING_SYSTEM = apple-generic
GCC_NO_COMMON_BLOCKS = YES
GCC_C_LANGUAGE_STANDARD = gnu99
CLANG_CXX_LANGUAGE_STANDARD = gnu++0x
CLANG_CXX_LIBRARY = libc++
CLANG_ENABLE_MODULES = YES
CLANG_ENABLE_OBJC_ARC = YES
ENABLE_STRICT_OBJC_MSGSEND = YES
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES
CLANG_WARN_DOCUMENTATION_COMMENTS = YES
CLANG_WARN_EMPTY_BODY = YES
CLANG_WARN_BOOL_CONVERSION = YES
CLANG_WARN_CONSTANT_CONVERSION = YES
GCC_WARN_64_TO_32_BIT_CONVERSION = YES
CLANG_WARN_ENUM_CONVERSION = YES
CLANG_WARN_INT_CONVERSION = YES
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES
CLANG_WARN_INFINITE_RECURSION = YES
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR
CLANG_WARN_STRICT_PROTOTYPES = YES
CLANG_WARN_COMMA = YES
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE
CLANG_WARN_UNREACHABLE_CODE = YES
GCC_WARN_UNUSED_FUNCTION = YES
GCC_WARN_UNUSED_VARIABLE = YES
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES
CLANG_WARN_SUSPICIOUS_MOVE = YES
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES
GCC_WARN_UNDECLARED_SELECTOR = YES
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
CLANG_ANALYZER_NONNULL = YES
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE
SWIFT_SWIFT3_OBJC_INFERENCE = Off

View File

@@ -0,0 +1,15 @@
#include "./Secrets_project.xcconfig"
DEBUG_INFORMATION_FORMAT = dwarf
ENABLE_TESTABILITY = YES
GCC_DYNAMIC_NO_PIC = NO
GCC_OPTIMIZATION_LEVEL = 0
GCC_PREPROCESSOR_DEFINITIONS = DEBUG=1 $(inherited)
MTL_ENABLE_DEBUG_INFO = YES
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG
SWIFT_COMPILATION_MODE = singlefile
SWIFT_OPTIMIZATION_LEVEL = -Onone
ONLY_ACTIVE_ARCH = YES

View File

@@ -0,0 +1,9 @@
#include "./Secrets_project.xcconfig"
DEBUG_INFORMATION_FORMAT = dwarf-with-dsym
ENABLE_NS_ASSERTIONS = NO
MTL_ENABLE_DEBUG_INFO = NO
SWIFT_OPTIMIZATION_LEVEL = -O
SWIFT_COMPILATION_MODE = wholemodule

View File

@@ -0,0 +1,3 @@
#include "./Secrets_project_debug.xcconfig"
OTHER_SWIFT_FLAGS = -DTEST $(inherited)

View File

@@ -0,0 +1,13 @@
INSTALL_PATH = $(LOCAL_LIBRARY_DIR)/Frameworks
SKIP_INSTALL = YES
DYLIB_COMPATIBILITY_VERSION = 1
DYLIB_CURRENT_VERSION = 1
DYLIB_INSTALL_NAME_BASE = @rpath
LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks @loader_path/Frameworks
DEFINES_MODULE = YES
FRAMEWORK_VERSION = A
INFOPLIST_FILE = Info.plist
PRODUCT_BUNDLE_IDENTIFIER = com.ranchero.Secrets
PRODUCT_NAME = $(TARGET_NAME)
CLANG_ENABLE_MODULES = YES
APPLICATION_EXTENSION_API_ONLY = YES

View File

@@ -185,7 +185,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "xcrun -sdk macosx swiftc -target x86_64-macosx10.11 ../../buildscripts/VerifyNoBuildSettings.swift -o $CONFIGURATION_TEMP_DIR/VerifyNoBS\n$CONFIGURATION_TEMP_DIR/VerifyNoBS ${PROJECT_NAME}.xcodeproj/project.pbxproj\n";
shellScript = "xcrun -sdk macosx swiftc -target x86_64-macosx10.11 ../../buildscripts/VerifyNoBuildSettings.swift -o $CONFIGURATION_TEMP_DIR/VerifyNoBS\n$CONFIGURATION_TEMP_DIR/VerifyNoBS ${PROJECT_NAME}.xcodeproj/project.pbxproj\n\nif [ $? -ne 0 ]\nthen\n echo \"error: Build Setting were found in the project.pbxproj file. Most likely you didn't intend to change this file and should revert it.\"\n exit 1\nfi\n\n";
};
/* End PBXShellScriptBuildPhase section */

View File

@@ -77,6 +77,22 @@ struct AppAssets {
return RSImage(named: "articleExtractorProgress4")
}()
static var extensionPointMarsEdit: RSImage = {
return RSImage(named: "extensionPointMarsEdit")!
}()
static var extensionPointMicroblog: RSImage = {
return RSImage(named: "extensionPointMicroblog")!
}()
static var extensionPointTwitter: RSImage = {
return RSImage(named: "extensionPointTwitter")!
}()
static var extensionPreference: RSImage? = {
return RSImage(contentsOfFile: "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/KEXT.icns")
}()
static var faviconTemplateImage: RSImage = {
return RSImage(named: "faviconTemplateImage")!
}()

View File

@@ -19,6 +19,7 @@ struct AppDefaults {
struct Key {
static let firstRunDate = "firstRunDate"
static let windowState = "windowState"
static let activeExtensionPointIDs = "activeExtensionPointIDs"
static let lastImageCacheFlushDate = "lastImageCacheFlushDate"
static let sidebarFontSize = "sidebarFontSize"
static let timelineFontSize = "timelineFontSize"
@@ -72,6 +73,15 @@ struct AppDefaults {
}
}
static var activeExtensionPointIDs: [[AnyHashable : AnyHashable]]? {
get {
return UserDefaults.standard.object(forKey: Key.activeExtensionPointIDs) as? [[AnyHashable : AnyHashable]]
}
set {
UserDefaults.standard.set(newValue, forKey: Key.activeExtensionPointIDs)
}
}
static var lastImageCacheFlushDate: Date? {
get {
return date(for: Key.lastImageCacheFlushDate)

View File

@@ -104,7 +104,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
super.init()
AccountManager.shared = AccountManager(accountsFolder: Platform.dataSubfolder(forApplication: nil, folderName: "Accounts")!)
FeedProviderManager.shared.delegate = ExtensionPointManager.shared
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(inspectableObjectsDidChange(_:)), name: .InspectableObjectsDidChange, object: nil)
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(didWakeNotification(_:)), name: NSWorkspace.didWakeNotification, object: nil)
@@ -134,10 +135,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
addFolderWindowController!.runSheetOnWindow(window)
}
func showAddFeedSheetOnWindow(_ window: NSWindow, urlString: String?, name: String?, account: Account?, folder: Folder?) {
func showAddWebFeedSheetOnWindow(_ window: NSWindow, urlString: String?, name: String?, account: Account?, folder: Folder?) {
addFeedController = AddFeedController(hostWindow: window)
addFeedController?.showAddFeedSheet(urlString, name, account, folder)
addFeedController?.showAddFeedSheet(.webFeed, urlString, name, account, folder)
}
// MARK: - NSApplicationDelegate
@@ -400,9 +400,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
if item.action == #selector(sortByNewestArticleOnTop(_:)) || item.action == #selector(sortByOldestArticleOnTop(_:)) {
return mainWindowController?.isOpen ?? false
}
if item.action == #selector(showAddFeedWindow(_:)) || item.action == #selector(showAddFolderWindow(_:)) {
if item.action == #selector(showAddWebFeedWindow(_:)) || item.action == #selector(showAddFolderWindow(_:)) {
return !isDisplayingSheet && !AccountManager.shared.activeAccounts.isEmpty
}
if item.action == #selector(showAddTwitterFeedWindow(_:)) {
return ExtensionPointManager.shared.activeExtensionPoints.values.contains(where: { $0 is TwitterFeedProvider })
}
#if !MAC_APP_STORE
if item.action == #selector(toggleWebInspectorEnabled(_:)) {
(item as! NSMenuItem).state = AppDefaults.webInspectorEnabled ? .on : .off
@@ -423,14 +426,14 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
}
// MARK: Add Feed
func addFeed(_ urlString: String?, name: String? = nil, account: Account? = nil, folder: Folder? = nil) {
func addWebFeed(_ urlString: String?, name: String? = nil, account: Account? = nil, folder: Folder? = nil) {
createAndShowMainWindowIfNecessary()
if mainWindowController!.isDisplayingSheet {
return
}
showAddFeedSheetOnWindow(mainWindowController!.window!, urlString: urlString, name: name, account: account, folder: folder)
showAddWebFeedSheetOnWindow(mainWindowController!.window!, urlString: urlString, name: name, account: account, folder: folder)
}
// MARK: - Dock Badge
@@ -461,8 +464,14 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present)
}
@IBAction func showAddFeedWindow(_ sender: Any?) {
addFeed(nil)
@IBAction func showAddWebFeedWindow(_ sender: Any?) {
addWebFeed(nil)
}
@IBAction func showAddTwitterFeedWindow(_ sender: Any?) {
createAndShowMainWindowIfNecessary()
addFeedController = AddFeedController(hostWindow: mainWindowController!.window!)
addFeedController?.showAddFeedSheet(.twitterFeed)
}
@IBAction func showAddFolderWindow(_ sender: Any?) {
@@ -535,7 +544,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
if AccountManager.shared.anyAccountHasFeedWithURL(appNewsURLString) {
return
}
addFeed(appNewsURLString, name: "NetNewsWire News")
addWebFeed(appNewsURLString, name: "NetNewsWire News")
}
@IBAction func openWebsite(_ sender: Any?) {

View File

@@ -0,0 +1,199 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="16096" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="16096"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="AddTwitterFeedWindowController" customModule="NetNewsWire" customModuleProvider="target">
<connections>
<outlet property="accountLabel" destination="Acr-Ig-NVG" id="1gD-BE-CjH"/>
<outlet property="accountPopupButton" destination="X1H-Vv-1CJ" id="I0k-bb-XcU"/>
<outlet property="addButton" destination="dtI-Hu-rFb" id="D11-zR-dWH"/>
<outlet property="folderPopupButton" destination="6vt-DL-mVR" id="98M-xt-ZYU"/>
<outlet property="nameTextField" destination="TzV-3k-fXd" id="h4h-5v-4cY"/>
<outlet property="screenSearchTextField" destination="cEh-Wt-f5D" id="bnp-Zp-1fe"/>
<outlet property="typeDescriptionLabel" destination="f4Z-B8-HHm" id="jZ2-gz-Zr2"/>
<outlet property="typePopupButton" destination="j18-w8-wsH" id="KFC-K4-0tG"/>
<outlet property="window" destination="QvC-M9-y7g" id="7rH-S2-LF4"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="Add Twitter Feed" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="QvC-M9-y7g">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="196" y="240" width="306" height="216"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1417"/>
<view key="contentView" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="306" height="216"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="hXq-IS-19x">
<rect key="frame" x="128" y="13" width="82" height="32"/>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="Dop-HC-6Q9">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
Gw
</string>
</buttonCell>
<connections>
<action selector="cancel:" target="-2" id="tcT-tt-t99"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="dtI-Hu-rFb">
<rect key="frame" x="210" y="13" width="82" height="32"/>
<buttonCell key="cell" type="push" title="Add" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="6NK-Ql-drk">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
DQ
</string>
</buttonCell>
<connections>
<action selector="addFeed:" target="-2" id="Ilv-Un-eDp"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ddC-6D-Tvd">
<rect key="frame" x="40" y="178" width="41" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Type:" id="qto-IO-a1j">
<font key="font" metaFont="systemBold"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="j18-w8-wsH">
<rect key="frame" x="85" y="172" width="204" height="25"/>
<popUpButtonCell key="cell" type="push" title="Home Timeline" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="uE6-1a-w5g" id="bad-PM-uqO">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<menu key="menu" id="Ibj-Uy-KK7">
<items>
<menuItem title="Home Timeline" state="on" id="uE6-1a-w5g"/>
<menuItem title="Mentions" tag="1" id="177-F8-Esj"/>
<menuItem title="Screen Name" tag="2" id="DBZ-RV-FfV"/>
<menuItem title="Search" tag="3" id="0gG-oY-8yR"/>
</items>
</menu>
</popUpButtonCell>
<connections>
<action selector="selectedType:" target="-2" id="eAs-So-odx"/>
</connections>
</popUpButton>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Acr-Ig-NVG">
<rect key="frame" x="18" y="147" width="63" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Account:" id="LFf-JL-Ahl">
<font key="font" metaFont="systemBold"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="X1H-Vv-1CJ">
<rect key="frame" x="85" y="141" width="204" height="25"/>
<popUpButtonCell key="cell" type="push" title="@vincode_io" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="Tfk-aQ-RKg" id="HPE-P1-Hje">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<menu key="menu" id="TmQ-5T-oaz">
<items>
<menuItem title="@vincode_io" state="on" id="Tfk-aQ-RKg"/>
</items>
</menu>
</popUpButtonCell>
</popUpButton>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="cEh-Wt-f5D">
<rect key="frame" x="87" y="144" width="199" height="21"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" drawsBackground="YES" id="NLJ-ih-hZ8">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<outlet property="delegate" destination="-2" id="hNy-Li-bjr"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="f4Z-B8-HHm">
<rect key="frame" x="85" y="122" width="203" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Label" id="5AA-um-oEb">
<font key="font" metaFont="controlContent" size="11"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="sM9-DX-M0c">
<rect key="frame" x="35" y="94" width="46" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Name:" id="8ca-Qp-BkT">
<font key="font" metaFont="systemBold"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="TzV-3k-fXd" userLabel="Name Text Field">
<rect key="frame" x="87" y="91" width="199" height="21"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" placeholderString="Optional" drawsBackground="YES" usesSingleLineMode="YES" id="pLP-pL-5R5">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="dNV-oD-vzR">
<rect key="frame" x="31" y="63" width="50" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Folder:" id="Kwx-7B-CIu">
<font key="font" metaFont="systemBold"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="6vt-DL-mVR" userLabel="Folder Popup">
<rect key="frame" x="85" y="57" width="204" height="25"/>
<popUpButtonCell key="cell" type="push" title="Item 1" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="tLJ-zY-CcZ" id="0cM-5q-Snl">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<menu key="menu" id="OpL-Uf-woJ">
<items>
<menuItem title="Item 1" state="on" id="tLJ-zY-CcZ"/>
<menuItem title="Item 2" id="APc-af-7Um"/>
<menuItem title="Item 3" id="j09-9b-bGs"/>
</items>
</menu>
</popUpButtonCell>
</popUpButton>
</subviews>
<constraints>
<constraint firstItem="dNV-oD-vzR" firstAttribute="baseline" secondItem="6vt-DL-mVR" secondAttribute="baseline" id="14b-jN-4Y6"/>
<constraint firstItem="X1H-Vv-1CJ" firstAttribute="firstBaseline" secondItem="Acr-Ig-NVG" secondAttribute="firstBaseline" id="3Cl-Bw-Pcy"/>
<constraint firstItem="X1H-Vv-1CJ" firstAttribute="top" secondItem="j18-w8-wsH" secondAttribute="bottom" constant="10" id="48A-2f-2Wq"/>
<constraint firstAttribute="bottom" secondItem="dtI-Hu-rFb" secondAttribute="bottom" constant="20" symbolic="YES" id="6ac-2K-RnD"/>
<constraint firstItem="cEh-Wt-f5D" firstAttribute="leading" secondItem="j18-w8-wsH" secondAttribute="leading" id="73d-zR-g8z"/>
<constraint firstItem="TzV-3k-fXd" firstAttribute="leading" secondItem="cEh-Wt-f5D" secondAttribute="leading" id="Ap9-Ln-amq"/>
<constraint firstAttribute="trailing" secondItem="X1H-Vv-1CJ" secondAttribute="trailing" constant="20" id="Boa-Qw-dIK"/>
<constraint firstItem="TzV-3k-fXd" firstAttribute="leading" secondItem="sM9-DX-M0c" secondAttribute="trailing" constant="8" id="Ebw-Fa-w9o"/>
<constraint firstItem="TzV-3k-fXd" firstAttribute="top" secondItem="f4Z-B8-HHm" secondAttribute="bottom" constant="10" id="Elk-Gm-e4i"/>
<constraint firstItem="X1H-Vv-1CJ" firstAttribute="leading" secondItem="Acr-Ig-NVG" secondAttribute="trailing" constant="8" id="HwM-IS-kMa"/>
<constraint firstItem="dtI-Hu-rFb" firstAttribute="width" secondItem="hXq-IS-19x" secondAttribute="width" id="J80-aG-OjE"/>
<constraint firstItem="sM9-DX-M0c" firstAttribute="baseline" secondItem="TzV-3k-fXd" secondAttribute="baseline" id="K9a-t8-khQ"/>
<constraint firstAttribute="trailing" secondItem="f4Z-B8-HHm" secondAttribute="trailing" constant="20" id="POl-uX-qpn"/>
<constraint firstItem="f4Z-B8-HHm" firstAttribute="leading" secondItem="j18-w8-wsH" secondAttribute="leading" id="RbK-fc-c6E"/>
<constraint firstItem="hXq-IS-19x" firstAttribute="centerY" secondItem="dtI-Hu-rFb" secondAttribute="centerY" id="Sgq-Cy-rII"/>
<constraint firstItem="6vt-DL-mVR" firstAttribute="top" secondItem="TzV-3k-fXd" secondAttribute="bottom" constant="10" id="Sjo-Bv-alZ"/>
<constraint firstAttribute="trailing" secondItem="TzV-3k-fXd" secondAttribute="trailing" constant="20" symbolic="YES" id="V1s-JA-hA8"/>
<constraint firstItem="6vt-DL-mVR" firstAttribute="leading" secondItem="dNV-oD-vzR" secondAttribute="trailing" constant="8" id="WNy-vn-p8M"/>
<constraint firstItem="f4Z-B8-HHm" firstAttribute="top" secondItem="cEh-Wt-f5D" secondAttribute="bottom" constant="8" id="WiN-GE-aPh"/>
<constraint firstAttribute="trailing" secondItem="cEh-Wt-f5D" secondAttribute="trailing" constant="20" id="ZSt-ga-a8N"/>
<constraint firstItem="dtI-Hu-rFb" firstAttribute="leading" secondItem="hXq-IS-19x" secondAttribute="trailing" constant="12" symbolic="YES" id="ahD-oU-iFu"/>
<constraint firstItem="Acr-Ig-NVG" firstAttribute="leading" secondItem="EiT-Mj-1SZ" secondAttribute="leading" constant="20" id="dhv-D0-aPe"/>
<constraint firstAttribute="trailing" secondItem="j18-w8-wsH" secondAttribute="trailing" constant="20" id="eQ9-hw-PXg"/>
<constraint firstItem="j18-w8-wsH" firstAttribute="top" secondItem="EiT-Mj-1SZ" secondAttribute="top" constant="20" symbolic="YES" id="fK6-IW-NhJ"/>
<constraint firstItem="j18-w8-wsH" firstAttribute="leading" secondItem="X1H-Vv-1CJ" secondAttribute="leading" id="gSv-gG-TLd"/>
<constraint firstItem="6vt-DL-mVR" firstAttribute="leading" secondItem="TzV-3k-fXd" secondAttribute="leading" id="hMP-wG-fsP"/>
<constraint firstItem="cEh-Wt-f5D" firstAttribute="top" secondItem="j18-w8-wsH" secondAttribute="bottom" constant="10" id="hxS-Z9-dWU"/>
<constraint firstItem="j18-w8-wsH" firstAttribute="firstBaseline" secondItem="ddC-6D-Tvd" secondAttribute="firstBaseline" id="iz7-4p-NWj"/>
<constraint firstAttribute="trailing" secondItem="dtI-Hu-rFb" secondAttribute="trailing" constant="20" symbolic="YES" id="kEo-af-SUe"/>
<constraint firstItem="j18-w8-wsH" firstAttribute="leading" secondItem="ddC-6D-Tvd" secondAttribute="trailing" constant="8" id="n9D-4Y-HXk"/>
<constraint firstAttribute="trailing" secondItem="6vt-DL-mVR" secondAttribute="trailing" constant="20" id="suO-dd-E0b"/>
</constraints>
</view>
<point key="canvasLocation" x="102" y="-768"/>
</window>
</objects>
</document>

View File

@@ -1,12 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="16096" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14460.31"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="16096"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="AddFeedWindowController" customModule="NetNewsWire" customModuleProvider="target">
<customObject id="-2" userLabel="File's Owner" customClass="AddWebFeedWindowController" customModule="NetNewsWire" customModuleProvider="target">
<connections>
<outlet property="addButton" destination="dtI-Hu-rFb" id="D11-zR-dWH"/>
<outlet property="folderPopupButton" destination="6vt-DL-mVR" id="98M-xt-ZYU"/>
@@ -17,17 +16,17 @@
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="Add Feed" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="QvC-M9-y7g">
<window title="Add Web Feed" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="QvC-M9-y7g">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="196" y="240" width="480" height="217"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1417"/>
<view key="contentView" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="480" height="217"/>
<rect key="frame" x="0.0" y="0.0" width="480" height="216"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="hVI-F6-nNT">
<rect key="frame" x="33" y="180" width="35" height="17"/>
<rect key="frame" x="33" y="180" width="35" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="URL:" id="8jE-9v-BT2">
<font key="font" metaFont="systemBold"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@@ -35,7 +34,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="gbr-mI-Uzj" userLabel="URL Text Field">
<rect key="frame" x="74" y="124" width="386" height="73"/>
<rect key="frame" x="74" y="123" width="386" height="73"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="386" id="Wfx-Jk-wQ0"/>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="73" id="x84-xj-BzJ"/>
@@ -50,7 +49,7 @@
</connections>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="sM9-DX-M0c">
<rect key="frame" x="22" y="95" width="46" height="17"/>
<rect key="frame" x="22" y="95" width="46" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Name:" id="8ca-Qp-BkT">
<font key="font" metaFont="systemBold"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@@ -58,7 +57,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="TzV-3k-fXd" userLabel="Name Text Field">
<rect key="frame" x="74" y="92" width="386" height="22"/>
<rect key="frame" x="74" y="92" width="386" height="21"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" placeholderString="Optional" drawsBackground="YES" usesSingleLineMode="YES" id="pLP-pL-5R5">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
@@ -66,7 +65,7 @@
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="dNV-oD-vzR">
<rect key="frame" x="18" y="63" width="50" height="17"/>
<rect key="frame" x="18" y="64" width="50" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Folder:" id="Kwx-7B-CIu">
<font key="font" metaFont="systemBold"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@@ -77,7 +76,7 @@
<rect key="frame" x="72" y="58" width="391" height="25"/>
<popUpButtonCell key="cell" type="push" title="Item 1" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="tLJ-zY-CcZ" id="0cM-5q-Snl">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<font key="font" metaFont="system"/>
<menu key="menu" id="OpL-Uf-woJ">
<items>
<menuItem title="Item 1" state="on" id="tLJ-zY-CcZ"/>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="15705" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15705"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="16096"/>
</dependencies>
<scenes>
<!--Application-->
@@ -70,7 +70,13 @@
<items>
<menuItem title="New Feed" keyEquivalent="n" id="Was-JA-tGl">
<connections>
<action selector="showAddFeedWindow:" target="Ady-hI-5gd" id="LkT-kx-aCR"/>
<action selector="showAddWebFeedWindow:" target="Ady-hI-5gd" id="LkT-kx-aCR"/>
</connections>
</menuItem>
<menuItem title="New Twitter Feed" id="Wlk-34-AUR">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="showAddTwitterFeedWindow:" target="Ady-hI-5gd" id="9gI-jL-Hmv"/>
</connections>
</menuItem>
<menuItem title="New Folder" keyEquivalent="N" id="wkh-LX-Xp1">

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="15505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15505"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="16096"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@@ -59,7 +59,7 @@
</buttonCell>
</button>
<connections>
<action selector="showAddFeedWindow:" target="Oky-zY-oP4" id="pEy-MV-Lnd"/>
<action selector="showAddWebFeedWindow:" target="Oky-zY-oP4" id="pEy-MV-Lnd"/>
</connections>
</toolbarItem>
<toolbarItem implicitItemIdentifier="25C9E98A-867B-4EE2-BC1A-7B453D6B40BF" label="New Folder" paletteLabel="New Folder" toolTip="New Folder" image="newFolder" id="st0-Wp-nPK" customClass="RSToolbarItem" customModule="RSCore">
@@ -318,7 +318,7 @@
<rect key="frame" x="0.0" y="0.0" width="166" height="283"/>
<clipView key="contentView" drawsBackground="NO" copiesOnScroll="NO" id="2eU-Wz-F9g">
<rect key="frame" x="0.0" y="0.0" width="166" height="283"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<outlineView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="firstColumnOnly" selectionHighlightStyle="sourceList" columnReordering="NO" columnResizing="NO" autosaveColumns="NO" typeSelect="NO" rowHeight="26" viewBased="YES" floatsGroupRows="NO" indentationPerLevel="23" outlineTableColumn="ih9-mJ-EA7" id="cnV-kg-Dn2" customClass="SidebarOutlineView" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="167" height="283"/>
@@ -329,7 +329,6 @@
<tableColumns>
<tableColumn width="164" minWidth="23" maxWidth="1000" id="ih9-mJ-EA7">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border">
<font key="font" metaFont="menu" size="11"/>
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
@@ -470,7 +469,7 @@
</objects>
<point key="canvasLocation" x="-74" y="-186.5"/>
</scene>
<!--Timeline View Controller-->
<!--Timeline Container View Controller-->
<scene sceneID="zUD-i8-QYC">
<objects>
<viewController id="36G-bQ-b96" customClass="TimelineContainerViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
@@ -485,7 +484,7 @@
</constraints>
<popUpButtonCell key="cell" type="recessed" title="Sort" bezelStyle="recessed" alignment="center" lineBreakMode="truncatingTail" borderStyle="border" tag="1" imageScaling="proportionallyDown" inset="2" pullsDown="YES" id="bl0-6I-cH2">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES" changeBackground="YES" changeGray="YES"/>
<font key="font" metaFont="menu" size="11"/>
<font key="font" metaFont="controlContent" size="11"/>
<menu key="menu" id="dN0-S2-uqU">
<items>
<menuItem title="Sort" tag="1" hidden="YES" id="4BZ-ya-evy">

View File

@@ -1,8 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="14868" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="mPU-HG-I4u">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="mPU-HG-I4u">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14868"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="16096"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@@ -375,7 +374,7 @@
<userDefaultsController id="Y8q-yi-F5Z"/>
<userDefaultsController id="mV3-0T-XFc"/>
</objects>
<point key="canvasLocation" x="-29" y="455.5"/>
<point key="canvasLocation" x="-36" y="422"/>
</scene>
<!--Accounts Preferences View Controller-->
<scene sceneID="Rsj-41-ZOj">
@@ -385,17 +384,17 @@
<rect key="frame" x="0.0" y="0.0" width="450" height="299"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="7UM-iq-OLB" customClass="AccountsTableViewBackgroundView" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="44" width="160" height="233"/>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="7UM-iq-OLB" customClass="PreferencesTableViewBackgroundView" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="44" width="160" height="223"/>
<subviews>
<scrollView borderType="none" autohidesScrollers="YES" horizontalLineScroll="26" horizontalPageScroll="10" verticalLineScroll="26" verticalPageScroll="10" hasHorizontalScroller="NO" horizontalScrollElasticity="none" translatesAutoresizingMaskIntoConstraints="NO" id="PaF-du-r3c">
<rect key="frame" x="1" y="0.0" width="158" height="232"/>
<rect key="frame" x="1" y="0.0" width="158" height="222"/>
<clipView key="contentView" id="cil-Gq-akO">
<rect key="frame" x="0.0" y="0.0" width="158" height="232"/>
<rect key="frame" x="0.0" y="0.0" width="158" height="222"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" columnReordering="NO" columnSelection="YES" columnResizing="NO" multipleSelection="NO" autosaveColumns="NO" rowHeight="24" viewBased="YES" id="aTp-KR-y6b">
<rect key="frame" x="0.0" y="0.0" width="159" height="232"/>
<rect key="frame" x="0.0" y="0.0" width="159" height="222"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<size key="intercellSpacing" width="3" height="2"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
@@ -403,7 +402,6 @@
<tableColumns>
<tableColumn width="156" minWidth="40" maxWidth="1000" id="JSx-yi-vwt">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border">
<font key="font" metaFont="controlContent" size="11"/>
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
@@ -499,11 +497,11 @@
<action selector="removeAccount:" target="z5c-Js-Up9" id="APC-9C-TC7"/>
</connections>
</button>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="1gP-iQ-hAV" customClass="AccountsControlsBackgroundView" customModule="NetNewsWire" customModuleProvider="target">
<customView translatesAutoresizingMaskIntoConstraints="NO" id="1gP-iQ-hAV" customClass="PreferencesControlsBackgroundView" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="83" y="20" width="97" height="24"/>
</customView>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="Y7D-xQ-wep">
<rect key="frame" x="188" y="20" width="242" height="257"/>
<rect key="frame" x="188" y="20" width="242" height="247"/>
</customView>
</subviews>
<constraints>
@@ -534,7 +532,7 @@
</viewController>
<customObject id="AgZ-2t-A2h" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-568" y="727"/>
<point key="canvasLocation" x="-558" y="806"/>
</scene>
<!--Container-->
<scene sceneID="fzS-hg-3TF">
@@ -549,6 +547,160 @@
</objects>
<point key="canvasLocation" x="-44" y="27"/>
</scene>
<!--Extension Point Preferences View Controller-->
<scene sceneID="2Q8-nu-xsg">
<objects>
<viewController storyboardIdentifier="Extensions" id="K4Z-qS-hrR" customClass="ExtensionPointPreferencesViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" misplaced="YES" id="Jpa-aD-PZF">
<rect key="frame" x="0.0" y="0.0" width="450" height="299"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="pjs-G4-byk" customClass="PreferencesTableViewBackgroundView" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="44" width="160" height="216"/>
<subviews>
<scrollView borderType="none" autohidesScrollers="YES" horizontalLineScroll="26" horizontalPageScroll="10" verticalLineScroll="26" verticalPageScroll="10" hasHorizontalScroller="NO" horizontalScrollElasticity="none" translatesAutoresizingMaskIntoConstraints="NO" id="29T-r2-ckC">
<rect key="frame" x="1" y="0.0" width="158" height="215"/>
<clipView key="contentView" id="dXw-GY-TP8">
<rect key="frame" x="0.0" y="0.0" width="158" height="215"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" columnReordering="NO" columnSelection="YES" columnResizing="NO" multipleSelection="NO" autosaveColumns="NO" rowHeight="24" viewBased="YES" id="dfn-Vn-oDp">
<rect key="frame" x="0.0" y="0.0" width="159" height="215"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<size key="intercellSpacing" width="3" height="2"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
<tableColumns>
<tableColumn width="156" minWidth="40" maxWidth="1000" id="jBM-96-TEB">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" title="Text Cell" id="uax-iF-gzP">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="Cell" id="xQs-6E-Kpy">
<rect key="frame" x="1" y="1" width="156" height="17"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kmG-vw-CbN">
<rect key="frame" x="3" y="0.0" width="17" height="17"/>
<constraints>
<constraint firstAttribute="height" constant="16" id="qC6-Mb-6EQ"/>
<constraint firstAttribute="width" constant="16" id="yi0-bd-XJq"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="NSActionTemplate" id="OVD-Jo-TXU"/>
</imageView>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" allowsExpansionToolTips="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6cr-cB-qAN">
<rect key="frame" x="26" y="1" width="126" height="16"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="goO-QG-kk7">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="6cr-cB-qAN" firstAttribute="centerY" secondItem="xQs-6E-Kpy" secondAttribute="centerY" id="5Zo-fV-HYU"/>
<constraint firstItem="kmG-vw-CbN" firstAttribute="leading" secondItem="xQs-6E-Kpy" secondAttribute="leading" constant="6" id="Ap0-bW-xsi"/>
<constraint firstAttribute="trailing" secondItem="6cr-cB-qAN" secondAttribute="trailing" constant="6" id="O9X-RU-yVU"/>
<constraint firstItem="kmG-vw-CbN" firstAttribute="centerY" secondItem="xQs-6E-Kpy" secondAttribute="centerY" id="a9O-2C-ez9"/>
<constraint firstItem="6cr-cB-qAN" firstAttribute="leading" secondItem="kmG-vw-CbN" secondAttribute="trailing" constant="6" id="yfP-7k-Uyb"/>
</constraints>
<connections>
<outlet property="imageView" destination="kmG-vw-CbN" id="Ktw-eT-QtE"/>
<outlet property="textField" destination="6cr-cB-qAN" id="7Cb-o3-IeP"/>
</connections>
</tableCellView>
</prototypeCellViews>
</tableColumn>
</tableColumns>
</tableView>
</subviews>
</clipView>
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="0n8-KN-h13">
<rect key="frame" x="-100" y="-100" width="118" height="16"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="t4w-hp-8WD">
<rect key="frame" x="224" y="17" width="15" height="102"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
</subviews>
<constraints>
<constraint firstAttribute="width" constant="160" id="0gU-oR-pQf"/>
<constraint firstAttribute="bottom" secondItem="29T-r2-ckC" secondAttribute="bottom" id="BMY-9E-vH2"/>
<constraint firstAttribute="trailing" secondItem="29T-r2-ckC" secondAttribute="trailing" constant="1" id="dAW-1i-3iD"/>
<constraint firstItem="29T-r2-ckC" firstAttribute="top" secondItem="pjs-G4-byk" secondAttribute="top" constant="1" id="tAi-6L-Tjj"/>
<constraint firstItem="29T-r2-ckC" firstAttribute="leading" secondItem="pjs-G4-byk" secondAttribute="leading" constant="1" id="wXE-ze-ubv"/>
</constraints>
</customView>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JA2-UT-8DR">
<rect key="frame" x="20" y="19" width="32" height="26"/>
<constraints>
<constraint firstAttribute="height" constant="24" id="Qnm-eZ-2KJ"/>
<constraint firstAttribute="width" constant="32" id="ZQY-kS-9lY"/>
</constraints>
<buttonCell key="cell" type="smallSquare" bezelStyle="smallSquare" image="NSAddTemplate" imagePosition="only" alignment="center" lineBreakMode="truncatingTail" state="on" borderStyle="border" inset="2" id="xk0-JH-jr9">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="enableExtensionPoints:" target="K4Z-qS-hrR" id="Jlk-S5-Kam"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="jfX-DL-TXs">
<rect key="frame" x="51" y="19" width="32" height="26"/>
<buttonCell key="cell" type="smallSquare" bezelStyle="smallSquare" image="NSRemoveTemplate" imagePosition="overlaps" alignment="center" lineBreakMode="truncatingTail" enabled="NO" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="4FB-KH-Ton">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="disableExtensionPoint:" target="K4Z-qS-hrR" id="Red-pz-FUE"/>
</connections>
</button>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="sak-nS-Xfu" customClass="PreferencesControlsBackgroundView" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="83" y="20" width="97" height="24"/>
</customView>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="N1N-pE-gBL">
<rect key="frame" x="188" y="20" width="242" height="240"/>
</customView>
</subviews>
<constraints>
<constraint firstItem="JA2-UT-8DR" firstAttribute="leading" secondItem="Jpa-aD-PZF" secondAttribute="leading" constant="20" symbolic="YES" id="0PA-e9-b64"/>
<constraint firstAttribute="bottom" secondItem="jfX-DL-TXs" secondAttribute="bottom" constant="20" symbolic="YES" id="2V0-KT-vWv"/>
<constraint firstItem="pjs-G4-byk" firstAttribute="top" secondItem="Jpa-aD-PZF" secondAttribute="top" constant="20" symbolic="YES" id="4j8-1U-WiI"/>
<constraint firstItem="sak-nS-Xfu" firstAttribute="trailing" secondItem="pjs-G4-byk" secondAttribute="trailing" id="9u7-Nb-S9f"/>
<constraint firstAttribute="bottom" secondItem="JA2-UT-8DR" secondAttribute="bottom" constant="20" symbolic="YES" id="Ebi-1b-c4r"/>
<constraint firstItem="jfX-DL-TXs" firstAttribute="width" secondItem="JA2-UT-8DR" secondAttribute="width" id="Ilb-x9-dYo"/>
<constraint firstItem="pjs-G4-byk" firstAttribute="leading" secondItem="Jpa-aD-PZF" secondAttribute="leading" constant="20" symbolic="YES" id="JGH-r5-Umj"/>
<constraint firstItem="JA2-UT-8DR" firstAttribute="top" secondItem="pjs-G4-byk" secondAttribute="bottom" id="JhX-tK-MxJ"/>
<constraint firstAttribute="trailing" secondItem="N1N-pE-gBL" secondAttribute="trailing" constant="20" symbolic="YES" id="MaO-sk-c4U"/>
<constraint firstItem="sak-nS-Xfu" firstAttribute="height" secondItem="jfX-DL-TXs" secondAttribute="height" id="Nnu-tE-a42"/>
<constraint firstItem="jfX-DL-TXs" firstAttribute="leading" secondItem="JA2-UT-8DR" secondAttribute="trailing" constant="-1" id="T2Z-VL-HDw"/>
<constraint firstItem="N1N-pE-gBL" firstAttribute="leading" secondItem="pjs-G4-byk" secondAttribute="trailing" constant="8" symbolic="YES" id="TkH-1v-Rt4"/>
<constraint firstItem="sak-nS-Xfu" firstAttribute="leading" secondItem="jfX-DL-TXs" secondAttribute="trailing" id="YNy-26-lR2"/>
<constraint firstItem="N1N-pE-gBL" firstAttribute="top" secondItem="Jpa-aD-PZF" secondAttribute="top" constant="20" symbolic="YES" id="Z4D-bk-Smx"/>
<constraint firstItem="sak-nS-Xfu" firstAttribute="bottom" secondItem="jfX-DL-TXs" secondAttribute="bottom" id="ofz-UJ-8BL"/>
<constraint firstItem="JA2-UT-8DR" firstAttribute="height" secondItem="jfX-DL-TXs" secondAttribute="height" id="vOo-mW-auf"/>
<constraint firstAttribute="bottom" secondItem="N1N-pE-gBL" secondAttribute="bottom" constant="20" symbolic="YES" id="vyg-BP-5bk"/>
</constraints>
</view>
<connections>
<outlet property="deleteButton" destination="jfX-DL-TXs" id="gT1-Dt-vZL"/>
<outlet property="detailView" destination="N1N-pE-gBL" id="PYj-cW-fz1"/>
<outlet property="tableView" destination="dfn-Vn-oDp" id="heh-fs-Tqr"/>
</connections>
</viewController>
<customObject id="Cne-wm-w1Q" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-36" y="806"/>
</scene>
</scenes>
<resources>
<image name="NSActionTemplate" width="14" height="14"/>

View File

@@ -29,23 +29,32 @@ class AddFeedController: AddFeedWindowControllerDelegate {
private var titleFromFeed: String?
init(hostWindow: NSWindow) {
self.hostWindow = hostWindow
}
func showAddFeedSheet(_ urlString: String?, _ name: String?, _ account: Account?, _ folder: Folder?) {
func showAddFeedSheet(_ type: AddFeedWindowControllerType, _ urlString: String? = nil, _ name: String? = nil, _ account: Account? = nil, _ folder: Folder? = nil) {
let folderTreeControllerDelegate = FolderTreeControllerDelegate()
let folderTreeController = TreeController(delegate: folderTreeControllerDelegate)
addFeedWindowController = AddFeedWindowController(urlString: urlString ?? urlStringFromPasteboard, name: name, account: account, folder: folder, folderTreeController: folderTreeController, delegate: self)
switch type {
case .webFeed:
addFeedWindowController = AddWebFeedWindowController(urlString: urlString ?? urlStringFromPasteboard,
name: name,
account: account,
folder: folder,
folderTreeController: folderTreeController,
delegate: self)
case .twitterFeed:
addFeedWindowController = AddTwitterFeedWindowController(folderTreeController: folderTreeController,
delegate: self)
}
addFeedWindowController!.runSheetOnWindow(hostWindow)
}
// MARK: AddFeedWindowControllerDelegate
func addFeedWindowController(_: AddFeedWindowController, userEnteredURL url: URL, userEnteredTitle title: String?, container: Container) {
closeAddFeedSheet(NSApplication.ModalResponse.OK)
guard let accountAndFolderSpecifier = accountAndFolderFromContainer(container) else {
@@ -81,11 +90,9 @@ class AddFeedController: AddFeedWindowControllerDelegate {
}
beginShowingProgress()
}
func addFeedWindowControllerUserDidCancel(_: AddFeedWindowController) {
closeAddFeedSheet(NSApplication.ModalResponse.cancel)
}
@@ -106,7 +113,6 @@ private extension AddFeedController {
}
func accountAndFolderFromContainer(_ container: Container) -> AccountAndFolderSpecifier? {
if let account = container as? Account {
return AccountAndFolderSpecifier(account: account, folder: nil)
}
@@ -117,7 +123,6 @@ private extension AddFeedController {
}
func closeAddFeedSheet(_ returnCode: NSApplication.ModalResponse) {
if let sheetWindow = addFeedWindowController?.window {
hostWindow.endSheet(sheetWindow, returnCode: returnCode)
}
@@ -126,17 +131,14 @@ private extension AddFeedController {
// MARK: Errors
func showAlreadySubscribedError(_ urlString: String) {
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = NSLocalizedString("Already subscribed", comment: "Feed finder")
alert.informativeText = NSLocalizedString("Cant add this feed because youve already subscribed to it.", comment: "Feed finder")
alert.beginSheetModal(for: hostWindow)
}
func showInitialDownloadError(_ error: Error) {
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = NSLocalizedString("Download Error", comment: "Feed finder")
@@ -144,31 +146,27 @@ private extension AddFeedController {
let formatString = NSLocalizedString("Cant add this feed because of a download error: “%@”", comment: "Feed finder")
let errorText = NSString.localizedStringWithFormat(formatString as NSString, error.localizedDescription)
alert.informativeText = errorText as String
alert.beginSheetModal(for: hostWindow)
}
func showNoFeedsErrorMessage() {
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = NSLocalizedString("Feed not found", comment: "Feed finder")
alert.informativeText = NSLocalizedString("Cant add a feed because no feed was found.", comment: "Feed finder")
alert.beginSheetModal(for: hostWindow)
}
// MARK: Progress
func beginShowingProgress() {
runIndeterminateProgressWithMessage(NSLocalizedString("Finding feed…", comment:"Feed finder"))
}
func endShowingProgress() {
stopIndeterminateProgress()
hostWindow.makeKeyAndOrderFront(self)
}
}

View File

@@ -0,0 +1,30 @@
//
// AddFeedWIndowController.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/21/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
import Account
enum AddFeedWindowControllerType {
case webFeed
case twitterFeed
}
protocol AddFeedWindowControllerDelegate: class {
// userEnteredURL will have already been validated and normalized.
func addFeedWindowController(_: AddFeedWindowController, userEnteredURL: URL, userEnteredTitle: String?, container: Container)
func addFeedWindowControllerUserDidCancel(_: AddFeedWindowController)
}
protocol AddFeedWindowController {
var window: NSWindow? { get }
func runSheetOnWindow(_ hostWindow: NSWindow)
}

View File

@@ -0,0 +1,198 @@
//
// AddTwitterFeedWindowController.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/21/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import AppKit
import RSCore
import RSTree
import Articles
import Account
class AddTwitterFeedWindowController : NSWindowController, AddFeedWindowController {
@IBOutlet weak var typePopupButton: NSPopUpButton!
@IBOutlet weak var typeDescriptionLabel: NSTextField!
@IBOutlet weak var accountLabel: NSTextField!
@IBOutlet weak var accountPopupButton: NSPopUpButton!
@IBOutlet weak var screenSearchTextField: NSTextField!
@IBOutlet var nameTextField: NSTextField!
@IBOutlet var addButton: NSButton!
@IBOutlet var folderPopupButton: NSPopUpButton!
private var urlString: String?
private var initialName: String?
private weak var initialAccount: Account?
private var initialFolder: Folder?
private weak var delegate: AddFeedWindowControllerDelegate?
private var folderTreeController: TreeController!
private var userEnteredScreenSearch: String? {
var s = screenSearchTextField.stringValue
s = s.collapsingWhitespace
if s.isEmpty {
return nil
}
return s
}
private var userEnteredTitle: String? {
var s = nameTextField.stringValue
s = s.collapsingWhitespace
if s.isEmpty {
return nil
}
return s
}
var hostWindow: NSWindow!
convenience init(folderTreeController: TreeController, delegate: AddFeedWindowControllerDelegate?) {
self.init(windowNibName: NSNib.Name("AddTwitterFeedSheet"))
self.folderTreeController = folderTreeController
self.delegate = delegate
}
func runSheetOnWindow(_ hostWindow: NSWindow) {
hostWindow.beginSheet(window!) { (returnCode: NSApplication.ModalResponse) -> Void in
}
}
override func windowDidLoad() {
let accountMenu = NSMenu()
for feedProvider in ExtensionPointManager.shared.activeFeedProviders {
if let twitterFeedProvider = feedProvider as? TwitterFeedProvider {
let accountMenuItem = NSMenuItem()
accountMenuItem.title = "@\(twitterFeedProvider.screenName)"
accountMenu.addItem(accountMenuItem)
}
}
accountPopupButton.menu = accountMenu
folderPopupButton.menu = FolderTreeMenu.createFolderPopupMenu(with: folderTreeController.rootNode)
if let container = AddWebFeedDefaultContainer.defaultContainer {
if let folder = container as? Folder, let account = folder.account {
FolderTreeMenu.select(account: account, folder: folder, in: folderPopupButton)
} else {
if let account = container as? Account {
FolderTreeMenu.select(account: account, folder: nil, in: folderPopupButton)
}
}
}
updateUI()
}
// MARK: Actions
@IBAction func selectedType(_ sender: Any) {
screenSearchTextField.stringValue = ""
updateUI()
}
@IBAction func cancel(_ sender: Any?) {
cancelSheet()
}
@IBAction func addFeed(_ sender: Any?) {
guard let type = TwitterFeedType(rawValue: typePopupButton.selectedItem?.tag ?? 0),
let atUsername = accountPopupButton.selectedItem?.title else { return }
let username = String(atUsername[atUsername.index(atUsername.startIndex, offsetBy: 1)..<atUsername.endIndex])
var screenSearch = userEnteredScreenSearch
if let screenName = screenSearch, type == .screenName && screenName.starts(with: "@") {
screenSearch = String(screenName[screenName.index(screenName.startIndex, offsetBy: 1)..<screenName.endIndex])
}
guard let url = TwitterFeedProvider.buildURL(type, username: username, screenName: screenSearch, searchField: screenSearch) else { return }
let container = selectedContainer()!
AddWebFeedDefaultContainer.saveDefaultContainer(container)
delegate?.addFeedWindowController(self, userEnteredURL: url, userEnteredTitle: userEnteredTitle, container: container)
}
}
extension AddTwitterFeedWindowController: NSTextFieldDelegate {
func controlTextDidChange(_ obj: Notification) {
updateUI()
}
}
private extension AddTwitterFeedWindowController {
private func updateUI() {
switch typePopupButton.selectedItem?.tag ?? 0 {
case 0:
accountLabel.isHidden = false
accountPopupButton.isHidden = false
typeDescriptionLabel.stringValue = NSLocalizedString("Tweets from everyone you follow", comment: "Home Timeline")
screenSearchTextField.isHidden = true
addButton.isEnabled = true
case 1:
accountLabel.isHidden = false
accountPopupButton.isHidden = false
typeDescriptionLabel.stringValue = NSLocalizedString("Tweets mentioning you", comment: "Mentions")
screenSearchTextField.isHidden = true
addButton.isEnabled = true
case 2:
accountLabel.isHidden = true
accountPopupButton.isHidden = true
var screenSearch = userEnteredScreenSearch
if screenSearch != nil {
if let screenName = screenSearch, screenName.starts(with: "@") {
screenSearch = String(screenName[screenName.index(screenName.startIndex, offsetBy: 1)..<screenName.endIndex])
}
typeDescriptionLabel.stringValue = NSLocalizedString("Tweets from @\(screenSearch!)", comment: "Home Timeline")
} else {
typeDescriptionLabel.stringValue = ""
}
screenSearchTextField.placeholderString = NSLocalizedString("@name", comment: "@name")
screenSearchTextField.isHidden = false
addButton.isEnabled = !screenSearchTextField.stringValue.isEmpty
default:
accountLabel.isHidden = true
accountPopupButton.isHidden = true
if !screenSearchTextField.stringValue.isEmpty {
typeDescriptionLabel.stringValue = NSLocalizedString("Tweets that contain \(screenSearchTextField.stringValue)", comment: "Home Timeline")
} else {
typeDescriptionLabel.stringValue = ""
}
screenSearchTextField.placeholderString = nil
screenSearchTextField.isHidden = false
addButton.isEnabled = !screenSearchTextField.stringValue.isEmpty
}
}
func cancelSheet() {
delegate?.addFeedWindowControllerUserDidCancel(self)
}
func selectedContainer() -> Container? {
return folderPopupButton.selectedItem?.representedObject as? Container
}
}

View File

@@ -12,15 +12,7 @@ import RSTree
import Articles
import Account
protocol AddFeedWindowControllerDelegate: class {
// userEnteredURL will have already been validated and normalized.
func addFeedWindowController(_: AddFeedWindowController, userEnteredURL: URL, userEnteredTitle: String?, container: Container)
func addFeedWindowControllerUserDidCancel(_: AddFeedWindowController)
}
class AddFeedWindowController : NSWindowController {
class AddWebFeedWindowController : NSWindowController, AddFeedWindowController {
@IBOutlet var urlTextField: NSTextField!
@IBOutlet var nameTextField: NSTextField!
@@ -46,7 +38,7 @@ class AddFeedWindowController : NSWindowController {
var hostWindow: NSWindow!
convenience init(urlString: String?, name: String?, account: Account?, folder: Folder?, folderTreeController: TreeController, delegate: AddFeedWindowControllerDelegate?) {
self.init(windowNibName: NSNib.Name("AddFeedSheet"))
self.init(windowNibName: NSNib.Name("AddWebFeedSheet"))
self.urlString = urlString
self.initialName = name
self.initialAccount = account
@@ -127,7 +119,7 @@ class AddFeedWindowController : NSWindowController {
}
}
private extension AddFeedWindowController {
private extension AddWebFeedWindowController {
private func updateUI() {
addButton.isEnabled = urlTextField.stringValue.mayBeURL

View File

@@ -1,13 +1,15 @@
// Add the mouse listeners for the above functions
function linkHover() {
window.onmouseover = function(event) {
if (event.target.matches('a')) {
window.webkit.messageHandlers.mouseDidEnter.postMessage(event.target.href);
var closestAnchor = event.target.closest('a')
if (closestAnchor) {
window.webkit.messageHandlers.mouseDidEnter.postMessage(closestAnchor.href);
}
}
window.onmouseout = function(event) {
if (event.target.matches('a')) {
window.webkit.messageHandlers.mouseDidExit.postMessage(event.target.href);
var closestAnchor = event.target.closest('a')
if (closestAnchor) {
window.webkit.messageHandlers.mouseDidExit.postMessage(closestAnchor.href);
}
}
}

View File

@@ -18,25 +18,20 @@ import RSCore
}
func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, sharingServicesForItems items: [Any], proposedSharingServices proposedServices: [NSSharingService]) -> [NSSharingService] {
return proposedServices + SharingServicePickerDelegate.customSharingServices(for: items)
}
func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, delegateFor sharingService: NSSharingService) -> NSSharingServiceDelegate? {
return sharingServiceDelegate
}
private static let sendToCommands: [SendToCommand] = {
return [SendToMicroBlogCommand(), SendToMarsEditCommand()]
}()
static func customSharingServices(for items: [Any]) -> [NSSharingService] {
let customServices = sendToCommands.compactMap { (sendToCommand) -> NSSharingService? in
let customServices = ExtensionPointManager.shared.activeSendToCommands.compactMap { (sendToCommand) -> NSSharingService? in
guard let object = items.first else {
return nil
}
guard sendToCommand.canSendObject(object, selectedText: nil) else {
return nil
}

View File

@@ -570,11 +570,11 @@ private extension SidebarOutlineDataSource {
// Show the add-feed sheet.
if let account = parentNode.representedObject as? Account {
appDelegate.addFeed(draggedFeed.url, name: draggedFeed.editedName ?? draggedFeed.name, account: account, folder: nil)
appDelegate.addWebFeed(draggedFeed.url, name: draggedFeed.editedName ?? draggedFeed.name, account: account, folder: nil)
} else {
let account = parentNode.parent?.representedObject as? Account
let folder = parentNode.representedObject as? Folder
appDelegate.addFeed(draggedFeed.url, name: draggedFeed.editedName ?? draggedFeed.name, account: account, folder: folder)
appDelegate.addWebFeed(draggedFeed.url, name: draggedFeed.editedName ?? draggedFeed.name, account: account, folder: folder)
}
return true

View File

@@ -129,7 +129,7 @@ private extension SidebarViewController {
let menu = NSMenu(title: "")
menu.addItem(withTitle: NSLocalizedString("New Feed", comment: "Command"), action: #selector(AppDelegate.showAddFeedWindow(_:)), keyEquivalent: "")
menu.addItem(withTitle: NSLocalizedString("New Feed", comment: "Command"), action: #selector(AppDelegate.showAddWebFeedWindow(_:)), keyEquivalent: "")
menu.addItem(withTitle: NSLocalizedString("New Folder", comment: "Command"), action: #selector(AppDelegate.showAddFolderWindow(_:)), keyEquivalent: "")
return menu

View File

@@ -15,14 +15,15 @@ struct TimelineCellData {
let text: String
let dateString: String
let feedName: String
let showFeedName: Bool
let byline: String
let showFeedName: TimelineShowFeedName
let iconImage: IconImage? // feed icon, user avatar, or favicon
let showIcon: Bool // Make space even when icon is nil
let featuredImage: NSImage? // image from within the article
let read: Bool
let starred: Bool
init(article: Article, showFeedName: Bool, feedName: String?, iconImage: IconImage?, showIcon: Bool, featuredImage: NSImage?) {
init(article: Article, showFeedName: TimelineShowFeedName, feedName: String?, byline: String?, iconImage: IconImage?, showIcon: Bool, featuredImage: NSImage?) {
self.title = ArticleStringFormatter.truncatedTitle(article)
self.text = ArticleStringFormatter.truncatedSummary(article)
@@ -31,10 +32,15 @@ struct TimelineCellData {
if let feedName = feedName {
self.feedName = ArticleStringFormatter.truncatedFeedName(feedName)
}
else {
} else {
self.feedName = ""
}
if let byline = byline {
self.byline = byline
} else {
self.byline = ""
}
self.showFeedName = showFeedName
@@ -51,7 +57,8 @@ struct TimelineCellData {
self.text = ""
self.dateString = ""
self.feedName = ""
self.showFeedName = false
self.byline = ""
self.showFeedName = .none
self.showIcon = false
self.iconImage = nil
self.featuredImage = nil

View File

@@ -171,7 +171,7 @@ private extension TimelineCellLayout {
}
static func rectForFeedName(_ textBoxRect: NSRect, _ dateRect: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect {
if !cellData.showFeedName {
if cellData.showFeedName == .none {
return NSZeroRect
}

View File

@@ -248,11 +248,14 @@ private extension TimelineTableCellView {
}
func updateFeedNameView() {
if cellData.showFeedName {
switch cellData.showFeedName {
case .byline:
showView(feedNameView)
updateTextFieldText(feedNameView, cellData.byline)
case .feed:
showView(feedNameView)
updateTextFieldText(feedNameView, cellData.feedName)
}
else {
case .none:
hideView(feedNameView)
}
}

View File

@@ -18,6 +18,12 @@ protocol TimelineDelegate: class {
func timelineInvalidatedRestorationState(_: TimelineViewController)
}
enum TimelineShowFeedName {
case none
case byline
case feed
}
final class TimelineViewController: NSViewController, UndoableCommandRunner, UnreadCountProvider {
@IBOutlet var tableView: TimelineTableView!
@@ -41,23 +47,11 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
didSet {
if !representedObjectArraysAreEqual(oldValue, representedObjects) {
unreadCount = 0
if let representedObjects = representedObjects {
if representedObjects.count == 1 && representedObjects.first is WebFeed {
showFeedNames = false
}
else {
showFeedNames = true
}
}
else {
showFeedNames = false
}
selectionDidChange(nil)
if showsSearchResults {
fetchAndReplaceArticlesAsync()
}
else {
} else {
fetchAndReplaceArticlesSync()
if articles.count > 0 {
tableView.scrollRowToVisible(0)
@@ -85,9 +79,11 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
defer {
updateUnreadCount()
}
if articles == oldValue {
return
}
if articles.representSameArticlesInSameOrder(as: oldValue) {
// When the array is the same  same articles, same order
// but some data in some of the articles may have changed.
@@ -96,7 +92,20 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
reloadVisibleCells()
return
}
updateShowIcons()
if let representedObjects = representedObjects, representedObjects.count == 1 && representedObjects.first is WebFeed {
showFeedNames = {
for article in articles {
if !article.byline().isEmpty {
return .byline
}
}
return .none
}()
} else {
showFeedNames = .feed
}
articleRowMap = [String: Int]()
tableView.reloadData()
}
@@ -117,7 +126,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
private var articleRowMap = [String: Int]() // articleID: rowIndex
private var cellAppearance: TimelineCellAppearance!
private var cellAppearanceWithIcon: TimelineCellAppearance!
private var showFeedNames = false {
private var showFeedNames: TimelineShowFeedName = .none {
didSet {
if showFeedNames != oldValue {
updateShowIcons()
@@ -663,7 +672,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
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 prototypeCellData = TimelineCellData(article: prototypeArticle, showFeedName: true, feedName: "Prototype Feed Name", iconImage: nil, showIcon: false, featuredImage: nil)
let prototypeCellData = TimelineCellData(article: prototypeArticle, showFeedName: .feed, feedName: "Prototype Feed Name", byline: nil, iconImage: nil, showIcon: false, featuredImage: nil)
let height = TimelineCellLayout.height(for: 100, cellData: prototypeCellData, appearance: cellAppearance)
return height
}
@@ -810,7 +819,7 @@ extension TimelineViewController: NSTableViewDelegate {
private func configureTimelineCell(_ cell: TimelineTableCellView, article: Article) {
cell.objectValue = article
let iconImage = article.iconImage()
cell.cellData = TimelineCellData(article: article, showFeedName: showFeedNames, feedName: article.webFeed?.nameForDisplay, iconImage: iconImage, showIcon: showIcons, featuredImage: nil)
cell.cellData = TimelineCellData(article: article, showFeedName: showFeedNames, feedName: article.webFeed?.nameForDisplay, byline: article.byline(), iconImage: iconImage, showIcon: showIcons, featuredImage: nil)
}
private func iconFor(_ article: Article) -> IconImage? {
@@ -946,20 +955,25 @@ private extension TimelineViewController {
}
func updateShowIcons() {
if showFeedNames {
if showFeedNames == .feed {
self.showIcons = true
return
}
if showFeedNames == .none {
self.showIcons = false
return
}
for article in articles {
if let authors = article.authors {
for author in authors {
if author.avatarURL != nil {
self.showIcons = true
return
for author in authors {
if author.avatarURL != nil {
self.showIcons = true
return
}
}
}
}
}
self.showIcons = false

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="16096" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="16096"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@@ -21,13 +21,13 @@
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<font key="font" metaFont="system"/>
<tabViewItems>
<tabViewItem label="Account Information" identifier="" id="35c-I3-wfs">
<tabViewItem label="Account" identifier="" id="35c-I3-wfs">
<view key="view" id="ft2-Mb-5LD">
<rect key="frame" x="10" y="33" width="326" height="254"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<gridView xPlacement="fill" yPlacement="center" rowAlignment="none" rowSpacing="9" translatesAutoresizingMaskIntoConstraints="NO" id="nVy-H3-bFO">
<rect key="frame" x="20" y="103" width="286" height="131"/>
<rect key="frame" x="20" y="108" width="286" height="126"/>
<rows>
<gridRow id="yLs-SL-a1b"/>
<gridRow yPlacement="top" id="etw-2m-nWZ"/>
@@ -41,7 +41,7 @@
<gridCells>
<gridCell row="yLs-SL-a1b" column="sMM-Ds-SKX" id="3ea-DE-T3i">
<textField key="contentView" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="jiQ-KJ-SS0">
<rect key="frame" x="-2" y="114" width="44" height="17"/>
<rect key="frame" x="-2" y="110" width="44" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="Type:" id="tC5-Vt-gBc">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@@ -51,7 +51,7 @@
</gridCell>
<gridCell row="yLs-SL-a1b" column="Fhf-h9-g0O" id="baI-Kp-tKF">
<textField key="contentView" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="XYX-iz-hnq">
<rect key="frame" x="44" y="114" width="73" height="17"/>
<rect key="frame" x="44" y="110" width="73" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="On My Mac" id="6yI-bV-1Sh">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@@ -62,7 +62,7 @@
<gridCell row="etw-2m-nWZ" column="sMM-Ds-SKX" id="htf-Ca-Hpv"/>
<gridCell row="etw-2m-nWZ" column="Fhf-h9-g0O" id="NrD-vV-1Y1">
<button key="contentView" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mgt-uY-fuq">
<rect key="frame" x="44" y="89" width="60" height="18"/>
<rect key="frame" x="44" y="85" width="60" height="18"/>
<buttonCell key="cell" type="check" title="Active" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="wxB-dX-nGt">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
@@ -74,7 +74,7 @@
</gridCell>
<gridCell row="3IT-3r-gEK" column="sMM-Ds-SKX" id="2yP-oZ-A6S">
<textField key="contentView" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ted-jN-oYR">
<rect key="frame" x="-2" y="63" width="44" height="17"/>
<rect key="frame" x="-2" y="60" width="44" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="Name:" id="uyQ-Zi-QCr">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@@ -84,7 +84,7 @@
</gridCell>
<gridCell row="3IT-3r-gEK" column="Fhf-h9-g0O" id="nCq-02-YVv">
<textField key="contentView" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="TT0-Kf-YTC">
<rect key="frame" x="46" y="60" width="100" height="22"/>
<rect key="frame" x="46" y="57" width="100" height="21"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" drawsBackground="YES" id="7Vp-Hq-j6n">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
@@ -95,7 +95,7 @@
<gridCell row="Y4C-5M-ySp" column="sMM-Ds-SKX" id="dON-E7-yd2"/>
<gridCell row="Y4C-5M-ySp" column="Fhf-h9-g0O" id="i7Y-4k-5TF">
<textField key="contentView" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="xp5-wk-PKc">
<rect key="frame" x="44" y="0.0" width="244" height="51"/>
<rect key="frame" x="44" y="0.0" width="244" height="48"/>
<textFieldCell key="cell" selectable="YES" title="The name appears in the sidebar. It can be anything you want. You can even use emoji. 🎸" id="MW0-mH-Gaa">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
@@ -106,7 +106,7 @@
</gridCells>
</gridView>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="gLh-gl-ZGQ">
<rect key="frame" x="109" y="55" width="109" height="32"/>
<rect key="frame" x="109" y="60" width="109" height="32"/>
<buttonCell key="cell" type="push" title="Credentials" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="vYg-ZC-o4W">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>

View File

@@ -9,6 +9,7 @@
import AppKit
import Account
import RSWeb
import Secrets
class AccountsFeedWranglerWindowController: NSWindowController {
@IBOutlet weak var progressIndicator: NSProgressIndicator!

View File

@@ -9,6 +9,7 @@
import AppKit
import Account
import RSWeb
import Secrets
class AccountsFeedbinWindowController: NSWindowController {

View File

@@ -9,6 +9,7 @@
import AppKit
import Account
import RSWeb
import Secrets
class AccountsNewsBlurWindowController: NSWindowController {
@IBOutlet weak var progressIndicator: NSProgressIndicator!

View File

@@ -9,6 +9,7 @@
import AppKit
import Account
import RSWeb
import Secrets
class AccountsReaderAPIWindowController: NSWindowController {

View File

@@ -0,0 +1,109 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="16096" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="16096"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="ExtensionPointAddViewController" customModule="NetNewsWire" customModuleProvider="target">
<connections>
<outlet property="tableView" destination="lyM-Zu-Let" id="JDz-05-OOg"/>
<outlet property="view" destination="c22-O7-iKe" id="Vfr-rK-EHC"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customView id="c22-O7-iKe">
<rect key="frame" x="0.0" y="0.0" width="480" height="272"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<scrollView autohidesScrollers="YES" horizontalLineScroll="42" horizontalPageScroll="10" verticalLineScroll="42" verticalPageScroll="10" usesPredominantAxisScrolling="NO" id="y2z-6c-TH0">
<rect key="frame" x="0.0" y="0.0" width="480" height="272"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<clipView key="contentView" id="qCn-Bf-ICO">
<rect key="frame" x="1" y="1" width="478" height="270"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" columnReordering="NO" columnSelection="YES" columnResizing="NO" multipleSelection="NO" autosaveColumns="NO" rowHeight="40" rowSizeStyle="automatic" viewBased="YES" id="lyM-Zu-Let">
<rect key="frame" x="0.0" y="0.0" width="478" height="270"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<size key="intercellSpacing" width="3" height="2"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
<tableViewGridLines key="gridStyleMask" horizontal="YES"/>
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
<tableColumns>
<tableColumn width="475" minWidth="40" maxWidth="1000" id="SlU-lH-CzT">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" title="Text Cell" id="Nhn-I6-76l">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="Cell" id="EGi-CQ-lPc" customClass="ExtensionPointAddTableCellView" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="1" y="1" width="475" height="40"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView distribution="fill" orientation="horizontal" alignment="centerY" spacing="17" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="iCD-Yx-4V5">
<rect key="frame" x="20" y="8" width="173" height="24"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="KmN-Zk-TBU">
<rect key="frame" x="0.0" y="0.0" width="24" height="24"/>
<constraints>
<constraint firstAttribute="height" constant="24" id="dbz-aC-h0q"/>
<constraint firstAttribute="width" constant="24" id="jN0-Et-ysS"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="oGL-yl-27S"/>
</imageView>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="uyu-5W-IaW">
<rect key="frame" x="39" y="1" width="136" height="23"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="iOW-VJ-bkx">
<font key="font" metaFont="system" size="20"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
</subviews>
<constraints>
<constraint firstItem="iCD-Yx-4V5" firstAttribute="centerY" secondItem="EGi-CQ-lPc" secondAttribute="centerY" id="IS1-7W-BWY"/>
<constraint firstItem="iCD-Yx-4V5" firstAttribute="leading" secondItem="EGi-CQ-lPc" secondAttribute="leading" constant="20" id="IsY-WH-f93"/>
</constraints>
<connections>
<outlet property="imageView" destination="KmN-Zk-TBU" id="Tfy-Eb-Isb"/>
<outlet property="titleLabel" destination="uyu-5W-IaW" id="QAe-Gk-Eeo"/>
</connections>
</tableCellView>
</prototypeCellViews>
</tableColumn>
</tableColumns>
</tableView>
</subviews>
</clipView>
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="qOf-Dj-ubR">
<rect key="frame" x="1" y="119" width="223" height="15"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="XFQ-Xy-wny">
<rect key="frame" x="224" y="17" width="15" height="102"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
</subviews>
<point key="canvasLocation" x="139" y="154"/>
</customView>
</objects>
</document>

View File

@@ -0,0 +1,16 @@
//
// ExtensionPointAddTableCellView.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/6/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import AppKit
class ExtensionPointAddTableCellView: NSTableCellView {
@IBOutlet weak var templateImageView: NSImageView?
@IBOutlet weak var titleLabel: NSTextField?
}

View File

@@ -0,0 +1,79 @@
//
// ExtensionPointAddViewController.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/6/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import AppKit
class ExtensionPointAddViewController: NSViewController {
@IBOutlet weak var tableView: NSTableView!
private var availableExtensionPointTypes = [ExtensionPoint.Type]()
private var extensionPointAddWindowController: NSWindowController?
init() {
super.init(nibName: "ExtensionPointAdd", bundle: nil)
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
availableExtensionPointTypes = ExtensionPointManager.shared.availableExtensionPointTypes
}
}
// MARK: - NSTableViewDataSource
extension ExtensionPointAddViewController: NSTableViewDataSource {
func numberOfRows(in tableView: NSTableView) -> Int {
return availableExtensionPointTypes.count
}
func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
return nil
}
}
// MARK: - NSTableViewDelegate
extension ExtensionPointAddViewController: NSTableViewDelegate {
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
if let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "Cell"), owner: nil) as? ExtensionPointAddTableCellView {
let extensionPointType = availableExtensionPointTypes[row]
cell.titleLabel?.stringValue = extensionPointType.title
cell.imageView?.image = extensionPointType.templateImage
return cell
}
return nil
}
func tableViewSelectionDidChange(_ notification: Notification) {
let selectedRow = tableView.selectedRow
guard selectedRow != -1 else {
return
}
let extensionPointType = availableExtensionPointTypes[selectedRow]
let windowController = ExtensionPointEnableWindowController()
windowController.extensionPointType = extensionPointType
windowController.runSheetOnWindow(self.view.window!)
extensionPointAddWindowController = windowController
tableView.selectRowIndexes([], byExtendingSelection: false)
}
}

View File

@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="16096" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="16096"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="ExtensionPointDetailViewController" customModule="NetNewsWire" customModuleProvider="target">
<connections>
<outlet property="descriptionLabel" destination="tK2-QL-hvM" id="5jU-Vz-6us"/>
<outlet property="imageView" destination="I6P-Q2-DtA" id="mBe-xk-jOe"/>
<outlet property="titleLabel" destination="d0R-Cs-axs" id="axb-bi-iwe"/>
<outlet property="view" destination="988-TV-aJt" id="cUJ-Ez-XiC"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<tabView id="988-TV-aJt">
<rect key="frame" x="0.0" y="0.0" width="346" height="300"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<font key="font" metaFont="system"/>
<tabViewItems>
<tabViewItem label="Extension" identifier="" id="k6A-mz-zOF">
<view key="view" id="jT6-Hh-gWM">
<rect key="frame" x="10" y="33" width="326" height="254"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView distribution="fill" orientation="horizontal" alignment="bottom" spacing="19" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Z8D-OO-XZd">
<rect key="frame" x="85" y="208" width="157" height="30"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="I6P-Q2-DtA">
<rect key="frame" x="0.0" y="0.0" width="28" height="28"/>
<constraints>
<constraint firstAttribute="width" constant="28" id="HqU-9L-bqb"/>
<constraint firstAttribute="height" constant="28" id="bpI-uD-bzZ"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" image="NSAdvanced" id="iCo-JD-zZy"/>
</imageView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="d0R-Cs-axs">
<rect key="frame" x="45" y="0.0" width="114" height="30"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Extension" id="CGj-bV-rXW">
<font key="font" metaFont="system" size="26"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" textCompletion="NO" translatesAutoresizingMaskIntoConstraints="NO" id="tK2-QL-hvM">
<rect key="frame" x="31" y="176" width="264" height="16"/>
<constraints>
<constraint firstAttribute="width" constant="260" id="GRp-qY-UP1"/>
</constraints>
<textFieldCell key="cell" selectable="YES" allowsUndo="NO" alignment="left" allowsEditingTextAttributes="YES" id="7dt-TS-iHM">
<font key="font" metaFont="system"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="tK2-QL-hvM" firstAttribute="top" secondItem="Z8D-OO-XZd" secondAttribute="bottom" constant="16" id="3Ww-vg-yg7"/>
<constraint firstItem="Z8D-OO-XZd" firstAttribute="top" secondItem="jT6-Hh-gWM" secondAttribute="top" constant="16" id="3hP-9H-3IX"/>
<constraint firstItem="tK2-QL-hvM" firstAttribute="centerX" secondItem="jT6-Hh-gWM" secondAttribute="centerX" id="7ik-M6-Wmx"/>
<constraint firstItem="Z8D-OO-XZd" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="jT6-Hh-gWM" secondAttribute="leading" constant="8" id="U8D-q2-eyi"/>
<constraint firstItem="Z8D-OO-XZd" firstAttribute="centerX" secondItem="jT6-Hh-gWM" secondAttribute="centerX" id="XZC-Yp-uT5"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="Z8D-OO-XZd" secondAttribute="trailing" constant="8" id="pok-cn-NFH"/>
</constraints>
</view>
</tabViewItem>
</tabViewItems>
<point key="canvasLocation" x="-195" y="110"/>
</tabView>
</objects>
<resources>
<image name="NSAdvanced" width="32" height="32"/>
</resources>
</document>

View File

@@ -0,0 +1,37 @@
//
// ExtensionPointDetailViewController.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/8/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Cocoa
class ExtensionPointDetailViewController: NSViewController {
@IBOutlet weak var imageView: NSImageView!
@IBOutlet weak var titleLabel: NSTextField!
@IBOutlet weak var descriptionLabel: NSTextField!
private var extensionPointWindowController: NSWindowController?
private var extensionPoint: ExtensionPoint?
init(extensionPoint: ExtensionPoint) {
super.init(nibName: "ExtensionPointDetail", bundle: nil)
self.extensionPoint = extensionPoint
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func viewDidLoad() {
super.viewDidLoad()
guard let extensionPoint = extensionPoint else { return }
imageView.image = extensionPoint.templateImage
titleLabel.stringValue = extensionPoint.title
descriptionLabel.attributedStringValue = extensionPoint.description
}
}

View File

@@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="16096" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="16096"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="ExtensionPointEnableWindowController" customModule="NetNewsWire" customModuleProvider="target">
<connections>
<outlet property="descriptionLabel" destination="thC-ep-vXS" id="o9I-vp-z54"/>
<outlet property="enableButton" destination="sGb-z5-IdF" id="yNw-Nn-4Kq"/>
<outlet property="imageView" destination="LSA-B8-aGZ" id="AN5-t1-d52"/>
<outlet property="titleLabel" destination="iAC-tU-rvZ" id="vMx-2H-b44"/>
<outlet property="window" destination="HNe-Jr-kev" id="C8n-l1-WhI"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="HNe-Jr-kev">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="196" y="240" width="407" height="156"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1417"/>
<view key="contentView" wantsLayer="YES" id="qAd-AQ-5ue">
<rect key="frame" x="0.0" y="0.0" width="407" height="156"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView distribution="fill" orientation="horizontal" alignment="bottom" spacing="19" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="nLd-4a-dQg">
<rect key="frame" x="109" y="89" width="189" height="51"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="LSA-B8-aGZ">
<rect key="frame" x="0.0" y="0.0" width="36" height="36"/>
<constraints>
<constraint firstAttribute="width" constant="36" id="SuU-du-YHk"/>
<constraint firstAttribute="height" constant="36" id="qxc-dc-d8U"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" image="NSAdvanced" id="5qe-pZ-t40"/>
</imageView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="iAC-tU-rvZ">
<rect key="frame" x="53" y="13" width="138" height="38"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Extension" id="kuv-Xu-aIk">
<font key="font" metaFont="system" size="32"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="sGb-z5-IdF">
<rect key="frame" x="312" y="13" width="81" height="32"/>
<buttonCell key="cell" type="push" title="Enable" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="Oh8-q3-Aup">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
DQ
</string>
</buttonCell>
<connections>
<action selector="enable:" target="-2" id="BN5-u0-DNe"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="aKy-4s-WDM">
<rect key="frame" x="231" y="13" width="82" height="32"/>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="2nM-LA-6fh">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
Gw
</string>
</buttonCell>
<connections>
<action selector="cancel:" target="-2" id="WK9-uJ-mIw"/>
</connections>
</button>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" textCompletion="NO" translatesAutoresizingMaskIntoConstraints="NO" id="thC-ep-vXS">
<rect key="frame" x="52" y="57" width="304" height="16"/>
<constraints>
<constraint firstAttribute="width" constant="300" id="igx-s6-xe9"/>
</constraints>
<textFieldCell key="cell" selectable="YES" allowsUndo="NO" alignment="left" allowsEditingTextAttributes="YES" id="aUU-dO-RNt">
<font key="font" metaFont="system"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="iAC-tU-rvZ" firstAttribute="top" secondItem="qAd-AQ-5ue" secondAttribute="top" constant="16" id="Cxn-GQ-jzh"/>
<constraint firstAttribute="bottom" secondItem="sGb-z5-IdF" secondAttribute="bottom" constant="20" id="Moe-ce-JeY"/>
<constraint firstAttribute="trailing" secondItem="sGb-z5-IdF" secondAttribute="trailing" constant="20" id="OdS-3p-qyB"/>
<constraint firstItem="sGb-z5-IdF" firstAttribute="leading" secondItem="aKy-4s-WDM" secondAttribute="trailing" constant="11" id="QPh-zm-9uL"/>
<constraint firstItem="thC-ep-vXS" firstAttribute="centerX" secondItem="qAd-AQ-5ue" secondAttribute="centerX" id="fC4-fE-SyO"/>
<constraint firstItem="aKy-4s-WDM" firstAttribute="centerY" secondItem="sGb-z5-IdF" secondAttribute="centerY" id="naD-Tq-iwx"/>
<constraint firstItem="thC-ep-vXS" firstAttribute="top" secondItem="nLd-4a-dQg" secondAttribute="bottom" constant="16" id="qRM-G0-del"/>
<constraint firstItem="aKy-4s-WDM" firstAttribute="top" secondItem="thC-ep-vXS" secondAttribute="bottom" constant="16" id="vrt-3v-j4f"/>
<constraint firstItem="nLd-4a-dQg" firstAttribute="centerX" secondItem="qAd-AQ-5ue" secondAttribute="centerX" id="xXl-e5-lnN"/>
</constraints>
</view>
<connections>
<outlet property="delegate" destination="-2" id="fo9-G5-zJh"/>
</connections>
<point key="canvasLocation" x="103.5" y="89.5"/>
</window>
</objects>
<resources>
<image name="NSAdvanced" width="32" height="32"/>
</resources>
</document>

View File

@@ -0,0 +1,133 @@
//
// ExtensionPointEnableBasicWindowController.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/8/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Cocoa
import AuthenticationServices
import OAuthSwift
import Secrets
class ExtensionPointEnableWindowController: NSWindowController {
@IBOutlet weak var imageView: NSImageView!
@IBOutlet weak var titleLabel: NSTextField!
@IBOutlet weak var descriptionLabel: NSTextField!
private weak var hostWindow: NSWindow?
private let callbackURL = URL(string: "vincodennw://")!
private var oauth: OAuthSwift?
var extensionPointType: ExtensionPoint.Type?
convenience init() {
self.init(windowNibName: NSNib.Name("ExtensionPointEnableBasic"))
}
override func windowDidLoad() {
super.windowDidLoad()
guard let extensionPointType = extensionPointType else { return }
imageView.image = extensionPointType.templateImage
titleLabel.stringValue = extensionPointType.title
descriptionLabel.attributedStringValue = extensionPointType.description
}
// MARK: API
func runSheetOnWindow(_ hostWindow: NSWindow) {
self.hostWindow = hostWindow
hostWindow.beginSheet(window!)
}
// MARK: Actions
@IBAction func cancel(_ sender: Any) {
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel)
}
@IBAction func enable(_ sender: Any) {
guard let extensionPointType = extensionPointType else { return }
if let oauth1 = extensionPointType as? OAuth1SwiftProvider.Type {
enableOauth1(oauth1)
} else {
ExtensionPointManager.shared.activateExtensionPoint(extensionPointType)
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.OK)
}
}
}
extension ExtensionPointEnableWindowController: 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
if case ASWebAuthenticationSessionError.canceledLogin = error {
print("Login cancelled.")
} else {
NSApplication.shared.presentError(error)
}
})
session.presentationContextProvider = self
if !session.start() {
print("Session failed to start!!!")
}
}
}
extension ExtensionPointEnableWindowController: ASWebAuthenticationPresentationContextProviding {
public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return hostWindow!
}
}
private extension ExtensionPointEnableWindowController {
func enableOauth1(_ provider: OAuth1SwiftProvider.Type) {
let oauth1 = provider.oauth1Swift
self.oauth = oauth1
oauth1.authorizeURLHandler = self
oauth1.authorize(withCallbackURL: callbackURL) { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let tokenSuccess):
// let token = tokenSuccess.credential.oauthToken
// let secret = tokenSuccess.credential.oauthTokenSecret
let screenName = tokenSuccess.parameters["screen_name"] as? String ?? ""
print("******************* \(screenName)")
self.hostWindow!.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK)
case .failure(let oauthSwiftError):
NSApplication.shared.presentError(oauthSwiftError)
}
self.oauth?.cancel()
self.oauth = nil
}
}
}

View File

@@ -0,0 +1,135 @@
//
// ExtensionPointEnableWindowController.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/8/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Cocoa
import AuthenticationServices
import OAuthSwift
import Secrets
class ExtensionPointEnableWindowController: NSWindowController {
@IBOutlet weak var imageView: NSImageView!
@IBOutlet weak var titleLabel: NSTextField!
@IBOutlet weak var descriptionLabel: NSTextField!
@IBOutlet weak var enableButton: NSButton!
private weak var hostWindow: NSWindow?
private let callbackURL = URL(string: "netnewswire://")!
private var oauth: OAuthSwift?
var extensionPointType: ExtensionPoint.Type?
convenience init() {
self.init(windowNibName: NSNib.Name("ExtensionPointEnable"))
}
override func windowDidLoad() {
super.windowDidLoad()
guard let extensionPointType = extensionPointType else { return }
imageView.image = extensionPointType.templateImage
titleLabel.stringValue = extensionPointType.title
descriptionLabel.attributedStringValue = extensionPointType.description
}
// MARK: API
func runSheetOnWindow(_ hostWindow: NSWindow) {
self.hostWindow = hostWindow
hostWindow.beginSheet(window!)
}
// MARK: Actions
@IBAction func cancel(_ sender: Any) {
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel)
}
@IBAction func enable(_ sender: Any) {
guard let extensionPointType = extensionPointType else { return }
enableButton.isEnabled = false
if let oauth1 = extensionPointType as? OAuth1SwiftProvider.Type {
enableOauth1(oauth1)
} else {
ExtensionPointManager.shared.activateExtensionPoint(extensionPointType)
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.OK)
}
}
}
extension ExtensionPointEnableWindowController: 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.hostWindow!.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK)
}
if case ASWebAuthenticationSessionError.canceledLogin = error {
print("Login cancelled.")
} else {
NSApplication.shared.presentError(error)
}
})
session.presentationContextProvider = self
if !session.start() {
print("Session failed to start!!!")
}
}
}
extension ExtensionPointEnableWindowController: ASWebAuthenticationPresentationContextProviding {
public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return hostWindow!
}
}
private extension ExtensionPointEnableWindowController {
func enableOauth1(_ provider: OAuth1SwiftProvider.Type) {
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)
self.hostWindow!.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK)
case .failure(let oauthSwiftError):
NSApplication.shared.presentError(oauthSwiftError)
}
self.oauth?.cancel()
self.oauth = nil
}
}
}

View File

@@ -0,0 +1,41 @@
//
// ExtensionPointMarsEditWindowController.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/8/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Cocoa
class ExtensionPointEnableMarsEditWindowController: NSWindowController {
private weak var hostWindow: NSWindow?
convenience init() {
self.init(windowNibName: NSNib.Name("ExtensionPointMarsEdit"))
}
override func windowDidLoad() {
super.windowDidLoad()
}
// MARK: API
func runSheetOnWindow(_ hostWindow: NSWindow) {
self.hostWindow = hostWindow
hostWindow.beginSheet(window!)
}
// MARK: Actions
@IBAction func cancel(_ sender: Any) {
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel)
}
@IBAction func enable(_ sender: Any) {
ExtensionPointManager.shared.activateExtensionPoint(ExtensionPointIdentifer.marsEdit)
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.OK)
}
}

View File

@@ -0,0 +1,127 @@
//
// ExtensionsPreferencesViewController.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/6/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import AppKit
final class ExtensionPointPreferencesViewController: NSViewController {
@IBOutlet weak var tableView: NSTableView!
@IBOutlet weak var detailView: NSView!
@IBOutlet weak var deleteButton: NSButton!
private var activeExtensionPoints = [ExtensionPoint]()
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
NotificationCenter.default.addObserver(self, selector: #selector(activeExtensionPointsDidChange(_:)), name: .ActiveExtensionPointsDidChange, object: nil)
// Fix tableView frame  for some reason IB wants it 1pt wider than the clip view. This leads to unwanted horizontal scrolling.
var rTable = tableView.frame
rTable.size.width = tableView.superview!.frame.size.width
tableView.frame = rTable
showDefaultView()
}
@IBAction func enableExtensionPoints(_ sender: Any) {
tableView.selectRowIndexes([], byExtendingSelection: false)
showController(ExtensionPointAddViewController())
}
@IBAction func disableExtensionPoint(_ sender: Any) {
guard tableView.selectedRow != -1 else {
return
}
let extensionPoint = activeExtensionPoints[tableView.selectedRow]
ExtensionPointManager.shared.deactivateExtensionPoint(extensionPoint.extensionPointID)
showController(ExtensionPointAddViewController())
}
}
// MARK: - NSTableViewDataSource
extension ExtensionPointPreferencesViewController: NSTableViewDataSource {
func numberOfRows(in tableView: NSTableView) -> Int {
return activeExtensionPoints.count
}
func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
return activeExtensionPoints[row]
}
}
// MARK: - NSTableViewDelegate
extension ExtensionPointPreferencesViewController: NSTableViewDelegate {
private static let cellIdentifier = NSUserInterfaceItemIdentifier(rawValue: "AccountCell")
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
if let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "Cell"), owner: nil) as? NSTableCellView {
let extensionPoint = activeExtensionPoints[row]
cell.textField?.stringValue = extensionPoint.title
cell.imageView?.image = extensionPoint.templateImage
return cell
}
return nil
}
func tableViewSelectionDidChange(_ notification: Notification) {
let selectedRow = tableView.selectedRow
if tableView.selectedRow == -1 {
deleteButton.isEnabled = false
return
} else {
deleteButton.isEnabled = true
}
let extensionPoint = activeExtensionPoints[selectedRow]
let controller = ExtensionPointDetailViewController(extensionPoint: extensionPoint)
showController(controller)
}
}
// MARK: - Private
private extension ExtensionPointPreferencesViewController {
@objc func activeExtensionPointsDidChange(_ note: Notification) {
showDefaultView()
}
func showDefaultView() {
activeExtensionPoints = Array(ExtensionPointManager.shared.activeExtensionPoints.values).sorted(by: { $0.title < $1.title })
tableView.reloadData()
showController(ExtensionPointAddViewController())
}
func showController(_ controller: NSViewController) {
if let controller = children.first {
children.removeAll()
controller.view.removeFromSuperview()
}
addChild(controller)
controller.view.translatesAutoresizingMaskIntoConstraints = false
detailView.addSubview(controller.view)
detailView.addFullSizeConstraints(forSubview: controller.view)
}
}

View File

@@ -1,5 +1,5 @@
//
// AccountsControlsBackgroundView.swift
// PreferencesControlsBackgroundView.swift
// NetNewsWire
//
// Created by Brent Simmons on 3/18/19.
@@ -9,7 +9,7 @@
import AppKit
import RSCore
final class AccountsControlsBackgroundView: NSView {
final class PreferencesControlsBackgroundView: NSView {
private let lightModeFillColor = NSColor(white: 0.97, alpha: 1.0)
private let darkModeFillColor = NSColor(red: 0.32, green: 0.34, blue: 0.35, alpha: 1.0)

View File

@@ -1,5 +1,5 @@
//
// AccountsTableViewBackgroundView.swift
// PreferencesTableViewBackgroundView.swift
// NetNewsWire
//
// Created by Brent Simmons on 3/19/19.
@@ -8,7 +8,7 @@
import AppKit
final class AccountsTableViewBackgroundView: NSView {
final class PreferencesTableViewBackgroundView: NSView {
let lightBorderColor = NSColor(white: 0.71, alpha: 1.0)
let darkBorderColor = NSColor(red: 0.41, green: 0.43, blue: 0.44, alpha: 1.0)

View File

@@ -12,18 +12,19 @@ private struct PreferencesToolbarItemSpec {
let identifier: NSToolbarItem.Identifier
let name: String
let imageName: NSImage.Name
let image: NSImage?
init(identifierRawValue: String, name: String, imageName: NSImage.Name) {
init(identifierRawValue: String, name: String, image: NSImage?) {
self.identifier = NSToolbarItem.Identifier(identifierRawValue)
self.name = name
self.imageName = imageName
self.image = image
}
}
private struct ToolbarItemIdentifier {
static let General = "General"
static let Accounts = "Accounts"
static let Extensions = "Extensions"
static let Advanced = "Advanced"
}
@@ -33,15 +34,24 @@ class PreferencesWindowController : NSWindowController, NSToolbarDelegate {
private var viewControllers = [String: NSViewController]()
private let toolbarItemSpecs: [PreferencesToolbarItemSpec] = {
var specs = [PreferencesToolbarItemSpec]()
specs += [PreferencesToolbarItemSpec(identifierRawValue: ToolbarItemIdentifier.General, name: NSLocalizedString("General", comment: "Preferences"), imageName: NSImage.preferencesGeneralName)]
specs += [PreferencesToolbarItemSpec(identifierRawValue: ToolbarItemIdentifier.Accounts, name: NSLocalizedString("Accounts", comment: "Preferences"), imageName: NSImage.userAccountsName)]
specs += [PreferencesToolbarItemSpec(identifierRawValue: ToolbarItemIdentifier.General,
name: NSLocalizedString("General", comment: "Preferences"),
image: NSImage(named: NSImage.preferencesGeneralName))]
specs += [PreferencesToolbarItemSpec(identifierRawValue: ToolbarItemIdentifier.Accounts,
name: NSLocalizedString("Accounts", comment: "Preferences"),
image: NSImage(named: NSImage.userAccountsName))]
specs += [PreferencesToolbarItemSpec(identifierRawValue: ToolbarItemIdentifier.Extensions,
name: NSLocalizedString("Extensions", comment: "Preferences"),
image: AppAssets.extensionPreference)]
// Omit the Advanced Preferences for now because the Software Update related functionality is
// forbidden/non-applicable, and we can rely upon Apple to some extent for crash reports. We
// can add back the Crash Reporter preferences when we're ready to dynamically shuffle the rest
// of the content in this tab.
#if !MAC_APP_STORE
specs += [PreferencesToolbarItemSpec(identifierRawValue: ToolbarItemIdentifier.Advanced, name: NSLocalizedString("Advanced", comment: "Preferences"), imageName: NSImage.advancedName)]
specs += [PreferencesToolbarItemSpec(identifierRawValue: ToolbarItemIdentifier.Advanced,
name: NSLocalizedString("Advanced", comment: "Preferences"),
image: NSImage(named: NSImage.advancedName))]
#endif
return specs
}()
@@ -84,7 +94,7 @@ class PreferencesWindowController : NSWindowController, NSToolbarDelegate {
toolbarItem.target = self
toolbarItem.label = toolbarItemSpec.name
toolbarItem.paletteLabel = toolbarItem.label
toolbarItem.image = NSImage(named: toolbarItemSpec.imageName)
toolbarItem.image = toolbarItemSpec.image
return toolbarItem
}

Some files were not shown because too many files have changed in this diff Show More