Merge pull request #1082 from jbennett/feature/feed-wrangler
[WIP] Feed wrangler support
This commit is contained in:
commit
ea064c1e2e
@ -238,6 +238,9 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport)
|
||||
case .feedly:
|
||||
self.delegate = FeedlyAccountDelegate(dataFolder: dataFolder, transport: transport, api: FeedlyAccountDelegate.environment)
|
||||
case .feedWrangler:
|
||||
self.delegate = FeedWranglerAccountDelegate(dataFolder: dataFolder, transport: transport)
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
@ -315,6 +318,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
FeedbinAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion)
|
||||
case .freshRSS:
|
||||
ReaderAPIAccountDelegate.validateCredentials(transport: transport, credentials: credentials, endpoint: endpoint, completion: completion)
|
||||
case .feedWrangler:
|
||||
FeedWranglerAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
@ -7,6 +7,16 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
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 */; };
|
||||
3B826DAA2385C81C00FC1ADB /* FeedWranglerSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B826DA12385C81C00FC1ADB /* FeedWranglerSubscription.swift */; };
|
||||
3B826DAB2385C81C00FC1ADB /* FeedWranglerConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B826DA22385C81C00FC1ADB /* FeedWranglerConfig.swift */; };
|
||||
3B826DAC2385C81C00FC1ADB /* FeedWranglerAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B826DA32385C81C00FC1ADB /* FeedWranglerAccountDelegate.swift */; };
|
||||
3B826DAD2385C81C00FC1ADB /* FeedWranglerFeedItemsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B826DA42385C81C00FC1ADB /* FeedWranglerFeedItemsRequest.swift */; };
|
||||
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 */; };
|
||||
5107A099227DE42E00C7C3C5 /* AccountCredentialsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A098227DE42E00C7C3C5 /* AccountCredentialsTest.swift */; };
|
||||
5107A09B227DE49500C7C3C5 /* TestAccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */; };
|
||||
5107A09D227DE77700C7C3C5 /* TestTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09C227DE77700C7C3C5 /* TestTransport.swift */; };
|
||||
@ -209,6 +219,16 @@
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
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>"; };
|
||||
3B826DA12385C81C00FC1ADB /* FeedWranglerSubscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedWranglerSubscription.swift; sourceTree = "<group>"; };
|
||||
3B826DA22385C81C00FC1ADB /* FeedWranglerConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedWranglerConfig.swift; sourceTree = "<group>"; };
|
||||
3B826DA32385C81C00FC1ADB /* FeedWranglerAccountDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedWranglerAccountDelegate.swift; sourceTree = "<group>"; };
|
||||
3B826DA42385C81C00FC1ADB /* FeedWranglerFeedItemsRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedWranglerFeedItemsRequest.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -400,6 +420,23 @@
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
3B826D9D2385C81C00FC1ADB /* FeedWrangler */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3B826D9E2385C81C00FC1ADB /* FeedWranglerAuthorizationResult.swift */,
|
||||
3B826D9F2385C81C00FC1ADB /* FeedWranglerFeedItem.swift */,
|
||||
3B826DA02385C81C00FC1ADB /* FeedWranglerAPICaller.swift */,
|
||||
3B826DA12385C81C00FC1ADB /* FeedWranglerSubscription.swift */,
|
||||
3B826DA22385C81C00FC1ADB /* FeedWranglerConfig.swift */,
|
||||
3B826DA32385C81C00FC1ADB /* FeedWranglerAccountDelegate.swift */,
|
||||
3B826DA42385C81C00FC1ADB /* FeedWranglerFeedItemsRequest.swift */,
|
||||
3B826DA52385C81C00FC1ADB /* FeedWranglerSubscriptionsRequest.swift */,
|
||||
3B826DA62385C81C00FC1ADB /* FeedWranglerGenericResult.swift */,
|
||||
3BC23AB82385ECB100371CBA /* FeedWranglerSubscriptionResult.swift */,
|
||||
);
|
||||
path = FeedWrangler;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5111D71C2357534700737D45 /* Feedbin */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -551,6 +588,7 @@
|
||||
515E4EB12324FF7D0057B0E7 /* Credentials */,
|
||||
8419742B1F6DDE84006346C4 /* LocalAccount */,
|
||||
84245C7D1FDDD2580074AFBB /* Feedbin */,
|
||||
3B826D9D2385C81C00FC1ADB /* FeedWrangler */,
|
||||
552032EA229D5D5A009559E0 /* ReaderAPI */,
|
||||
9EA31339231E368100268BA0 /* Feedly */,
|
||||
848935031F62484F00CEBD24 /* AccountTests */,
|
||||
@ -735,8 +773,10 @@
|
||||
buildConfigurationList = 8489350A1F62485000CEBD24 /* Build configuration list for PBXNativeTarget "Account" */;
|
||||
buildPhases = (
|
||||
9E964EBB2375512300A7AF2E /* Run Script: Update OAuthAuthorizationClient+Feedly.swift */,
|
||||
3B826DCF2385CE1B00FC1ADB /* Run Script: Update FeedWranglerConfig.swift */,
|
||||
848934F11F62484F00CEBD24 /* Sources */,
|
||||
9E964EBC2375517100A7AF2E /* Run Script: Reset OAuthAuthorizationClient+Feedly.swift */,
|
||||
3B826DD02385CE9500FC1ADB /* Run Script: Reset FeedWranglerConfig.swift */,
|
||||
848934F21F62484F00CEBD24 /* Frameworks */,
|
||||
848934F31F62484F00CEBD24 /* Headers */,
|
||||
848934F41F62484F00CEBD24 /* Resources */,
|
||||
@ -891,6 +931,42 @@
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
3B826DCF2385CE1B00FC1ADB /* Run Script: Update FeedWranglerConfig.swift */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script: Update FeedWranglerConfig.swift";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "FAILED=false\n\nif [ -z \"${FEED_WRANGLER_KEY}\" ]; then\nFAILED=true\nfi\n\nif [ \"$FAILED\" = true ]; then\necho \"Missing Feed Wrangler Key. FeedWranglerConfig.swift not changed.\"\nexit 0\nfi\n\nsed -i .tmp \"s|{FEEDWRANGLERKEY}|${FEED_WRANGLER_KEY}|g; s|{FEEDWRANGLERKEY}|${FEED_WRANGLER_KEY}|g\" \"${SRCROOT}/FeedWrangler/FeedWranglerConfig.swift\"\n\nrm -f \"${SRCROOT}/FeedWrangler/FeedWranglerConfig.swift.tmp\"\n\necho \"All Feed Wrangler env values found!\"\n\n";
|
||||
};
|
||||
3B826DD02385CE9500FC1ADB /* Run Script: Reset FeedWranglerConfig.swift */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script: Reset FeedWranglerConfig.swift";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "git checkout \"${SRCROOT}/FeedWrangler/FeedWranglerConfig.swift\"\n";
|
||||
};
|
||||
51C8F34C234FB14B0048ED95 /* Run Script: Verify No Build Settings */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@ -964,6 +1040,7 @@
|
||||
8469F81C1F6DD15E0084783E /* Account.swift in Sources */,
|
||||
9EAEC60E2332FEC20085D7C9 /* FeedlyFeed.swift in Sources */,
|
||||
5144EA4E227B829A00D19003 /* FeedbinAccountDelegate.swift in Sources */,
|
||||
3B826DAF2385C81C00FC1ADB /* FeedWranglerGenericResult.swift in Sources */,
|
||||
9ECC9A85234DC16E009B5144 /* FeedlyAccountDelegateError.swift in Sources */,
|
||||
9EA3133B231E368100268BA0 /* FeedlyAccountDelegate.swift in Sources */,
|
||||
51E5959B228C781500FCC42B /* FeedbinStarredEntry.swift in Sources */,
|
||||
@ -990,11 +1067,14 @@
|
||||
9E85C8EB236700E600D0F1F7 /* FeedlyGetEntriesOperation.swift in Sources */,
|
||||
9E1D154D233370D800F4944C /* FeedlySyncAllOperation.swift in Sources */,
|
||||
844B297D2106C7EC004020B3 /* WebFeed.swift in Sources */,
|
||||
3B826DA72385C81C00FC1ADB /* FeedWranglerAuthorizationResult.swift in Sources */,
|
||||
9E964EBA23754B4000A7AF2E /* OAuthAccountAuthorizationOperation.swift in Sources */,
|
||||
9E1D15572334355900F4944C /* FeedlyRequestStreamsOperation.swift in Sources */,
|
||||
9E1D15512334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift in Sources */,
|
||||
9E1773D7234575AB0056A5A8 /* FeedlyTag.swift in Sources */,
|
||||
3B826DAB2385C81C00FC1ADB /* FeedWranglerConfig.swift in Sources */,
|
||||
515E4EB62324FF8C0057B0E7 /* URLRequest+RSWeb.swift in Sources */,
|
||||
3B826DA82385C81C00FC1ADB /* FeedWranglerFeedItem.swift in Sources */,
|
||||
9E672396236F7E68000BE141 /* OAuthAcessTokenRefreshing.swift in Sources */,
|
||||
9E7299D723505E9600DAEFB7 /* FeedlyAddFeedOperation.swift in Sources */,
|
||||
9EEAE075235D01C400E3FEE4 /* FeedlyMarkArticlesService.swift in Sources */,
|
||||
@ -1005,6 +1085,7 @@
|
||||
9EAEC624233315F60085D7C9 /* FeedlyEntry.swift in Sources */,
|
||||
9EEAE073235D01AE00E3FEE4 /* FeedlyGetStreamIdsService.swift in Sources */,
|
||||
9EEEF7212355277F009E9D80 /* FeedlySyncStarredArticlesOperation.swift in Sources */,
|
||||
3BC23AB92385ECB100371CBA /* FeedWranglerSubscriptionResult.swift in Sources */,
|
||||
5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */,
|
||||
84B99C9F1FAE8D3200ECDEDB /* ContainerPath.swift in Sources */,
|
||||
9E510D6E234F16A8002E6F1A /* FeedlyAddFeedRequest.swift in Sources */,
|
||||
@ -1013,6 +1094,7 @@
|
||||
55203300229D5D5A009559E0 /* ReaderAPICaller.swift in Sources */,
|
||||
9E1D154F233371DD00F4944C /* FeedlyGetCollectionsOperation.swift in Sources */,
|
||||
9EAEC626233318400085D7C9 /* FeedlyStream.swift in Sources */,
|
||||
3B826DA92385C81C00FC1ADB /* FeedWranglerAPICaller.swift in Sources */,
|
||||
9EAEC60C2332FE830085D7C9 /* FeedlyCollection.swift in Sources */,
|
||||
51E3EB41229AF61B00645299 /* AccountError.swift in Sources */,
|
||||
9E1D155D233447F000F4944C /* FeedlyUpdateAccountFeedsWithItemsOperation.swift in Sources */,
|
||||
@ -1031,6 +1113,7 @@
|
||||
9EC688EA232B973C00A8D0A2 /* FeedlyAPICaller.swift in Sources */,
|
||||
9E1773D32345700F0056A5A8 /* FeedlyLink.swift in Sources */,
|
||||
9EAEC62823331C350085D7C9 /* FeedlyCategory.swift in Sources */,
|
||||
3B826DAE2385C81C00FC1ADB /* FeedWranglerSubscriptionsRequest.swift in Sources */,
|
||||
9E964EB823754AC400A7AF2E /* OAuthAuthorizationClient+Feedly.swift in Sources */,
|
||||
9EF1B10923590E93000A486A /* FeedlyStreamIds.swift in Sources */,
|
||||
84D09623217418DC00D77525 /* FeedbinTagging.swift in Sources */,
|
||||
@ -1038,6 +1121,7 @@
|
||||
5165D72A22835F7D00D9D53D /* HTMLFeedFinder.swift in Sources */,
|
||||
841974011F6DD1EC006346C4 /* Folder.swift in Sources */,
|
||||
510BD111232C3801002692E4 /* AccountMetadataFile.swift in Sources */,
|
||||
3B826DAD2385C81C00FC1ADB /* FeedWranglerFeedItemsRequest.swift in Sources */,
|
||||
846E774F1F6EF9C000A165E2 /* LocalAccountDelegate.swift in Sources */,
|
||||
515E4EB52324FF8C0057B0E7 /* CredentialsManager.swift in Sources */,
|
||||
844B297F210CE37E004020B3 /* UnreadCountProvider.swift in Sources */,
|
||||
@ -1048,6 +1132,8 @@
|
||||
84F1F06E2243524700DA0616 /* AccountMetadata.swift in Sources */,
|
||||
9EF1B10723590D61000A486A /* FeedlyGetStreamIdsOperation.swift in Sources */,
|
||||
84245C851FDDD8CB0074AFBB /* FeedbinSubscription.swift in Sources */,
|
||||
3B826DAA2385C81C00FC1ADB /* FeedWranglerSubscription.swift in Sources */,
|
||||
3B826DAC2385C81C00FC1ADB /* FeedWranglerAccountDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -15,6 +15,8 @@ public enum CredentialsError: Error {
|
||||
|
||||
public enum CredentialsType: String {
|
||||
case basic = "password"
|
||||
case feedWranglerBasic = "feedWranglerBasic"
|
||||
case feedWranglerToken = "feedWranglerToken"
|
||||
case readerBasic = "readerBasic"
|
||||
case readerAPIKey = "readerAPIKey"
|
||||
case oauthAccessToken = "oauthAccessToken"
|
||||
|
@ -25,6 +25,14 @@ public extension URLRequest {
|
||||
let base64 = data?.base64EncodedString()
|
||||
let auth = "Basic \(base64 ?? "")"
|
||||
setValue(auth, forHTTPHeaderField: HTTPRequestHeader.authorization)
|
||||
case .feedWranglerBasic:
|
||||
self.url = url.appendingQueryItems([
|
||||
URLQueryItem(name: "email", value: credentials.username),
|
||||
URLQueryItem(name: "password", value: credentials.secret),
|
||||
URLQueryItem(name: "client_key", value: FeedWranglerConfig.clientKey)
|
||||
])
|
||||
case .feedWranglerToken:
|
||||
self.url = url.appendingQueryItem(URLQueryItem(name: "access_token", value: credentials.secret))
|
||||
case .readerBasic:
|
||||
setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
httpMethod = "POST"
|
||||
|
291
Frameworks/Account/FeedWrangler/FeedWranglerAPICaller.swift
Normal file
291
Frameworks/Account/FeedWrangler/FeedWranglerAPICaller.swift
Normal file
@ -0,0 +1,291 @@
|
||||
//
|
||||
// FeedWranglerAPICaller.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Jonathan Bennett on 2019-08-29.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import Foundation
|
||||
import SyncDatabase
|
||||
import RSWeb
|
||||
|
||||
enum FeedWranglerError : Error {
|
||||
case general(message: String)
|
||||
}
|
||||
|
||||
final class FeedWranglerAPICaller: NSObject {
|
||||
|
||||
private var transport: Transport!
|
||||
|
||||
var credentials: Credentials?
|
||||
weak var accountMetadata: AccountMetadata?
|
||||
|
||||
init(transport: Transport) {
|
||||
super.init()
|
||||
self.transport = transport
|
||||
}
|
||||
|
||||
func cancelAll() {
|
||||
transport.cancelAll()
|
||||
}
|
||||
|
||||
func logout(completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
let url = FeedWranglerConfig.clientURL.appendingPathComponent("users/logout")
|
||||
let request = URLRequest(url: url, credentials: credentials)
|
||||
|
||||
transport.send(request: request) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateCredentials(completion: @escaping (Result<Credentials?, Error>) -> Void) {
|
||||
let url = FeedWranglerConfig.clientURL.appendingPathComponent("users/authorize")
|
||||
let username = self.credentials?.username ?? ""
|
||||
|
||||
standardSend(url: url, resultType: FeedWranglerAuthorizationResult.self) { result in
|
||||
switch result {
|
||||
case .success(let (_, results)):
|
||||
if let accessToken = results?.accessToken {
|
||||
let authCredentials = Credentials(type: .feedWranglerToken, username: username, secret: accessToken)
|
||||
completion(.success(authCredentials))
|
||||
} else {
|
||||
completion(.success(nil))
|
||||
}
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveSubscriptions(completion: @escaping (Result<[FeedWranglerSubscription], Error>) -> Void) {
|
||||
let url = FeedWranglerConfig.clientURL.appendingPathComponent("subscriptions/list")
|
||||
|
||||
standardSend(url: url, resultType: FeedWranglerSubscriptionsRequest.self) { result in
|
||||
switch result {
|
||||
case .success(let (_, results)):
|
||||
completion(.success(results?.feeds ?? []))
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addSubscription(url: String, completion: @escaping (Result<FeedWranglerSubscription, Error>) -> Void) {
|
||||
let url = FeedWranglerConfig
|
||||
.clientURL
|
||||
.appendingPathComponent("subscriptions/add_feed_and_wait")
|
||||
.appendingQueryItems([
|
||||
URLQueryItem(name: "feed_url", value: url),
|
||||
URLQueryItem(name: "choose_first", value: "true")
|
||||
])
|
||||
|
||||
standardSend(url: url, resultType: FeedWranglerSubscriptionResult.self) { result in
|
||||
switch result {
|
||||
case .success(let (_, results)):
|
||||
if let results = results {
|
||||
if let error = results.error {
|
||||
completion(.failure(FeedWranglerError.general(message: error)))
|
||||
} else {
|
||||
completion(.success(results.feed))
|
||||
}
|
||||
} else {
|
||||
completion(.failure(FeedWranglerError.general(message: "No feed found")))
|
||||
}
|
||||
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func renameSubscription(feedID: String, newName: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
let url = FeedWranglerConfig.clientURL
|
||||
.appendingPathComponent("subscriptions/rename_feed")
|
||||
.appendingQueryItems([
|
||||
URLQueryItem(name: "feed_id", value: feedID),
|
||||
URLQueryItem(name: "feed_name", value: newName),
|
||||
])
|
||||
|
||||
standardSend(url: url, resultType: FeedWranglerSubscriptionsRequest.self) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeSubscription(feedID: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
let url = FeedWranglerConfig.clientURL
|
||||
.appendingPathComponent("subscriptions/remove_feed")
|
||||
.appendingQueryItem(URLQueryItem(name: "feed_id", value: feedID))
|
||||
|
||||
standardSend(url: url, resultType: FeedWranglerGenericResult.self) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: FeedItems
|
||||
func retrieveEntries(articleIDs: [String], completion: @escaping (Result<[FeedWranglerFeedItem], Error>) -> Void) {
|
||||
let IDs = articleIDs.joined(separator: ",")
|
||||
let url = FeedWranglerConfig.clientURL
|
||||
.appendingPathComponent("feed_items/get")
|
||||
.appendingQueryItem(URLQueryItem(name: "feed_item_ids", value: IDs))
|
||||
|
||||
standardSend(url: url, resultType: FeedWranglerFeedItemsRequest.self) { result in
|
||||
switch result {
|
||||
case .success(let (_, results)):
|
||||
completion(.success(results?.feedItems ?? []))
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func retrieveFeedItems(page: Int = 0, feed: WebFeed? = nil, completion: @escaping (Result<[FeedWranglerFeedItem], Error>) -> Void) {
|
||||
let queryItems = [
|
||||
URLQueryItem(name: "read", value: "false"),
|
||||
URLQueryItem(name: "offset", value: String(page * FeedWranglerConfig.pageSize)),
|
||||
feed.map { URLQueryItem(name: "feed_id", value: $0.webFeedID) }
|
||||
].compactMap { $0 }
|
||||
let url = FeedWranglerConfig.clientURL
|
||||
.appendingPathComponent("feed_items/list")
|
||||
.appendingQueryItems(queryItems)
|
||||
|
||||
standardSend(url: url, resultType: FeedWranglerFeedItemsRequest.self) { result in
|
||||
switch result {
|
||||
case .success(let (_, results)):
|
||||
completion(.success(results?.feedItems ?? []))
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveUnreadFeedItems(page: Int = 0, completion: @escaping (Result<[FeedWranglerFeedItem], Error>) -> Void) {
|
||||
let url = FeedWranglerConfig.clientURL
|
||||
.appendingPathComponent("feed_items/list")
|
||||
.appendingQueryItems([
|
||||
URLQueryItem(name: "read", value: "false"),
|
||||
URLQueryItem(name: "offset", value: String(page * FeedWranglerConfig.pageSize)),
|
||||
])
|
||||
|
||||
standardSend(url: url, resultType: FeedWranglerFeedItemsRequest.self) { result in
|
||||
switch result {
|
||||
case .success(let (_, results)):
|
||||
completion(.success(results?.feedItems ?? []))
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveAllUnreadFeedItems(foundItems: [FeedWranglerFeedItem] = [], page: Int = 0, completion: @escaping (Result<[FeedWranglerFeedItem], Error>) -> Void) {
|
||||
retrieveUnreadFeedItems(page: page) { result in
|
||||
switch result {
|
||||
case .success(let newItems):
|
||||
if newItems.count > 0 {
|
||||
self.retrieveAllUnreadFeedItems(foundItems: foundItems + newItems, page: (page + 1), completion: completion)
|
||||
} else {
|
||||
completion(.success(foundItems + newItems))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveStarredFeedItems(page: Int = 0, completion: @escaping (Result<[FeedWranglerFeedItem], Error>) -> Void) {
|
||||
let url = FeedWranglerConfig.clientURL
|
||||
.appendingPathComponent("feed_items/list")
|
||||
.appendingQueryItems([
|
||||
URLQueryItem(name: "starred", value: "true"),
|
||||
URLQueryItem(name: "offset", value: String(page * FeedWranglerConfig.pageSize)),
|
||||
])
|
||||
|
||||
standardSend(url: url, resultType: FeedWranglerFeedItemsRequest.self) { result in
|
||||
switch result {
|
||||
case .success(let (_, results)):
|
||||
completion(.success(results?.feedItems ?? []))
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveAllStarredFeedItems(foundItems: [FeedWranglerFeedItem] = [], page: Int = 0, completion: @escaping (Result<[FeedWranglerFeedItem], Error>) -> Void) {
|
||||
retrieveStarredFeedItems(page: page) { result in
|
||||
switch result {
|
||||
case .success(let newItems):
|
||||
if newItems.count > 0 {
|
||||
self.retrieveAllStarredFeedItems(foundItems: foundItems + newItems, page: (page + 1), completion: completion)
|
||||
} else {
|
||||
completion(.success(foundItems + newItems))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateArticleStatus(_ articleID: String, _ statuses: [SyncStatus], completion: @escaping () -> Void) {
|
||||
|
||||
var queryItems = statuses.compactMap { status -> URLQueryItem? in
|
||||
switch status.key {
|
||||
case .read:
|
||||
return URLQueryItem(name: "read", value: status.flag.description)
|
||||
|
||||
case .starred:
|
||||
return URLQueryItem(name: "starred", value: status.flag.description)
|
||||
|
||||
case .userDeleted:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
queryItems.append(URLQueryItem(name: "feed_item_id", value: articleID))
|
||||
let url = FeedWranglerConfig.clientURL
|
||||
.appendingPathComponent("feed_items/update")
|
||||
.appendingQueryItems(queryItems)
|
||||
|
||||
standardSend(url: url, resultType: FeedWranglerGenericResult.self) { result in
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
private func standardSend<R: Decodable>(url: URL?, resultType: R.Type, completion: @escaping (Result<(HTTPURLResponse, R?), Error>) -> Void) {
|
||||
guard let callURL = url else {
|
||||
completion(.failure(TransportError.noURL))
|
||||
return
|
||||
}
|
||||
let request = URLRequest(url: callURL, credentials: credentials)
|
||||
|
||||
transport.send(request: request, resultType: resultType, completion: completion)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,537 @@
|
||||
//
|
||||
// FeedWranglerAccountDelegate.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Jonathan Bennett on 2019-08-29.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Articles
|
||||
import RSCore
|
||||
import RSParser
|
||||
import RSWeb
|
||||
import SyncDatabase
|
||||
import os.log
|
||||
|
||||
final class FeedWranglerAccountDelegate: AccountDelegate {
|
||||
|
||||
var behaviors: AccountBehaviors = []
|
||||
|
||||
var isOPMLImportInProgress = false
|
||||
var server: String? = FeedWranglerConfig.clientPath
|
||||
var credentials: Credentials? {
|
||||
didSet {
|
||||
caller.credentials = credentials
|
||||
}
|
||||
}
|
||||
|
||||
var accountMetadata: AccountMetadata?
|
||||
var refreshProgress = DownloadProgress(numberOfTasks: 0)
|
||||
|
||||
private let caller: FeedWranglerAPICaller
|
||||
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Feed Wrangler")
|
||||
private let database: SyncDatabase
|
||||
|
||||
init(dataFolder: String, transport: Transport?) {
|
||||
if let transport = transport {
|
||||
caller = FeedWranglerAPICaller(transport: transport)
|
||||
} else {
|
||||
let sessionConfiguration = URLSessionConfiguration.default
|
||||
sessionConfiguration.requestCachePolicy = .reloadIgnoringLocalCacheData
|
||||
sessionConfiguration.timeoutIntervalForRequest = 60.0
|
||||
sessionConfiguration.httpShouldSetCookies = false
|
||||
sessionConfiguration.httpCookieAcceptPolicy = .never
|
||||
sessionConfiguration.httpMaximumConnectionsPerHost = 1
|
||||
sessionConfiguration.httpCookieStorage = nil
|
||||
sessionConfiguration.urlCache = nil
|
||||
|
||||
if let userAgentHeaders = UserAgent.headers() {
|
||||
sessionConfiguration.httpAdditionalHeaders = userAgentHeaders
|
||||
}
|
||||
|
||||
let session = URLSession(configuration: sessionConfiguration)
|
||||
caller = FeedWranglerAPICaller(transport: session)
|
||||
}
|
||||
|
||||
database = SyncDatabase(databaseFilePath: dataFolder.appending("/DB.sqlite3"))
|
||||
}
|
||||
|
||||
func accountWillBeDeleted(_ account: Account) {
|
||||
caller.logout() { _ in }
|
||||
}
|
||||
|
||||
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(6)
|
||||
|
||||
self.refreshCredentials(for: account) {
|
||||
self.refreshProgress.completeTask()
|
||||
self.refreshSubscriptions(for: account) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
self.sendArticleStatus(for: account) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshArticles(for: account) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshMissingArticles(for: account) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cancelAll(for account: Account) {
|
||||
caller.cancelAll()
|
||||
}
|
||||
|
||||
func refreshCredentials(for account: Account, completion: @escaping (() -> Void)) {
|
||||
os_log(.debug, log: log, "Refreshing credentials...")
|
||||
// MARK: TODO
|
||||
credentials = try? account.retrieveCredentials(type: .feedWranglerToken)
|
||||
completion()
|
||||
}
|
||||
|
||||
func refreshSubscriptions(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
os_log(.debug, log: log, "Refreshing subscriptions...")
|
||||
caller.retrieveSubscriptions { result in
|
||||
switch result {
|
||||
case .success(let subscriptions):
|
||||
self.syncFeeds(account, subscriptions)
|
||||
completion(.success(()))
|
||||
|
||||
case .failure(let error):
|
||||
os_log(.debug, log: self.log, "Failed to refresh subscriptions: %@", error.localizedDescription)
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func refreshArticles(for account: Account, page: Int = 0, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
os_log(.debug, log: log, "Refreshing articles, page: %d...", page)
|
||||
|
||||
caller.retrieveFeedItems(page: page) { result in
|
||||
switch result {
|
||||
case .success(let items):
|
||||
self.syncFeedItems(account, items) {
|
||||
if items.count == 0 {
|
||||
completion(.success(()))
|
||||
} else {
|
||||
self.refreshArticles(for: account, page: (page + 1), completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func refreshMissingArticles(for account: Account, completion: @escaping ((Result<Void, Error>)-> Void)) {
|
||||
os_log(.debug, log: log, "Refreshing missing articles...")
|
||||
let group = DispatchGroup()
|
||||
|
||||
let fetchedArticleIDs = account.fetchArticleIDsForStatusesWithoutArticles()
|
||||
let articleIDs = Array(fetchedArticleIDs)
|
||||
let chunkedArticleIDs = articleIDs.chunked(into: 100)
|
||||
|
||||
for chunk in chunkedArticleIDs {
|
||||
group.enter()
|
||||
self.caller.retrieveEntries(articleIDs: chunk) { result in
|
||||
switch result {
|
||||
case .success(let entries):
|
||||
self.syncFeedItems(account, entries) {
|
||||
group.leave()
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Refresh missing articles failed: %@", error.localizedDescription)
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
self.refreshProgress.completeTask()
|
||||
os_log(.debug, log: self.log, "Done refreshing missing articles.")
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
os_log(.debug, log: log, "Sending article status...")
|
||||
|
||||
let syncStatuses = database.selectForProcessing()
|
||||
let articleStatuses = Dictionary(grouping: syncStatuses, by: { $0.articleID })
|
||||
let group = DispatchGroup()
|
||||
|
||||
articleStatuses.forEach { articleID, statuses in
|
||||
group.enter()
|
||||
caller.updateArticleStatus(articleID, statuses) {
|
||||
group.leave()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
os_log(.debug, log: self.log, "Done sending article statuses.")
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
os_log(.debug, log: log, "Refreshing article status...")
|
||||
let group = DispatchGroup()
|
||||
|
||||
group.enter()
|
||||
caller.retrieveAllUnreadFeedItems { result in
|
||||
switch result {
|
||||
case .success(let items):
|
||||
self.syncArticleReadState(account, items)
|
||||
group.leave()
|
||||
|
||||
case .failure(let error):
|
||||
os_log(.info, log: self.log, "Retrieving unread entries failed: %@.", error.localizedDescription)
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
// starred
|
||||
group.enter()
|
||||
caller.retrieveAllStarredFeedItems { result in
|
||||
switch result {
|
||||
case .success(let items):
|
||||
self.syncArticleStarredState(account, items)
|
||||
group.leave()
|
||||
|
||||
case .failure(let error):
|
||||
os_log(.info, log: self.log, "Retrieving starred entries failed: %@.", error.localizedDescription)
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
os_log(.debug, log: self.log, "Done refreshing article statuses.")
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func importOPML(for account: Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
func addFolder(for account: Account, name: String, completion: @escaping (Result<Folder, Error>) -> Void) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
func createWebFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(2)
|
||||
|
||||
self.refreshCredentials(for: account) {
|
||||
self.refreshProgress.completeTask()
|
||||
self.caller.addSubscription(url: url) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success(let subscription):
|
||||
self.addFeedWranglerSubscription(account: account, subscription: subscription, name: name, container: container, completion: completion)
|
||||
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func addFeedWranglerSubscription(account: Account, subscription sub: FeedWranglerSubscription, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
||||
DispatchQueue.main.async {
|
||||
let feed = account.createWebFeed(with: sub.title, url: sub.feedURL, webFeedID: String(sub.feedID), homePageURL: sub.siteURL)
|
||||
|
||||
account.addWebFeed(feed, to: container) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
if let name = name {
|
||||
account.renameWebFeed(feed, to: name) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.initialFeedDownload(account: account, feed: feed, completion: completion)
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.initialFeedDownload(account: account, feed: feed, completion: completion)
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func initialFeedDownload(account: Account, feed: WebFeed, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
||||
|
||||
self.caller.retrieveFeedItems(page: 0, feed: feed) { results in
|
||||
switch results {
|
||||
case .success(let entries):
|
||||
self.syncFeedItems(account, entries) {
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(feed))
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(2)
|
||||
|
||||
self.refreshCredentials(for: account) {
|
||||
self.refreshProgress.completeTask()
|
||||
self.caller.renameSubscription(feedID: feed.webFeedID, newName: name) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
feed.editedName = name
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addWebFeed(for account: Account, with feed: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
// just add to account, folders are not supported
|
||||
DispatchQueue.main.async {
|
||||
account.addFeedIfNotInAnyFolder(feed)
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func removeWebFeed(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(2)
|
||||
|
||||
self.refreshCredentials(for: account) {
|
||||
self.refreshProgress.completeTask()
|
||||
self.caller.removeSubscription(feedID: feed.webFeedID) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
account.clearWebFeedMetadata(feed)
|
||||
account.removeWebFeed(feed)
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func moveWebFeed(for account: Account, with feed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
func restoreWebFeed(for account: Account, feed: WebFeed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
|
||||
let syncStatuses = articles.map { SyncStatus(articleID: $0.articleID, key: statusKey, flag: flag)}
|
||||
database.insertStatuses(syncStatuses)
|
||||
|
||||
if database.selectPendingCount() > 0 {
|
||||
sendArticleStatus(for: account) { _ in
|
||||
// do it in the background
|
||||
}
|
||||
}
|
||||
|
||||
return account.update(articles, statusKey: statusKey, flag: flag)
|
||||
}
|
||||
|
||||
func accountDidInitialize(_ account: Account) {
|
||||
credentials = try? account.retrieveCredentials(type: .feedWranglerToken)
|
||||
}
|
||||
|
||||
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: @escaping (Result<Credentials?, Error>) -> Void) {
|
||||
let caller = FeedWranglerAPICaller(transport: transport)
|
||||
caller.credentials = credentials
|
||||
caller.validateCredentials() { result in
|
||||
DispatchQueue.main.async {
|
||||
completion(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
private extension FeedWranglerAccountDelegate {
|
||||
|
||||
func syncFeeds(_ account: Account, _ subscriptions: [FeedWranglerSubscription]) {
|
||||
assert(Thread.isMainThread)
|
||||
let feedIds = subscriptions.map { String($0.feedID) }
|
||||
|
||||
let feedsToRemove = account.topLevelWebFeeds.filter { !feedIds.contains($0.webFeedID) }
|
||||
account.removeFeeds(feedsToRemove)
|
||||
|
||||
var subscriptionsToAdd = Set<FeedWranglerSubscription>()
|
||||
subscriptions.forEach { subscription in
|
||||
let subscriptionId = String(subscription.feedID)
|
||||
|
||||
if let feed = account.existingWebFeed(withWebFeedID: subscriptionId) {
|
||||
feed.name = subscription.title
|
||||
feed.editedName = nil
|
||||
feed.homePageURL = subscription.siteURL
|
||||
feed.subscriptionID = nil // MARK: TODO What should this be?
|
||||
} else {
|
||||
subscriptionsToAdd.insert(subscription)
|
||||
}
|
||||
}
|
||||
|
||||
subscriptionsToAdd.forEach { subscription in
|
||||
let feedId = String(subscription.feedID)
|
||||
let feed = account.createWebFeed(with: subscription.title, url: subscription.feedURL, webFeedID: feedId, homePageURL: subscription.siteURL)
|
||||
feed.subscriptionID = nil
|
||||
account.addWebFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
func syncFeedItems(_ account: Account, _ feedItems: [FeedWranglerFeedItem], completion: @escaping (() -> Void)) {
|
||||
let parsedItems = feedItems.map { (item: FeedWranglerFeedItem) -> ParsedItem in
|
||||
let itemID = String(item.feedItemID)
|
||||
// let authors = ...
|
||||
let parsedItem = ParsedItem(syncServiceID: itemID, uniqueID: itemID, feedURL: String(item.feedID), url: nil, externalURL: item.url, title: item.title, contentHTML: item.body, contentText: nil, summary: nil, imageURL: nil, bannerImageURL: nil, datePublished: item.publishedDate, dateModified: item.updatedDate, authors: nil, tags: nil, attachments: nil)
|
||||
|
||||
return parsedItem
|
||||
}
|
||||
|
||||
let feedIDsAndItems = Dictionary(grouping: parsedItems, by: { $0.feedURL }).mapValues { Set($0) }
|
||||
account.update(webFeedIDsAndItems: feedIDsAndItems, defaultRead: true, completion: completion)
|
||||
}
|
||||
|
||||
func syncArticleReadState(_ account: Account, _ unreadFeedItems: [FeedWranglerFeedItem]) {
|
||||
let unreadServerItemIDs = Set(unreadFeedItems.map { String($0.feedItemID) })
|
||||
let unreadLocalItemIDs = account.fetchUnreadArticleIDs()
|
||||
|
||||
// unread if unread on server
|
||||
let unreadDiffItemIDs = unreadServerItemIDs.subtracting(unreadLocalItemIDs)
|
||||
let unreadFoundArticles = account.fetchArticles(.articleIDs(unreadDiffItemIDs))
|
||||
account.update(unreadFoundArticles, statusKey: .read, flag: false)
|
||||
|
||||
let unreadFoundItemIDs = Set(unreadFoundArticles.map { $0.articleID })
|
||||
let missingArticleIDs = unreadDiffItemIDs.subtracting(unreadFoundItemIDs)
|
||||
account.ensureStatuses(missingArticleIDs, true, .read, false)
|
||||
|
||||
let readItemIDs = unreadLocalItemIDs.subtracting(unreadServerItemIDs)
|
||||
let readArtices = account.fetchArticles(.articleIDs(readItemIDs))
|
||||
account.update(readArtices, statusKey: .read, flag: true)
|
||||
|
||||
let foundReadArticleIDs = Set(readArtices.map { $0.articleID })
|
||||
let readMissingIDs = readItemIDs.subtracting(foundReadArticleIDs)
|
||||
account.ensureStatuses(readMissingIDs, true, .read, true)
|
||||
}
|
||||
|
||||
func syncArticleStarredState(_ account: Account, _ unreadFeedItems: [FeedWranglerFeedItem]) {
|
||||
let unreadServerItemIDs = Set(unreadFeedItems.map { String($0.feedItemID) })
|
||||
let unreadLocalItemIDs = account.fetchUnreadArticleIDs()
|
||||
|
||||
// starred if start on server
|
||||
let unreadDiffItemIDs = unreadServerItemIDs.subtracting(unreadLocalItemIDs)
|
||||
let unreadFoundArticles = account.fetchArticles(.articleIDs(unreadDiffItemIDs))
|
||||
account.update(unreadFoundArticles, statusKey: .starred, flag: true)
|
||||
|
||||
let unreadFoundItemIDs = Set(unreadFoundArticles.map { $0.articleID })
|
||||
let missingArticleIDs = unreadDiffItemIDs.subtracting(unreadFoundItemIDs)
|
||||
account.ensureStatuses(missingArticleIDs, true, .starred, true)
|
||||
|
||||
let readItemIDs = unreadLocalItemIDs.subtracting(unreadServerItemIDs)
|
||||
let readArtices = account.fetchArticles(.articleIDs(readItemIDs))
|
||||
account.update(readArtices, statusKey: .starred, flag: false)
|
||||
|
||||
let foundReadArticleIDs = Set(readArtices.map { $0.articleID })
|
||||
let readMissingIDs = readItemIDs.subtracting(foundReadArticleIDs)
|
||||
account.ensureStatuses(readMissingIDs, true, .starred, false)
|
||||
}
|
||||
|
||||
func syncArticleState(_ account: Account, key: ArticleStatus.Key, flag: Bool, serverFeedItems: [FeedWranglerFeedItem]) {
|
||||
let serverFeedItemIDs = serverFeedItems.map { String($0.feedID) }
|
||||
|
||||
// todo generalize this logic
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
//
|
||||
// FeedWranglerAuthorizationResult.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Jonathan Bennett on 2019-11-20.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedWranglerAuthorizationResult: Hashable, Codable {
|
||||
|
||||
let accessToken: String?
|
||||
let error: String?
|
||||
let result: String
|
||||
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case accessToken = "access_token"
|
||||
case error = "error"
|
||||
case result = "result"
|
||||
}
|
||||
}
|
18
Frameworks/Account/FeedWrangler/FeedWranglerConfig.swift
Normal file
18
Frameworks/Account/FeedWrangler/FeedWranglerConfig.swift
Normal file
@ -0,0 +1,18 @@
|
||||
//
|
||||
// FeedWranglerConfig.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Jonathan Bennett on 9/27/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum FeedWranglerConfig {
|
||||
static let pageSize = 100
|
||||
static let clientKey = "{FEEDWRANGLERKEY}" // Add FEED_WRANGLER_KEY = XYZ to SharedXcodeSettings/DeveloperSettings.xcconfig
|
||||
static let clientPath = "https://feedwrangler.net/api/v2/"
|
||||
static let clientURL = {
|
||||
URL(string: FeedWranglerConfig.clientPath)!
|
||||
}()
|
||||
}
|
62
Frameworks/Account/FeedWrangler/FeedWranglerFeedItem.swift
Normal file
62
Frameworks/Account/FeedWrangler/FeedWranglerFeedItem.swift
Normal file
@ -0,0 +1,62 @@
|
||||
//
|
||||
// FeedWranglerFeedItem.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Jonathan Bennett on 2019-10-16.4// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedWranglerFeedItem: Hashable, Codable {
|
||||
|
||||
let feedItemID: Int
|
||||
let publishedAt: Int
|
||||
let createdAt: Int
|
||||
let versionKey: Int
|
||||
let updatedAt: Int
|
||||
let url: String
|
||||
let title: String
|
||||
let starred: Bool
|
||||
let read: Bool
|
||||
let readLater: Bool
|
||||
let body: String
|
||||
let author: String?
|
||||
let feedID: Int
|
||||
let feedName: String
|
||||
|
||||
var publishedDate: Date {
|
||||
get {
|
||||
Date(timeIntervalSince1970: Double(publishedAt))
|
||||
}
|
||||
}
|
||||
|
||||
var createdDate: Date {
|
||||
get {
|
||||
Date(timeIntervalSince1970: Double(createdAt))
|
||||
}
|
||||
}
|
||||
|
||||
var updatedDate: Date {
|
||||
get {
|
||||
Date(timeIntervalSince1970: Double(updatedAt))
|
||||
}
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case feedItemID = "feed_item_id"
|
||||
case publishedAt = "published_at"
|
||||
case createdAt = "created_at"
|
||||
case versionKey = "version_key"
|
||||
case updatedAt = "updated_at"
|
||||
case url = "url"
|
||||
case title = "title"
|
||||
case starred = "starred"
|
||||
case read = "read"
|
||||
case readLater = "read_later"
|
||||
case body = "body"
|
||||
case author = "author"
|
||||
case feedID = "feed_id"
|
||||
case feedName = "feed_name"
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
//
|
||||
// FeedWranglerFeedItemsRequest.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Jonathan Bennett on 2019-10-16.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedWranglerFeedItemsRequest: Hashable, Codable {
|
||||
|
||||
let count: Int
|
||||
let feedItems: [FeedWranglerFeedItem]
|
||||
let error: String?
|
||||
let result: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case count = "count"
|
||||
case feedItems = "feed_items"
|
||||
case error = "error"
|
||||
case result = "result"
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
//
|
||||
// FeedWranglerGenericResult.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Jonathan Bennett on 2019-10-16.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedWranglerGenericResult: Hashable, Codable {
|
||||
|
||||
let error: String?
|
||||
let result: String
|
||||
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
//
|
||||
// FeedWranglerSubscription.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Jonathan Bennett on 2019-10-16.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
import Foundation
|
||||
import RSCore
|
||||
import RSParser
|
||||
|
||||
struct FeedWranglerSubscription: Hashable, Codable {
|
||||
|
||||
let title: String
|
||||
let feedID: Int
|
||||
let feedURL: String
|
||||
let siteURL: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case title = "title"
|
||||
case feedID = "feed_id"
|
||||
case feedURL = "feed_url"
|
||||
case siteURL = "site_url"
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
//
|
||||
// FeedWranglerSubscriptionResult.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Jonathan Bennett on 2019-11-20.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedWranglerSubscriptionResult: Hashable, Codable {
|
||||
|
||||
let feed: FeedWranglerSubscription
|
||||
let error: String?
|
||||
let result: String
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
//
|
||||
// FeedWranglerSubscriptionsRequest.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Jonathan Bennett on 2019-10-16.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedWranglerSubscriptionsRequest: Hashable, Codable {
|
||||
|
||||
let feeds: [FeedWranglerSubscription]
|
||||
let error: String?
|
||||
let result: String
|
||||
|
||||
}
|
@ -879,18 +879,15 @@ final class ReaderAPICaller: NSObject {
|
||||
return
|
||||
}
|
||||
|
||||
guard var components = URLComponents(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.itemIds.rawValue), resolvingAgainstBaseURL: false) else {
|
||||
completion(.failure(TransportError.noURL))
|
||||
return
|
||||
}
|
||||
|
||||
components.queryItems = [
|
||||
let url = baseURL
|
||||
.appendingPathComponent(ReaderAPIEndpoints.itemIds.rawValue)
|
||||
.appendingQueryItems([
|
||||
URLQueryItem(name: "s", value: "user/-/state/com.google/starred"),
|
||||
URLQueryItem(name: "n", value: "10000"),
|
||||
URLQueryItem(name: "output", value: "json")
|
||||
]
|
||||
])
|
||||
|
||||
guard let callURL = components.url else {
|
||||
guard let callURL = url else {
|
||||
completion(.failure(TransportError.noURL))
|
||||
return
|
||||
}
|
||||
|
@ -33,6 +33,10 @@ struct AppAssets {
|
||||
return RSImage(named: "accountFeedly")
|
||||
}()
|
||||
|
||||
static var accountFeedWrangler: RSImage! = {
|
||||
return RSImage(named: "accountFeedWrangler")
|
||||
}()
|
||||
|
||||
static var accountFreshRSS: RSImage! = {
|
||||
return RSImage(named: "accountFreshRSS")
|
||||
}()
|
||||
@ -125,6 +129,8 @@ struct AppAssets {
|
||||
return AppAssets.accountFeedbin
|
||||
case .feedly:
|
||||
return AppAssets.accountFeedly
|
||||
case .feedWrangler:
|
||||
return AppAssets.accountFeedWrangler
|
||||
case .freshRSS:
|
||||
return AppAssets.accountFreshRSS
|
||||
default:
|
||||
|
@ -15,7 +15,7 @@ class AccountsAddViewController: NSViewController {
|
||||
|
||||
private var accountsAddWindowController: NSWindowController?
|
||||
|
||||
private let addableAccountTypes: [AccountType] = [.onMyMac, .feedbin, .feedly, .freshRSS]
|
||||
private let addableAccountTypes: [AccountType] = [.onMyMac, .feedbin, .feedly, .feedWrangler, .freshRSS]
|
||||
|
||||
init() {
|
||||
super.init(nibName: "AccountsAdd", bundle: nil)
|
||||
@ -65,6 +65,9 @@ extension AccountsAddViewController: NSTableViewDelegate {
|
||||
case .feedbin:
|
||||
cell.accountNameLabel?.stringValue = NSLocalizedString("Feedbin", comment: "Feedbin")
|
||||
cell.accountImageView?.image = AppAssets.accountFeedbin
|
||||
case .feedWrangler:
|
||||
cell.accountNameLabel?.stringValue = NSLocalizedString("Feed Wrangler", comment: "Feed Wrangler")
|
||||
cell.accountImageView?.image = AppAssets.accountFeedWrangler
|
||||
case .freshRSS:
|
||||
cell.accountNameLabel?.stringValue = NSLocalizedString("FreshRSS", comment: "FreshRSS")
|
||||
cell.accountImageView?.image = AppAssets.accountFreshRSS
|
||||
@ -95,6 +98,10 @@ extension AccountsAddViewController: NSTableViewDelegate {
|
||||
let accountsFeedbinWindowController = AccountsFeedbinWindowController()
|
||||
accountsFeedbinWindowController.runSheetOnWindow(self.view.window!)
|
||||
accountsAddWindowController = accountsFeedbinWindowController
|
||||
case .feedWrangler:
|
||||
let accountsFeedWranglerWindowController = AccountsFeedWranglerWindowController()
|
||||
accountsFeedWranglerWindowController.runSheetOnWindow(self.view.window!)
|
||||
accountsAddWindowController = accountsFeedWranglerWindowController
|
||||
case .freshRSS:
|
||||
let accountsReaderAPIWindowController = AccountsReaderAPIWindowController()
|
||||
accountsReaderAPIWindowController.accountType = .freshRSS
|
||||
|
@ -80,6 +80,11 @@ final class AccountsDetailViewController: NSViewController, NSTextFieldDelegate
|
||||
accountsFreshRSSWindowController.runSheetOnWindow(self.view.window!)
|
||||
accountsWindowController = accountsFreshRSSWindowController
|
||||
break
|
||||
case .feedWrangler:
|
||||
let accountsFeedWranglerWindowController = AccountsFeedWranglerWindowController()
|
||||
accountsFeedWranglerWindowController.account = account
|
||||
accountsFeedWranglerWindowController.runSheetOnWindow(self.view.window!)
|
||||
accountsWindowController = accountsFeedWranglerWindowController
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
185
Mac/Preferences/Accounts/AccountsFeedWrangler.xib
Normal file
185
Mac/Preferences/Accounts/AccountsFeedWrangler.xib
Normal file
@ -0,0 +1,185 @@
|
||||
<?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">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="AccountsFeedWranglerWindowController" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="actionButton" destination="9mz-D9-krh" id="ozu-6Q-9Lb"/>
|
||||
<outlet property="errorMessageLabel" destination="byK-Sd-r7F" id="8zt-9d-dWQ"/>
|
||||
<outlet property="passwordTextField" destination="JSa-LY-zNQ" id="5cF-bM-CJE"/>
|
||||
<outlet property="progressIndicator" destination="B0W-bh-Evv" id="Tiq-gx-s3F"/>
|
||||
<outlet property="usernameTextField" destination="78p-Cf-f55" id="Gg5-Ce-RJv"/>
|
||||
<outlet property="window" destination="F0z-JX-Cv5" id="gIp-Ho-8D9"/>
|
||||
</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="F0z-JX-Cv5">
|
||||
<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="433" height="249"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="3840" height="2137"/>
|
||||
<view key="contentView" id="se5-gp-TjO">
|
||||
<rect key="frame" x="0.0" y="0.0" width="433" height="249"/>
|
||||
<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="7Ht-Fn-0Ya">
|
||||
<rect key="frame" x="91" y="190" width="251" height="39"/>
|
||||
<subviews>
|
||||
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Ssh-Dh-xbg">
|
||||
<rect key="frame" x="0.0" y="0.0" width="36" height="36"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="36" id="Ern-Kk-8LX"/>
|
||||
<constraint firstAttribute="width" constant="36" id="PLS-68-NMc"/>
|
||||
</constraints>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="accountFeedWrangler" id="y38-YL-woC"/>
|
||||
</imageView>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="lti-yM-8LV">
|
||||
<rect key="frame" x="53" y="0.0" width="200" height="39"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="Feed Wrangler" id="ras-dj-nP8">
|
||||
<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>
|
||||
<gridView xPlacement="trailing" yPlacement="center" rowAlignment="none" rowSpacing="12" columnSpacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="zBB-JH-huI">
|
||||
<rect key="frame" x="79" y="61" width="276" height="97"/>
|
||||
<rows>
|
||||
<gridRow id="DRl-lC-vUc"/>
|
||||
<gridRow id="eW8-uH-txq"/>
|
||||
<gridRow id="DbI-7g-Xme"/>
|
||||
</rows>
|
||||
<columns>
|
||||
<gridColumn id="fCQ-jY-Mts"/>
|
||||
<gridColumn xPlacement="leading" id="7CY-bX-6x4"/>
|
||||
</columns>
|
||||
<gridCells>
|
||||
<gridCell row="DRl-lC-vUc" column="fCQ-jY-Mts" id="4DI-01-jGD">
|
||||
<textField key="contentView" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Zy6-9c-8TI">
|
||||
<rect key="frame" x="23" y="78" width="41" height="17"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="Email:" id="DqN-SV-v35">
|
||||
<font key="font" usesAppearanceFont="YES"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
</gridCell>
|
||||
<gridCell row="DRl-lC-vUc" column="7CY-bX-6x4" id="Z0b-qS-MUJ">
|
||||
<textField key="contentView" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="78p-Cf-f55">
|
||||
<rect key="frame" x="76" y="75" width="200" height="22"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="200" id="Qin-jm-4zt"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" drawsBackground="YES" id="fCk-Tf-q01">
|
||||
<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>
|
||||
</gridCell>
|
||||
<gridCell row="eW8-uH-txq" column="fCQ-jY-Mts" id="Hqa-3w-cQv">
|
||||
<textField key="contentView" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="wEx-TM-rPM">
|
||||
<rect key="frame" x="-2" y="44" width="66" height="17"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="Password:" id="7g8-Kk-ISg">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
</gridCell>
|
||||
<gridCell row="eW8-uH-txq" column="7CY-bX-6x4" id="m16-3v-9pf">
|
||||
<secureTextField key="contentView" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JSa-LY-zNQ">
|
||||
<rect key="frame" x="76" y="41" width="200" height="22"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="200" id="eal-wa-1nU"/>
|
||||
</constraints>
|
||||
<secureTextFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" drawsBackground="YES" usesSingleLineMode="YES" id="trK-OG-tBe">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
<allowedInputSourceLocales>
|
||||
<string>NSAllRomanInputSourcesLocaleIdentifier</string>
|
||||
</allowedInputSourceLocales>
|
||||
</secureTextFieldCell>
|
||||
</secureTextField>
|
||||
</gridCell>
|
||||
<gridCell row="DbI-7g-Xme" column="fCQ-jY-Mts" headOfMergedCell="xX0-vn-AId" xPlacement="leading" id="xX0-vn-AId">
|
||||
<textField key="contentView" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="byK-Sd-r7F">
|
||||
<rect key="frame" x="-2" y="6" width="104" height="17"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" id="0yh-Ab-UTX">
|
||||
<font key="font" usesAppearanceFont="YES"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
</gridCell>
|
||||
<gridCell row="DbI-7g-Xme" column="7CY-bX-6x4" headOfMergedCell="xX0-vn-AId" id="hk5-St-E4y"/>
|
||||
</gridCells>
|
||||
</gridView>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="9mz-D9-krh">
|
||||
<rect key="frame" x="340" y="13" width="79" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Action" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="IMO-YT-k9Z">
|
||||
<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="action:" target="-2" id="Kix-5a-5Og"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="XAM-Hb-0Hw">
|
||||
<rect key="frame" x="258" y="13" width="82" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="ufs-ar-BAY">
|
||||
<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="WAD-ES-hpq"/>
|
||||
</connections>
|
||||
</button>
|
||||
<progressIndicator hidden="YES" wantsLayer="YES" horizontalHuggingPriority="750" verticalHuggingPriority="750" maxValue="100" displayedWhenStopped="NO" bezeled="NO" indeterminate="YES" controlSize="small" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="B0W-bh-Evv">
|
||||
<rect key="frame" x="209" y="166" width="16" height="16"/>
|
||||
</progressIndicator>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="9mz-D9-krh" firstAttribute="leading" secondItem="XAM-Hb-0Hw" secondAttribute="trailing" constant="12" symbolic="YES" id="CC8-HR-FDy"/>
|
||||
<constraint firstItem="XAM-Hb-0Hw" firstAttribute="centerY" secondItem="9mz-D9-krh" secondAttribute="centerY" id="M2M-fb-kfR"/>
|
||||
<constraint firstAttribute="bottom" secondItem="9mz-D9-krh" secondAttribute="bottom" constant="20" id="PK2-Ye-400"/>
|
||||
<constraint firstItem="zBB-JH-huI" firstAttribute="top" secondItem="B0W-bh-Evv" secondAttribute="bottom" constant="8" id="V7z-a7-OOG"/>
|
||||
<constraint firstItem="9mz-D9-krh" firstAttribute="top" secondItem="zBB-JH-huI" secondAttribute="bottom" constant="20" symbolic="YES" id="Wu3-hp-Vzh"/>
|
||||
<constraint firstItem="zBB-JH-huI" firstAttribute="centerX" secondItem="se5-gp-TjO" secondAttribute="centerX" id="aFI-4s-mMv"/>
|
||||
<constraint firstAttribute="trailing" secondItem="9mz-D9-krh" secondAttribute="trailing" constant="20" id="fVQ-zN-rKd"/>
|
||||
<constraint firstItem="B0W-bh-Evv" firstAttribute="top" secondItem="lti-yM-8LV" secondAttribute="bottom" constant="8" id="gq2-tB-pXH"/>
|
||||
<constraint firstItem="7Ht-Fn-0Ya" firstAttribute="top" secondItem="se5-gp-TjO" secondAttribute="top" constant="20" id="jlY-Jg-KJR"/>
|
||||
<constraint firstItem="B0W-bh-Evv" firstAttribute="centerX" secondItem="se5-gp-TjO" secondAttribute="centerX" id="lrN-Gd-iXd"/>
|
||||
<constraint firstItem="7Ht-Fn-0Ya" firstAttribute="centerX" secondItem="se5-gp-TjO" secondAttribute="centerX" id="tAZ-Te-w3H"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="-2" id="0bl-1N-AYu"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="116.5" y="136.5"/>
|
||||
</window>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="accountFeedWrangler" width="144" height="144"/>
|
||||
</resources>
|
||||
</document>
|
@ -0,0 +1,110 @@
|
||||
//
|
||||
// AccountsFeedWranglerWindowController.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Jonathan Bennett on 2019-08-29.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import Account
|
||||
import RSWeb
|
||||
|
||||
class AccountsFeedWranglerWindowController: NSWindowController {
|
||||
@IBOutlet weak var progressIndicator: NSProgressIndicator!
|
||||
@IBOutlet weak var usernameTextField: NSTextField!
|
||||
@IBOutlet weak var passwordTextField: NSSecureTextField!
|
||||
@IBOutlet weak var errorMessageLabel: NSTextField!
|
||||
@IBOutlet weak var actionButton: NSButton!
|
||||
|
||||
var account: Account?
|
||||
|
||||
private weak var hostWindow: NSWindow?
|
||||
|
||||
convenience init() {
|
||||
self.init(windowNibName: NSNib.Name("AccountsFeedWrangler"))
|
||||
}
|
||||
|
||||
override func windowDidLoad() {
|
||||
if let account = account, let credentials = try? account.retrieveCredentials(type: .basic) {
|
||||
usernameTextField.stringValue = credentials.username
|
||||
actionButton.title = NSLocalizedString("Update", comment: "Update")
|
||||
} else {
|
||||
actionButton.title = NSLocalizedString("Create", comment: "Create")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: API
|
||||
|
||||
func runSheetOnWindow(_ hostWindow: NSWindow, completionHandler handler: ((NSApplication.ModalResponse) -> Void)? = nil) {
|
||||
self.hostWindow = hostWindow
|
||||
hostWindow.beginSheet(window!, completionHandler: handler)
|
||||
}
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
@IBAction func cancel(_ sender: Any) {
|
||||
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel)
|
||||
}
|
||||
|
||||
@IBAction func action(_ sender: Any) {
|
||||
self.errorMessageLabel.stringValue = ""
|
||||
|
||||
guard !usernameTextField.stringValue.isEmpty && !passwordTextField.stringValue.isEmpty else {
|
||||
self.errorMessageLabel.stringValue = NSLocalizedString("Username & password required.", comment: "Credentials Error")
|
||||
return
|
||||
}
|
||||
|
||||
actionButton.isEnabled = false
|
||||
progressIndicator.isHidden = false
|
||||
progressIndicator.startAnimation(self)
|
||||
|
||||
let credentials = Credentials(type: .feedWranglerBasic, username: usernameTextField.stringValue, secret: passwordTextField.stringValue)
|
||||
Account.validateCredentials(type: .feedWrangler, credentials: credentials) { [weak self] result in
|
||||
|
||||
guard let self = self else { return }
|
||||
|
||||
self.actionButton.isEnabled = true
|
||||
self.progressIndicator.isHidden = true
|
||||
self.progressIndicator.stopAnimation(self)
|
||||
|
||||
switch result {
|
||||
case .success(let validatedCredentials):
|
||||
guard let validatedCredentials = validatedCredentials else {
|
||||
self.errorMessageLabel.stringValue = NSLocalizedString("Invalid email/password combination.", comment: "Credentials Error")
|
||||
return
|
||||
}
|
||||
var newAccount = false
|
||||
if self.account == nil {
|
||||
self.account = AccountManager.shared.createAccount(type: .feedWrangler)
|
||||
newAccount = true
|
||||
}
|
||||
|
||||
do {
|
||||
try self.account?.removeCredentials(type: .feedWranglerBasic)
|
||||
try self.account?.removeCredentials(type: .feedWranglerToken)
|
||||
try self.account?.storeCredentials(credentials)
|
||||
try self.account?.storeCredentials(validatedCredentials)
|
||||
if newAccount {
|
||||
self.account?.refreshAll() { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK)
|
||||
} catch {
|
||||
self.errorMessageLabel.stringValue = NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error")
|
||||
}
|
||||
|
||||
case .failure:
|
||||
|
||||
self.errorMessageLabel.stringValue = NSLocalizedString("Network error. Try again later.", comment: "Credentials Error")
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -103,18 +103,7 @@ extension AccountsPreferencesViewController: NSTableViewDelegate {
|
||||
if let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "Cell"), owner: nil) as? NSTableCellView {
|
||||
let account = sortedAccounts[row]
|
||||
cell.textField?.stringValue = account.nameForDisplay
|
||||
switch account.type {
|
||||
case .onMyMac:
|
||||
cell.imageView?.image = AppAssets.accountLocal
|
||||
case .feedbin:
|
||||
cell.imageView?.image = AppAssets.accountFeedbin
|
||||
case .freshRSS:
|
||||
cell.imageView?.image = AppAssets.accountFreshRSS
|
||||
case .feedly:
|
||||
cell.imageView?.image = AppAssets.accountFeedly
|
||||
default:
|
||||
break
|
||||
}
|
||||
cell.imageView?.image = account.smallIcon?.image
|
||||
return cell
|
||||
}
|
||||
return nil
|
||||
|
15
Mac/Resources/Assets.xcassets/accountFeedWrangler.imageset/Contents.json
vendored
Normal file
15
Mac/Resources/Assets.xcassets/accountFeedWrangler.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "outline-512.png"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
BIN
Mac/Resources/Assets.xcassets/accountFeedWrangler.imageset/outline-512.png
vendored
Normal file
BIN
Mac/Resources/Assets.xcassets/accountFeedWrangler.imageset/outline-512.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
@ -7,6 +7,10 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
3B826DCB2385C84800FC1ADB /* AccountsFeedWrangler.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3B826DB02385C84800FC1ADB /* AccountsFeedWrangler.xib */; };
|
||||
3B826DCC2385C84800FC1ADB /* AccountsFeedWranglerWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B826DCA2385C84800FC1ADB /* AccountsFeedWranglerWindowController.swift */; };
|
||||
3B826DCD2385C89600FC1ADB /* AccountsFeedWrangler.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3B826DB02385C84800FC1ADB /* AccountsFeedWrangler.xib */; };
|
||||
3B826DCE2385C89600FC1ADB /* AccountsFeedWranglerWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B826DCA2385C84800FC1ADB /* AccountsFeedWranglerWindowController.swift */; };
|
||||
49F40DF82335B71000552BF4 /* newsfoot.js in Resources */ = {isa = PBXBuildFile; fileRef = 49F40DEF2335B71000552BF4 /* newsfoot.js */; };
|
||||
49F40DF92335B71000552BF4 /* newsfoot.js in Resources */ = {isa = PBXBuildFile; fileRef = 49F40DEF2335B71000552BF4 /* newsfoot.js */; };
|
||||
5108F6B62375E612001ABC45 /* CacheCleaner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5108F6B52375E612001ABC45 /* CacheCleaner.swift */; };
|
||||
@ -1215,6 +1219,8 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
3B826DB02385C84800FC1ADB /* AccountsFeedWrangler.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountsFeedWrangler.xib; sourceTree = "<group>"; };
|
||||
3B826DCA2385C84800FC1ADB /* AccountsFeedWranglerWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsFeedWranglerWindowController.swift; sourceTree = "<group>"; };
|
||||
49F40DEF2335B71000552BF4 /* newsfoot.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = newsfoot.js; sourceTree = "<group>"; };
|
||||
5108F6B52375E612001ABC45 /* CacheCleaner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheCleaner.swift; sourceTree = "<group>"; };
|
||||
5108F6D12375EED2001ABC45 /* TimelineCustomizerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineCustomizerViewController.swift; sourceTree = "<group>"; };
|
||||
@ -2522,6 +2528,8 @@
|
||||
5144EA2E2279FAB600D19003 /* AccountsDetailViewController.swift */,
|
||||
5144EA50227B8E4500D19003 /* AccountsFeedbin.xib */,
|
||||
5144EA4F227B8E4500D19003 /* AccountsFeedbinWindowController.swift */,
|
||||
3B826DB02385C84800FC1ADB /* AccountsFeedWrangler.xib */,
|
||||
3B826DCA2385C84800FC1ADB /* AccountsFeedWranglerWindowController.swift */,
|
||||
55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */,
|
||||
55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */,
|
||||
5144EA352279FC3D00D19003 /* AccountsAddLocal.xib */,
|
||||
@ -2952,7 +2960,7 @@
|
||||
};
|
||||
513C5CE5232571C2003D4054 = {
|
||||
CreatedOnToolsVersion = 11.0;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
DevelopmentTeam = 8EQFQ9RY84;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
518B2ED12351B3DD00400001 = {
|
||||
@ -2962,7 +2970,7 @@
|
||||
TestTargetID = 840D617B2029031C009BC708;
|
||||
};
|
||||
6581C73220CED60000F4AD34 = {
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
DevelopmentTeam = 8EQFQ9RY84;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
65ED3FA2235DEF6C0081F399 = {
|
||||
@ -2975,7 +2983,7 @@
|
||||
};
|
||||
840D617B2029031C009BC708 = {
|
||||
CreatedOnToolsVersion = 9.3;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
DevelopmentTeam = 8EQFQ9RY84;
|
||||
ProvisioningStyle = Automatic;
|
||||
SystemCapabilities = {
|
||||
com.apple.BackgroundModes = {
|
||||
@ -2985,7 +2993,7 @@
|
||||
};
|
||||
849C645F1ED37A5D003D8FC0 = {
|
||||
CreatedOnToolsVersion = 8.2.1;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
DevelopmentTeam = 8EQFQ9RY84;
|
||||
ProvisioningStyle = Automatic;
|
||||
SystemCapabilities = {
|
||||
com.apple.HardenedRuntime = {
|
||||
@ -2995,7 +3003,7 @@
|
||||
};
|
||||
849C64701ED37A5D003D8FC0 = {
|
||||
CreatedOnToolsVersion = 8.2.1;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
DevelopmentTeam = 8EQFQ9RY84;
|
||||
ProvisioningStyle = Automatic;
|
||||
TestTargetID = 849C645F1ED37A5D003D8FC0;
|
||||
};
|
||||
@ -3393,6 +3401,7 @@
|
||||
65ED4066235DEF6C0081F399 /* TimelineTableView.xib in Resources */,
|
||||
65ED4067235DEF6C0081F399 /* page.html in Resources */,
|
||||
65ED4068235DEF6C0081F399 /* MainWindow.storyboard in Resources */,
|
||||
3B826DCD2385C89600FC1ADB /* AccountsFeedWrangler.xib in Resources */,
|
||||
65ED4069235DEF6C0081F399 /* AccountsReaderAPI.xib in Resources */,
|
||||
65ED406A235DEF6C0081F399 /* newsfoot.js in Resources */,
|
||||
65ED406B235DEF6C0081F399 /* CrashReporterWindow.xib in Resources */,
|
||||
@ -3478,6 +3487,7 @@
|
||||
8405DDA222168920008CE1BF /* TimelineTableView.xib in Resources */,
|
||||
B528F81E23333C7E00E735DD /* page.html in Resources */,
|
||||
8483630E2262A3FE00DA1D35 /* MainWindow.storyboard in Resources */,
|
||||
3B826DCB2385C84800FC1ADB /* AccountsFeedWrangler.xib in Resources */,
|
||||
55E15BCB229D65A900D6602A /* AccountsReaderAPI.xib in Resources */,
|
||||
49F40DF82335B71000552BF4 /* newsfoot.js in Resources */,
|
||||
84BAE64921CEDAF20046DB56 /* CrashReporterWindow.xib in Resources */,
|
||||
@ -3829,6 +3839,7 @@
|
||||
65ED3FFF235DEF6C0081F399 /* SidebarOutlineDataSource.swift in Sources */,
|
||||
65ED4000235DEF6C0081F399 /* SidebarCellAppearance.swift in Sources */,
|
||||
65ED4001235DEF6C0081F399 /* StarredFeedDelegate.swift in Sources */,
|
||||
3B826DCE2385C89600FC1ADB /* AccountsFeedWranglerWindowController.swift in Sources */,
|
||||
65ED4002235DEF6C0081F399 /* FaviconDownloader.swift in Sources */,
|
||||
65ED4003235DEF6C0081F399 /* AdvancedPreferencesViewController.swift in Sources */,
|
||||
65ED4004235DEF6C0081F399 /* SharingServicePickerDelegate.swift in Sources */,
|
||||
@ -4151,6 +4162,7 @@
|
||||
84CAFCAF22BC8C35007694F0 /* FetchRequestOperation.swift in Sources */,
|
||||
8426119E1FCB6ED40086A189 /* HTMLMetadataDownloader.swift in Sources */,
|
||||
849A976E1ED9EBC8007D329B /* TimelineViewController.swift in Sources */,
|
||||
3B826DCC2385C84800FC1ADB /* AccountsFeedWranglerWindowController.swift in Sources */,
|
||||
5154368B229404D1005E1CDF /* FaviconGenerator.swift in Sources */,
|
||||
5183CCE6226F4E110010922C /* RefreshInterval.swift in Sources */,
|
||||
849A97771ED9EC04007D329B /* TimelineCellData.swift in Sources */,
|
||||
|
Loading…
x
Reference in New Issue
Block a user