mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Merge branch 'extension-point'
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -70,6 +70,6 @@ fastlane/screenshots
|
||||
fastlane/test_output
|
||||
|
||||
|
||||
|
||||
/Shared/Secrets.swift
|
||||
/Frameworks/Secrets/Secrets.swift
|
||||
*.py[cod]
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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 */,
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import Foundation
|
||||
import Articles
|
||||
import RSWeb
|
||||
import Secrets
|
||||
|
||||
protocol AccountDelegate {
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
33
Frameworks/Account/FeedProvider/FeedProvider.swift
Normal file
33
Frameworks/Account/FeedProvider/FeedProvider.swift
Normal 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)
|
||||
|
||||
}
|
||||
42
Frameworks/Account/FeedProvider/FeedProviderManager.swift
Normal file
42
Frameworks/Account/FeedProvider/FeedProviderManager.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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)])
|
||||
}
|
||||
|
||||
}
|
||||
28
Frameworks/Account/FeedProvider/Twitter/TwitterHashtag.swift
Normal file
28
Frameworks/Account/FeedProvider/Twitter/TwitterHashtag.swift
Normal 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
|
||||
}
|
||||
}
|
||||
22
Frameworks/Account/FeedProvider/Twitter/TwitterMedia.swift
Normal file
22
Frameworks/Account/FeedProvider/Twitter/TwitterMedia.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
33
Frameworks/Account/FeedProvider/Twitter/TwitterMention.swift
Normal file
33
Frameworks/Account/FeedProvider/Twitter/TwitterMention.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
182
Frameworks/Account/FeedProvider/Twitter/TwitterStatus.swift
Normal file
182
Frameworks/Account/FeedProvider/Twitter/TwitterStatus.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
29
Frameworks/Account/FeedProvider/Twitter/TwitterSymbol.swift
Normal file
29
Frameworks/Account/FeedProvider/Twitter/TwitterSymbol.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
33
Frameworks/Account/FeedProvider/Twitter/TwitterURL.swift
Normal file
33
Frameworks/Account/FeedProvider/Twitter/TwitterURL.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
44
Frameworks/Account/FeedProvider/Twitter/TwitterUser.swift
Normal file
44
Frameworks/Account/FeedProvider/Twitter/TwitterUser.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
34
Frameworks/Account/FeedProvider/Twitter/TwitterVideo.swift
Normal file
34
Frameworks/Account/FeedProvider/Twitter/TwitterVideo.swift
Normal 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"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import Foundation
|
||||
import Foundation
|
||||
import SyncDatabase
|
||||
import RSWeb
|
||||
import Secrets
|
||||
|
||||
enum FeedWranglerError : Error {
|
||||
case general(message: String)
|
||||
|
||||
@@ -12,6 +12,7 @@ import RSParser
|
||||
import RSWeb
|
||||
import SyncDatabase
|
||||
import os.log
|
||||
import Secrets
|
||||
|
||||
final class FeedWranglerAccountDelegate: AccountDelegate {
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Secrets
|
||||
|
||||
enum FeedWranglerConfig {
|
||||
static let pageSize = 100
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
import Foundation
|
||||
import RSWeb
|
||||
import Secrets
|
||||
|
||||
enum CreateSubscriptionResult {
|
||||
case created(FeedbinSubscription)
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
import RSWeb
|
||||
import Secrets
|
||||
|
||||
final class FeedlyAPICaller {
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,6 +12,7 @@ import RSParser
|
||||
import RSWeb
|
||||
import SyncDatabase
|
||||
import os.log
|
||||
import Secrets
|
||||
|
||||
final class FeedlyAccountDelegate: AccountDelegate {
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Secrets
|
||||
|
||||
extension OAuthAuthorizationClient {
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -10,6 +10,7 @@ import Foundation
|
||||
import os.log
|
||||
import RSWeb
|
||||
import RSCore
|
||||
import Secrets
|
||||
|
||||
class FeedlyAddExistingFeedOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlyCheckpointOperationDelegate {
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import os.log
|
||||
import SyncDatabase
|
||||
import RSWeb
|
||||
import RSCore
|
||||
import Secrets
|
||||
|
||||
class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlySearchOperationDelegate, FeedlyCheckpointOperationDelegate {
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
import Secrets
|
||||
|
||||
/// Single responsibility is to identify articles that have changed since a particular date.
|
||||
///
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import Foundation
|
||||
import os.log
|
||||
import SyncDatabase
|
||||
import Secrets
|
||||
|
||||
/// Clone locally the remote starred article state.
|
||||
///
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -10,6 +10,7 @@ import Foundation
|
||||
import os.log
|
||||
import RSParser
|
||||
import SyncDatabase
|
||||
import Secrets
|
||||
|
||||
/// Clone locally the remote unread article state.
|
||||
///
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import Foundation
|
||||
import os.log
|
||||
import RSWeb
|
||||
import Secrets
|
||||
|
||||
final class FeedlyRefreshAccessTokenOperation: FeedlyOperation {
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -11,6 +11,7 @@ import os.log
|
||||
import RSParser
|
||||
import RSCore
|
||||
import RSWeb
|
||||
import Secrets
|
||||
|
||||
final class FeedlySyncStreamContentsOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlyGetStreamContentsOperationDelegate, FeedlyCheckpointOperationDelegate {
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
import RSWeb
|
||||
import Secrets
|
||||
|
||||
final class NewsBlurAPICaller: NSObject {
|
||||
static let SessionIdCookie = "newsblur_sessionid"
|
||||
|
||||
@@ -13,6 +13,7 @@ import RSParser
|
||||
import RSWeb
|
||||
import SyncDatabase
|
||||
import os.log
|
||||
import Secrets
|
||||
|
||||
final class NewsBlurAccountDelegate: AccountDelegate {
|
||||
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
import RSWeb
|
||||
import Secrets
|
||||
|
||||
enum CreateReaderAPISubscriptionResult {
|
||||
case created(ReaderAPISubscription)
|
||||
|
||||
@@ -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
|
||||
@@ -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 */
|
||||
|
||||
|
||||
@@ -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 */
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ public enum CredentialsType: String {
|
||||
case readerBasic = "readerBasic"
|
||||
case readerAPIKey = "readerAPIKey"
|
||||
case oauthAccessToken = "oauthAccessToken"
|
||||
case oauthAccessTokenSecret = "oauthAccessTokenSecret"
|
||||
case oauthRefreshToken = "oauthRefreshToken"
|
||||
}
|
||||
|
||||
24
Frameworks/Secrets/Info.plist
Normal file
24
Frameworks/Secrets/Info.plist
Normal 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>
|
||||
16
Frameworks/Secrets/OAuth1SwiftProvider.swift
Normal file
16
Frameworks/Secrets/OAuth1SwiftProvider.swift
Normal 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 }
|
||||
|
||||
}
|
||||
@@ -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))
|
||||
277
Frameworks/Secrets/Secrets.xcodeproj/project.pbxproj
Normal file
277
Frameworks/Secrets/Secrets.xcodeproj/project.pbxproj
Normal 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 */;
|
||||
}
|
||||
61
Frameworks/Secrets/xcconfig/Secrets_project.xcconfig
Normal file
61
Frameworks/Secrets/xcconfig/Secrets_project.xcconfig
Normal 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
|
||||
15
Frameworks/Secrets/xcconfig/Secrets_project_debug.xcconfig
Normal file
15
Frameworks/Secrets/xcconfig/Secrets_project_debug.xcconfig
Normal 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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
#include "./Secrets_project_debug.xcconfig"
|
||||
|
||||
OTHER_SWIFT_FLAGS = -DTEST $(inherited)
|
||||
13
Frameworks/Secrets/xcconfig/Secrets_target.xcconfig
Normal file
13
Frameworks/Secrets/xcconfig/Secrets_target.xcconfig
Normal 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
|
||||
@@ -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 */
|
||||
|
||||
|
||||
@@ -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")!
|
||||
}()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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?) {
|
||||
|
||||
199
Mac/Base.lproj/AddTwitterFeedSheet.xib
Normal file
199
Mac/Base.lproj/AddTwitterFeedSheet.xib
Normal 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>
|
||||
@@ -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"/>
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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("Can’t add this feed because you’ve 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("Can’t 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("Can’t 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
30
Mac/MainWindow/AddFeed/AddFeedWIndowController.swift
Normal file
30
Mac/MainWindow/AddFeed/AddFeedWIndowController.swift
Normal 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)
|
||||
|
||||
}
|
||||
198
Mac/MainWindow/AddFeed/AddTwitterFeedWindowController.swift
Normal file
198
Mac/MainWindow/AddFeed/AddTwitterFeedWindowController.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import AppKit
|
||||
import Account
|
||||
import RSWeb
|
||||
import Secrets
|
||||
|
||||
class AccountsFeedWranglerWindowController: NSWindowController {
|
||||
@IBOutlet weak var progressIndicator: NSProgressIndicator!
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import AppKit
|
||||
import Account
|
||||
import RSWeb
|
||||
import Secrets
|
||||
|
||||
class AccountsFeedbinWindowController: NSWindowController {
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import AppKit
|
||||
import Account
|
||||
import RSWeb
|
||||
import Secrets
|
||||
|
||||
class AccountsNewsBlurWindowController: NSWindowController {
|
||||
@IBOutlet weak var progressIndicator: NSProgressIndicator!
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import AppKit
|
||||
import Account
|
||||
import RSWeb
|
||||
import Secrets
|
||||
|
||||
class AccountsReaderAPIWindowController: NSWindowController {
|
||||
|
||||
|
||||
109
Mac/Preferences/ExtensionPoints/ExtensionPointAdd.xib
Normal file
109
Mac/Preferences/ExtensionPoints/ExtensionPointAdd.xib
Normal 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>
|
||||
@@ -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?
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
86
Mac/Preferences/ExtensionPoints/ExtensionPointDetail.xib
Normal file
86
Mac/Preferences/ExtensionPoints/ExtensionPointDetail.xib
Normal 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>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
116
Mac/Preferences/ExtensionPoints/ExtensionPointEnable.xib
Normal file
116
Mac/Preferences/ExtensionPoints/ExtensionPointEnable.xib
Normal 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>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
Reference in New Issue
Block a user