Google Reader API Account Provider and initial integration
* Creation of account classes (based on FeedBin) * Integration on Mac side into account dialog * Initial authentication call works and extracts auth token, but no where to put it right now.
This commit is contained in:
parent
6b93576f37
commit
84dbdf25e2
|
@ -33,6 +33,7 @@ public enum AccountType: Int {
|
|||
case feedbin = 17
|
||||
case feedWrangler = 18
|
||||
case newsBlur = 19
|
||||
case googleReaderCompatible = 20
|
||||
// TODO: more
|
||||
}
|
||||
|
||||
|
@ -196,6 +197,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
self.delegate = LocalAccountDelegate()
|
||||
case .feedbin:
|
||||
self.delegate = FeedbinAccountDelegate(dataFolder: dataFolder, transport: transport)
|
||||
case .googleReaderCompatible:
|
||||
self.delegate = GoogleReaderCompatibleAccountDelegate(dataFolder: dataFolder, transport: transport)
|
||||
default:
|
||||
fatalError("Only Local and Feedbin accounts are supported")
|
||||
}
|
||||
|
@ -223,6 +226,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
defaultName = "FeedWrangler"
|
||||
case .newsBlur:
|
||||
defaultName = "NewsBlur"
|
||||
case .googleReaderCompatible:
|
||||
defaultName = "Google Reader Compatible"
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(downloadProgressDidChange(_:)), name: .DownloadProgressDidChange, object: nil)
|
||||
|
@ -254,6 +259,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
switch credentials {
|
||||
case .basic(let username, _):
|
||||
self.username = username
|
||||
case .googleLogin(let username, _, _, _):
|
||||
self.username = username
|
||||
}
|
||||
|
||||
try CredentialsManager.storeCredentials(credentials, server: server)
|
||||
|
@ -283,6 +290,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
LocalAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion)
|
||||
case .feedbin:
|
||||
FeedbinAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion)
|
||||
case .googleReaderCompatible:
|
||||
GoogleReaderCompatibleAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
|
@ -35,6 +35,17 @@
|
|||
51E490362288C37100C791F0 /* FeedbinDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E490352288C37100C791F0 /* FeedbinDate.swift */; };
|
||||
51E59599228C77BC00FCC42B /* FeedbinUnreadEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E59598228C77BC00FCC42B /* FeedbinUnreadEntry.swift */; };
|
||||
51E5959B228C781500FCC42B /* FeedbinStarredEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */; };
|
||||
552032F6229D5D5A009559E0 /* GoogleReaderCompatibleDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032EB229D5D5A009559E0 /* GoogleReaderCompatibleDate.swift */; };
|
||||
552032F7229D5D5A009559E0 /* GoogleReaderCompatibleIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032EC229D5D5A009559E0 /* GoogleReaderCompatibleIcon.swift */; };
|
||||
552032F8229D5D5A009559E0 /* GoogleReaderCompatibleEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032ED229D5D5A009559E0 /* GoogleReaderCompatibleEntry.swift */; };
|
||||
552032F9229D5D5A009559E0 /* GoogleReaderCompatibleSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032EE229D5D5A009559E0 /* GoogleReaderCompatibleSubscription.swift */; };
|
||||
552032FA229D5D5A009559E0 /* GoogleReaderCompatibleStarredEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032EF229D5D5A009559E0 /* GoogleReaderCompatibleStarredEntry.swift */; };
|
||||
552032FB229D5D5A009559E0 /* GoogleReaderCompatibleTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F0229D5D5A009559E0 /* GoogleReaderCompatibleTag.swift */; };
|
||||
552032FC229D5D5A009559E0 /* GoogleReaderCompatibleUnreadEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F1229D5D5A009559E0 /* GoogleReaderCompatibleUnreadEntry.swift */; };
|
||||
552032FD229D5D5A009559E0 /* GoogleReaderCompatibleTagging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F2229D5D5A009559E0 /* GoogleReaderCompatibleTagging.swift */; };
|
||||
552032FE229D5D5A009559E0 /* GoogleReaderCompatibleAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F3229D5D5A009559E0 /* GoogleReaderCompatibleAccountDelegate.swift */; };
|
||||
552032FF229D5D5A009559E0 /* GoogleReaderCompatibleImportResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F4229D5D5A009559E0 /* GoogleReaderCompatibleImportResult.swift */; };
|
||||
55203300229D5D5A009559E0 /* GoogleReaderCompatibleAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F5229D5D5A009559E0 /* GoogleReaderCompatibleAPICaller.swift */; };
|
||||
841973FE1F6DD1BC006346C4 /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 841973EF1F6DD19E006346C4 /* RSCore.framework */; };
|
||||
841973FF1F6DD1C5006346C4 /* RSParser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 841973FA1F6DD1AC006346C4 /* RSParser.framework */; };
|
||||
841974011F6DD1EC006346C4 /* Folder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841974001F6DD1EC006346C4 /* Folder.swift */; };
|
||||
|
@ -136,6 +147,17 @@
|
|||
51E490352288C37100C791F0 /* FeedbinDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinDate.swift; sourceTree = "<group>"; };
|
||||
51E59598228C77BC00FCC42B /* FeedbinUnreadEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinUnreadEntry.swift; sourceTree = "<group>"; };
|
||||
51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinStarredEntry.swift; sourceTree = "<group>"; };
|
||||
552032EB229D5D5A009559E0 /* GoogleReaderCompatibleDate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleDate.swift; sourceTree = "<group>"; };
|
||||
552032EC229D5D5A009559E0 /* GoogleReaderCompatibleIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleIcon.swift; sourceTree = "<group>"; };
|
||||
552032ED229D5D5A009559E0 /* GoogleReaderCompatibleEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleEntry.swift; sourceTree = "<group>"; };
|
||||
552032EE229D5D5A009559E0 /* GoogleReaderCompatibleSubscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleSubscription.swift; sourceTree = "<group>"; };
|
||||
552032EF229D5D5A009559E0 /* GoogleReaderCompatibleStarredEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleStarredEntry.swift; sourceTree = "<group>"; };
|
||||
552032F0229D5D5A009559E0 /* GoogleReaderCompatibleTag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleTag.swift; sourceTree = "<group>"; };
|
||||
552032F1229D5D5A009559E0 /* GoogleReaderCompatibleUnreadEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleUnreadEntry.swift; sourceTree = "<group>"; };
|
||||
552032F2229D5D5A009559E0 /* GoogleReaderCompatibleTagging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleTagging.swift; sourceTree = "<group>"; };
|
||||
552032F3229D5D5A009559E0 /* GoogleReaderCompatibleAccountDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleAccountDelegate.swift; sourceTree = "<group>"; };
|
||||
552032F4229D5D5A009559E0 /* GoogleReaderCompatibleImportResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleImportResult.swift; sourceTree = "<group>"; };
|
||||
552032F5229D5D5A009559E0 /* GoogleReaderCompatibleAPICaller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleAPICaller.swift; sourceTree = "<group>"; };
|
||||
841973E81F6DD19E006346C4 /* RSCore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSCore.xcodeproj; path = ../RSCore/RSCore.xcodeproj; sourceTree = "<group>"; };
|
||||
841973F41F6DD1AC006346C4 /* RSParser.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSParser.xcodeproj; path = ../RSParser/RSParser.xcodeproj; sourceTree = "<group>"; };
|
||||
841974001F6DD1EC006346C4 /* Folder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Folder.swift; sourceTree = "<group>"; };
|
||||
|
@ -222,6 +244,24 @@
|
|||
path = JSON;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
552032EA229D5D5A009559E0 /* GoogleReaderCompatible */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
552032EB229D5D5A009559E0 /* GoogleReaderCompatibleDate.swift */,
|
||||
552032EC229D5D5A009559E0 /* GoogleReaderCompatibleIcon.swift */,
|
||||
552032ED229D5D5A009559E0 /* GoogleReaderCompatibleEntry.swift */,
|
||||
552032EE229D5D5A009559E0 /* GoogleReaderCompatibleSubscription.swift */,
|
||||
552032EF229D5D5A009559E0 /* GoogleReaderCompatibleStarredEntry.swift */,
|
||||
552032F0229D5D5A009559E0 /* GoogleReaderCompatibleTag.swift */,
|
||||
552032F1229D5D5A009559E0 /* GoogleReaderCompatibleUnreadEntry.swift */,
|
||||
552032F2229D5D5A009559E0 /* GoogleReaderCompatibleTagging.swift */,
|
||||
552032F3229D5D5A009559E0 /* GoogleReaderCompatibleAccountDelegate.swift */,
|
||||
552032F4229D5D5A009559E0 /* GoogleReaderCompatibleImportResult.swift */,
|
||||
552032F5229D5D5A009559E0 /* GoogleReaderCompatibleAPICaller.swift */,
|
||||
);
|
||||
path = GoogleReaderCompatible;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
841973E91F6DD19E006346C4 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -302,6 +342,7 @@
|
|||
5165D71F22835E9800D9D53D /* FeedFinder */,
|
||||
8419742B1F6DDE84006346C4 /* LocalAccount */,
|
||||
84245C7D1FDDD2580074AFBB /* Feedbin */,
|
||||
552032EA229D5D5A009559E0 /* GoogleReaderCompatible */,
|
||||
848935031F62484F00CEBD24 /* AccountTests */,
|
||||
848934F71F62484F00CEBD24 /* Products */,
|
||||
8469F80F1F6DC3C10084783E /* Frameworks */,
|
||||
|
@ -515,14 +556,17 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
84C8B3F41F89DE430053CCA6 /* DataExtensions.swift in Sources */,
|
||||
552032F9229D5D5A009559E0 /* GoogleReaderCompatibleSubscription.swift in Sources */,
|
||||
84C3654A1F899F3B001EC85C /* CombinedRefreshProgress.swift in Sources */,
|
||||
8469F81C1F6DD15E0084783E /* Account.swift in Sources */,
|
||||
5144EA4E227B829A00D19003 /* FeedbinAccountDelegate.swift in Sources */,
|
||||
51E5959B228C781500FCC42B /* FeedbinStarredEntry.swift in Sources */,
|
||||
846E77451F6EF9B900A165E2 /* Container.swift in Sources */,
|
||||
552032FD229D5D5A009559E0 /* GoogleReaderCompatibleTagging.swift in Sources */,
|
||||
84F73CF1202788D90000BCEF /* ArticleFetcher.swift in Sources */,
|
||||
841974251F6DDCE4006346C4 /* AccountDelegate.swift in Sources */,
|
||||
5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */,
|
||||
552032FA229D5D5A009559E0 /* GoogleReaderCompatibleStarredEntry.swift in Sources */,
|
||||
846E77541F6F00E300A165E2 /* AccountManager.swift in Sources */,
|
||||
51E490362288C37100C791F0 /* FeedbinDate.swift in Sources */,
|
||||
5165D72922835F7A00D9D53D /* FeedSpecifier.swift in Sources */,
|
||||
|
@ -532,11 +576,19 @@
|
|||
5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */,
|
||||
84B99C9F1FAE8D3200ECDEDB /* ContainerPath.swift in Sources */,
|
||||
5133231122810EB200C30F19 /* FeedbinIcon.swift in Sources */,
|
||||
552032F6229D5D5A009559E0 /* GoogleReaderCompatibleDate.swift in Sources */,
|
||||
846E77501F6EF9C400A165E2 /* LocalAccountRefresher.swift in Sources */,
|
||||
55203300229D5D5A009559E0 /* GoogleReaderCompatibleAPICaller.swift in Sources */,
|
||||
51E3EB41229AF61B00645299 /* AccountError.swift in Sources */,
|
||||
51E59599228C77BC00FCC42B /* FeedbinUnreadEntry.swift in Sources */,
|
||||
552032F8229D5D5A009559E0 /* GoogleReaderCompatibleEntry.swift in Sources */,
|
||||
552032FB229D5D5A009559E0 /* GoogleReaderCompatibleTag.swift in Sources */,
|
||||
5165D72822835F7800D9D53D /* FeedFinder.swift in Sources */,
|
||||
552032F7229D5D5A009559E0 /* GoogleReaderCompatibleIcon.swift in Sources */,
|
||||
552032FF229D5D5A009559E0 /* GoogleReaderCompatibleImportResult.swift in Sources */,
|
||||
51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */,
|
||||
552032FE229D5D5A009559E0 /* GoogleReaderCompatibleAccountDelegate.swift in Sources */,
|
||||
552032FC229D5D5A009559E0 /* GoogleReaderCompatibleUnreadEntry.swift in Sources */,
|
||||
84D09623217418DC00D77525 /* FeedbinTagging.swift in Sources */,
|
||||
84CAD7161FDF2E22000F0755 /* FeedbinEntry.swift in Sources */,
|
||||
5165D72A22835F7D00D9D53D /* HTMLFeedFinder.swift in Sources */,
|
||||
|
|
|
@ -0,0 +1,615 @@
|
|||
//
|
||||
// GoogleReaderCompatibleAPICaller.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 5/2/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
// GoogleReaderCompatible currently has a maximum of 250 requests per second. If you begin to receive
|
||||
// HTTP Response Codes of 403, you have exceeded this limit. Wait 5 minutes and your
|
||||
// IP address will become unblocked and you can use the service again.
|
||||
|
||||
import Foundation
|
||||
import RSWeb
|
||||
|
||||
enum CreateGoogleReaderSubscriptionResult {
|
||||
case created(GoogleReaderCompatibleSubscription)
|
||||
case multipleChoice([GoogleReaderCompatibleSubscriptionChoice])
|
||||
case alreadySubscribed
|
||||
case notFound
|
||||
}
|
||||
|
||||
final class GoogleReaderCompatibleAPICaller: NSObject {
|
||||
|
||||
struct ConditionalGetKeys {
|
||||
static let subscriptions = "subscriptions"
|
||||
static let tags = "tags"
|
||||
static let taggings = "taggings"
|
||||
static let icons = "icons"
|
||||
static let unreadEntries = "unreadEntries"
|
||||
static let starredEntries = "starredEntries"
|
||||
}
|
||||
|
||||
private let GoogleReaderCompatibleBaseURL = URL(string: "https://api.GoogleReaderCompatible.com/v2/")!
|
||||
private var transport: Transport!
|
||||
|
||||
var credentials: Credentials?
|
||||
var apiAuthToken: String?
|
||||
weak var accountMetadata: AccountMetadata?
|
||||
|
||||
init(transport: Transport) {
|
||||
super.init()
|
||||
self.transport = transport
|
||||
}
|
||||
|
||||
func validateCredentials(completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||
guard let credentials = credentials else {
|
||||
completion(.failure(CredentialsError.incompleteCredentials))
|
||||
return
|
||||
}
|
||||
|
||||
guard case .googleLogin(let username, let password, let apiUrl, _) = credentials else {
|
||||
completion(.failure(CredentialsError.incompleteCredentials))
|
||||
return
|
||||
}
|
||||
|
||||
guard var loginURL = URLComponents(url: apiUrl.appendingPathComponent("/accounts/ClientLogin"), resolvingAgainstBaseURL: false) else {
|
||||
completion(.failure(CredentialsError.incompleteCredentials))
|
||||
return
|
||||
}
|
||||
|
||||
loginURL.queryItems = [
|
||||
URLQueryItem(name: "Email", value: username),
|
||||
URLQueryItem(name: "Passwd", value: password)
|
||||
]
|
||||
|
||||
guard let callURL = loginURL.url else {
|
||||
completion(.failure(CredentialsError.incompleteCredentials))
|
||||
return
|
||||
}
|
||||
|
||||
let request = URLRequest(url: callURL, credentials: credentials)
|
||||
|
||||
transport.send(request: request) { result in
|
||||
switch result {
|
||||
case .success(let (_, data)):
|
||||
guard let resultData = data else {
|
||||
completion(.failure(TransportError.noData))
|
||||
break
|
||||
}
|
||||
|
||||
// Convert the return data to UTF8 and then parse out the Auth token
|
||||
guard let rawData = String(data: resultData, encoding: .utf8) else {
|
||||
completion(.failure(TransportError.noData))
|
||||
break
|
||||
}
|
||||
|
||||
var authData: [String: String] = [:]
|
||||
rawData.split(separator: "\n").forEach({ (line: Substring) in
|
||||
let items = line.split(separator: "=").map{String($0)}
|
||||
authData[items[0]] = items[1]
|
||||
})
|
||||
|
||||
guard let authString = authData["Auth"] else {
|
||||
completion(.failure(CredentialsError.incompleteCredentials))
|
||||
break
|
||||
}
|
||||
|
||||
// Save Auth Token for later use
|
||||
self.apiAuthToken = authString
|
||||
|
||||
completion(.success(true))
|
||||
case .failure(let error):
|
||||
switch error {
|
||||
case TransportError.httpError(let status):
|
||||
if status == 401 {
|
||||
completion(.success(false))
|
||||
} else {
|
||||
completion(.failure(error))
|
||||
}
|
||||
default:
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func importOPML(opmlData: Data, completion: @escaping (Result<GoogleReaderCompatibleImportResult, Error>) -> Void) {
|
||||
|
||||
let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("imports.json")
|
||||
var request = URLRequest(url: callURL, credentials: credentials)
|
||||
request.addValue("text/xml; charset=utf-8", forHTTPHeaderField: HTTPRequestHeader.contentType)
|
||||
|
||||
transport.send(request: request, method: HTTPMethod.post, payload: opmlData) { result in
|
||||
|
||||
switch result {
|
||||
case .success(let (_, data)):
|
||||
|
||||
guard let resultData = data else {
|
||||
completion(.failure(TransportError.noData))
|
||||
break
|
||||
}
|
||||
|
||||
do {
|
||||
let result = try JSONDecoder().decode(GoogleReaderCompatibleImportResult.self, from: resultData)
|
||||
completion(.success(result))
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func retrieveOPMLImportResult(importID: Int, completion: @escaping (Result<GoogleReaderCompatibleImportResult?, Error>) -> Void) {
|
||||
|
||||
let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("imports/\(importID).json")
|
||||
let request = URLRequest(url: callURL, credentials: credentials)
|
||||
|
||||
transport.send(request: request, resultType: GoogleReaderCompatibleImportResult.self) { result in
|
||||
|
||||
switch result {
|
||||
case .success(let (_, importResult)):
|
||||
completion(.success(importResult))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func retrieveTags(completion: @escaping (Result<[GoogleReaderCompatibleTag]?, Error>) -> Void) {
|
||||
|
||||
let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("tags.json")
|
||||
let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.tags]
|
||||
let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet)
|
||||
|
||||
transport.send(request: request, resultType: [GoogleReaderCompatibleTag].self) { result in
|
||||
|
||||
switch result {
|
||||
case .success(let (response, tags)):
|
||||
self.storeConditionalGet(key: ConditionalGetKeys.tags, headers: response.allHeaderFields)
|
||||
completion(.success(tags))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func renameTag(oldName: String, newName: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("tags.json")
|
||||
let request = URLRequest(url: callURL, credentials: credentials)
|
||||
let payload = GoogleReaderCompatibleRenameTag(oldName: oldName, newName: newName)
|
||||
transport.send(request: request, method: HTTPMethod.post, payload: payload, completion: completion)
|
||||
}
|
||||
|
||||
func deleteTag(name: String, completion: @escaping (Result<[GoogleReaderCompatibleTagging]?, Error>) -> Void) {
|
||||
|
||||
let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("tags.json")
|
||||
let request = URLRequest(url: callURL, credentials: credentials)
|
||||
let payload = GoogleReaderCompatibleDeleteTag(name: name)
|
||||
|
||||
transport.send(request: request, method: HTTPMethod.delete, payload: payload, resultType: [GoogleReaderCompatibleTagging].self) { result in
|
||||
|
||||
switch result {
|
||||
case .success(let (_, taggings)):
|
||||
completion(.success(taggings))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func retrieveSubscriptions(completion: @escaping (Result<[GoogleReaderCompatibleSubscription]?, Error>) -> Void) {
|
||||
|
||||
let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("subscriptions.json")
|
||||
let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.subscriptions]
|
||||
let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet)
|
||||
|
||||
transport.send(request: request, resultType: [GoogleReaderCompatibleSubscription].self) { result in
|
||||
|
||||
switch result {
|
||||
case .success(let (response, subscriptions)):
|
||||
self.storeConditionalGet(key: ConditionalGetKeys.subscriptions, headers: response.allHeaderFields)
|
||||
completion(.success(subscriptions))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func createSubscription(url: String, completion: @escaping (Result<CreateGoogleReaderSubscriptionResult, Error>) -> Void) {
|
||||
|
||||
let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("subscriptions.json")
|
||||
var request = URLRequest(url: callURL, credentials: credentials)
|
||||
request.addValue("application/json; charset=utf-8", forHTTPHeaderField: HTTPRequestHeader.contentType)
|
||||
|
||||
let payload: Data
|
||||
do {
|
||||
payload = try JSONEncoder().encode(GoogleReaderCompatibleCreateSubscription(feedURL: url))
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
transport.send(request: request, method: HTTPMethod.post, payload: payload) { result in
|
||||
|
||||
switch result {
|
||||
case .success(let (response, data)):
|
||||
|
||||
switch response.forcedStatusCode {
|
||||
case 201:
|
||||
guard let subData = data else {
|
||||
completion(.failure(TransportError.noData))
|
||||
break
|
||||
}
|
||||
do {
|
||||
let subscription = try JSONDecoder().decode(GoogleReaderCompatibleSubscription.self, from: subData)
|
||||
completion(.success(.created(subscription)))
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
case 300:
|
||||
guard let subData = data else {
|
||||
completion(.failure(TransportError.noData))
|
||||
break
|
||||
}
|
||||
do {
|
||||
let subscriptions = try JSONDecoder().decode([GoogleReaderCompatibleSubscriptionChoice].self, from: subData)
|
||||
completion(.success(.multipleChoice(subscriptions)))
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
case 302:
|
||||
completion(.success(.alreadySubscribed))
|
||||
default:
|
||||
completion(.failure(TransportError.httpError(status: response.forcedStatusCode)))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
|
||||
switch error {
|
||||
case TransportError.httpError(let status):
|
||||
switch status {
|
||||
case 401:
|
||||
// I don't know why we get 401's here. This looks like a GoogleReaderCompatible bug, but it only happens
|
||||
// when you are already subscribed to the feed.
|
||||
completion(.success(.alreadySubscribed))
|
||||
case 404:
|
||||
completion(.success(.notFound))
|
||||
default:
|
||||
completion(.failure(error))
|
||||
}
|
||||
default:
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func renameSubscription(subscriptionID: String, newName: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("subscriptions/\(subscriptionID)/update.json")
|
||||
let request = URLRequest(url: callURL, credentials: credentials)
|
||||
let payload = GoogleReaderCompatibleUpdateSubscription(title: newName)
|
||||
transport.send(request: request, method: HTTPMethod.post, payload: payload, completion: completion)
|
||||
}
|
||||
|
||||
func deleteSubscription(subscriptionID: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("subscriptions/\(subscriptionID).json")
|
||||
let request = URLRequest(url: callURL, credentials: credentials)
|
||||
transport.send(request: request, method: HTTPMethod.delete, completion: completion)
|
||||
}
|
||||
|
||||
func retrieveTaggings(completion: @escaping (Result<[GoogleReaderCompatibleTagging]?, Error>) -> Void) {
|
||||
|
||||
let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("taggings.json")
|
||||
let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.taggings]
|
||||
let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet)
|
||||
|
||||
transport.send(request: request, resultType: [GoogleReaderCompatibleTagging].self) { result in
|
||||
|
||||
switch result {
|
||||
case .success(let (response, taggings)):
|
||||
self.storeConditionalGet(key: ConditionalGetKeys.taggings, headers: response.allHeaderFields)
|
||||
completion(.success(taggings))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func createTagging(feedID: Int, name: String, completion: @escaping (Result<Int, Error>) -> Void) {
|
||||
|
||||
let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("taggings.json")
|
||||
var request = URLRequest(url: callURL, credentials: credentials)
|
||||
request.addValue("application/json; charset=utf-8", forHTTPHeaderField: HTTPRequestHeader.contentType)
|
||||
|
||||
let payload: Data
|
||||
do {
|
||||
payload = try JSONEncoder().encode(GoogleReaderCompatibleCreateTagging(feedID: feedID, name: name))
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
transport.send(request: request, method: HTTPMethod.post, payload:payload) { result in
|
||||
|
||||
switch result {
|
||||
case .success(let (response, _)):
|
||||
if let taggingLocation = response.valueForHTTPHeaderField(HTTPResponseHeader.location),
|
||||
let lowerBound = taggingLocation.range(of: "v2/taggings/")?.upperBound,
|
||||
let upperBound = taggingLocation.range(of: ".json")?.lowerBound,
|
||||
let taggingID = Int(taggingLocation[lowerBound..<upperBound]) {
|
||||
completion(.success(taggingID))
|
||||
} else {
|
||||
completion(.failure(TransportError.noData))
|
||||
}
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func deleteTagging(taggingID: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("taggings/\(taggingID).json")
|
||||
var request = URLRequest(url: callURL, credentials: credentials)
|
||||
request.addValue("application/json; charset=utf-8", forHTTPHeaderField: HTTPRequestHeader.contentType)
|
||||
transport.send(request: request, method: HTTPMethod.delete, completion: completion)
|
||||
}
|
||||
|
||||
func retrieveIcons(completion: @escaping (Result<[GoogleReaderCompatibleIcon]?, Error>) -> Void) {
|
||||
|
||||
let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("icons.json")
|
||||
let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.icons]
|
||||
let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet)
|
||||
|
||||
transport.send(request: request, resultType: [GoogleReaderCompatibleIcon].self) { result in
|
||||
|
||||
switch result {
|
||||
case .success(let (response, icons)):
|
||||
self.storeConditionalGet(key: ConditionalGetKeys.icons, headers: response.allHeaderFields)
|
||||
completion(.success(icons))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func retrieveEntries(articleIDs: [String], completion: @escaping (Result<([GoogleReaderCompatibleEntry]?), Error>) -> Void) {
|
||||
|
||||
guard !articleIDs.isEmpty else {
|
||||
completion(.success(([GoogleReaderCompatibleEntry]())))
|
||||
return
|
||||
}
|
||||
|
||||
let concatIDs = articleIDs.reduce("") { param, articleID in return param + ",\(articleID)" }
|
||||
let paramIDs = String(concatIDs.dropFirst())
|
||||
|
||||
var callComponents = URLComponents(url: GoogleReaderCompatibleBaseURL.appendingPathComponent("entries.json"), resolvingAgainstBaseURL: false)!
|
||||
callComponents.queryItems = [URLQueryItem(name: "ids", value: paramIDs), URLQueryItem(name: "mode", value: "extended")]
|
||||
let request = URLRequest(url: callComponents.url!, credentials: credentials)
|
||||
|
||||
transport.send(request: request, resultType: [GoogleReaderCompatibleEntry].self) { result in
|
||||
|
||||
switch result {
|
||||
case .success(let (_, entries)):
|
||||
completion(.success((entries)))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func retrieveEntries(feedID: String, completion: @escaping (Result<([GoogleReaderCompatibleEntry]?, String?), Error>) -> Void) {
|
||||
|
||||
let since = Calendar.current.date(byAdding: .month, value: -3, to: Date()) ?? Date()
|
||||
let sinceString = GoogleReaderCompatibleDate.formatter.string(from: since)
|
||||
|
||||
var callComponents = URLComponents(url: GoogleReaderCompatibleBaseURL.appendingPathComponent("feeds/\(feedID)/entries.json"), resolvingAgainstBaseURL: false)!
|
||||
callComponents.queryItems = [URLQueryItem(name: "since", value: sinceString), URLQueryItem(name: "per_page", value: "100"), URLQueryItem(name: "mode", value: "extended")]
|
||||
let request = URLRequest(url: callComponents.url!, credentials: credentials)
|
||||
|
||||
transport.send(request: request, resultType: [GoogleReaderCompatibleEntry].self) { result in
|
||||
|
||||
switch result {
|
||||
case .success(let (response, entries)):
|
||||
|
||||
let pagingInfo = HTTPLinkPagingInfo(urlResponse: response)
|
||||
completion(.success((entries, pagingInfo.nextPage)))
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func retrieveEntries(completion: @escaping (Result<([GoogleReaderCompatibleEntry]?, String?, Int?), Error>) -> Void) {
|
||||
|
||||
let since: Date = {
|
||||
if let lastArticleFetch = accountMetadata?.lastArticleFetch {
|
||||
return lastArticleFetch
|
||||
} else {
|
||||
return Calendar.current.date(byAdding: .month, value: -3, to: Date()) ?? Date()
|
||||
}
|
||||
}()
|
||||
|
||||
let sinceString = GoogleReaderCompatibleDate.formatter.string(from: since)
|
||||
var callComponents = URLComponents(url: GoogleReaderCompatibleBaseURL.appendingPathComponent("entries.json"), resolvingAgainstBaseURL: false)!
|
||||
callComponents.queryItems = [URLQueryItem(name: "since", value: sinceString), URLQueryItem(name: "per_page", value: "100"), URLQueryItem(name: "mode", value: "extended")]
|
||||
let request = URLRequest(url: callComponents.url!, credentials: credentials)
|
||||
|
||||
transport.send(request: request, resultType: [GoogleReaderCompatibleEntry].self) { result in
|
||||
|
||||
switch result {
|
||||
case .success(let (response, entries)):
|
||||
|
||||
let dateInfo = HTTPDateInfo(urlResponse: response)
|
||||
self.accountMetadata?.lastArticleFetch = dateInfo?.date
|
||||
|
||||
let pagingInfo = HTTPLinkPagingInfo(urlResponse: response)
|
||||
let lastPageNumber = self.extractPageNumber(link: pagingInfo.lastPage)
|
||||
completion(.success((entries, pagingInfo.nextPage, lastPageNumber)))
|
||||
|
||||
case .failure(let error):
|
||||
self.accountMetadata?.lastArticleFetch = nil
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func retrieveEntries(page: String, completion: @escaping (Result<([GoogleReaderCompatibleEntry]?, String?), Error>) -> Void) {
|
||||
|
||||
guard let url = URL(string: page), var callComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
|
||||
completion(.success((nil, nil)))
|
||||
return
|
||||
}
|
||||
|
||||
callComponents.queryItems?.append(URLQueryItem(name: "mode", value: "extended"))
|
||||
let request = URLRequest(url: callComponents.url!, credentials: credentials)
|
||||
|
||||
transport.send(request: request, resultType: [GoogleReaderCompatibleEntry].self) { result in
|
||||
|
||||
switch result {
|
||||
case .success(let (response, entries)):
|
||||
|
||||
let pagingInfo = HTTPLinkPagingInfo(urlResponse: response)
|
||||
completion(.success((entries, pagingInfo.nextPage)))
|
||||
|
||||
case .failure(let error):
|
||||
self.accountMetadata?.lastArticleFetch = nil
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func retrieveUnreadEntries(completion: @escaping (Result<[Int]?, Error>) -> Void) {
|
||||
|
||||
let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("unread_entries.json")
|
||||
let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.unreadEntries]
|
||||
let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet)
|
||||
|
||||
transport.send(request: request, resultType: [Int].self) { result in
|
||||
|
||||
switch result {
|
||||
case .success(let (response, unreadEntries)):
|
||||
self.storeConditionalGet(key: ConditionalGetKeys.unreadEntries, headers: response.allHeaderFields)
|
||||
completion(.success(unreadEntries))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func createUnreadEntries(entries: [Int], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("unread_entries.json")
|
||||
let request = URLRequest(url: callURL, credentials: credentials)
|
||||
let payload = GoogleReaderCompatibleUnreadEntry(unreadEntries: entries)
|
||||
transport.send(request: request, method: HTTPMethod.post, payload: payload, completion: completion)
|
||||
}
|
||||
|
||||
func deleteUnreadEntries(entries: [Int], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("unread_entries.json")
|
||||
let request = URLRequest(url: callURL, credentials: credentials)
|
||||
let payload = GoogleReaderCompatibleUnreadEntry(unreadEntries: entries)
|
||||
transport.send(request: request, method: HTTPMethod.delete, payload: payload, completion: completion)
|
||||
}
|
||||
|
||||
func retrieveStarredEntries(completion: @escaping (Result<[Int]?, Error>) -> Void) {
|
||||
|
||||
let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("starred_entries.json")
|
||||
let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.starredEntries]
|
||||
let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet)
|
||||
|
||||
transport.send(request: request, resultType: [Int].self) { result in
|
||||
|
||||
switch result {
|
||||
case .success(let (response, starredEntries)):
|
||||
self.storeConditionalGet(key: ConditionalGetKeys.starredEntries, headers: response.allHeaderFields)
|
||||
completion(.success(starredEntries))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func createStarredEntries(entries: [Int], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("starred_entries.json")
|
||||
let request = URLRequest(url: callURL, credentials: credentials)
|
||||
let payload = GoogleReaderCompatibleStarredEntry(starredEntries: entries)
|
||||
transport.send(request: request, method: HTTPMethod.post, payload: payload, completion: completion)
|
||||
}
|
||||
|
||||
func deleteStarredEntries(entries: [Int], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("starred_entries.json")
|
||||
let request = URLRequest(url: callURL, credentials: credentials)
|
||||
let payload = GoogleReaderCompatibleStarredEntry(starredEntries: entries)
|
||||
transport.send(request: request, method: HTTPMethod.delete, payload: payload, completion: completion)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
extension GoogleReaderCompatibleAPICaller {
|
||||
|
||||
func storeConditionalGet(key: String, headers: [AnyHashable : Any]) {
|
||||
if var conditionalGet = accountMetadata?.conditionalGetInfo {
|
||||
conditionalGet[key] = HTTPConditionalGetInfo(headers: headers)
|
||||
accountMetadata?.conditionalGetInfo = conditionalGet
|
||||
}
|
||||
}
|
||||
|
||||
func extractPageNumber(link: String?) -> Int? {
|
||||
|
||||
guard let link = link else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let lowerBound = link.range(of: "page=")?.upperBound {
|
||||
if let upperBound = link.range(of: "&")?.lowerBound {
|
||||
return Int(link[lowerBound..<upperBound])
|
||||
}
|
||||
if let upperBound = link.range(of: ">")?.lowerBound {
|
||||
return Int(link[lowerBound..<upperBound])
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// GoogleReaderCompatibleDate.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 5/12/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct GoogleReaderCompatibleDate {
|
||||
|
||||
public static var formatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"
|
||||
formatter.locale = Locale(identifier: "en_US")
|
||||
formatter.timeZone = TimeZone(abbreviation: "GMT")
|
||||
return formatter
|
||||
}()
|
||||
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
//
|
||||
// GoogleReaderCompatibleArticle.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Brent Simmons on 12/11/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RSParser
|
||||
import RSCore
|
||||
|
||||
struct GoogleReaderCompatibleEntry: Codable {
|
||||
|
||||
let articleID: Int
|
||||
let feedID: Int
|
||||
let title: String?
|
||||
let url: String?
|
||||
let authorName: String?
|
||||
let contentHTML: String?
|
||||
let summary: String?
|
||||
let datePublished: String?
|
||||
let dateArrived: String?
|
||||
let jsonFeed: GoogleReaderCompatibleEntryJSONFeed?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case articleID = "id"
|
||||
case feedID = "feed_id"
|
||||
case title = "title"
|
||||
case url = "url"
|
||||
case authorName = "author"
|
||||
case contentHTML = "content"
|
||||
case summary = "summary"
|
||||
case datePublished = "published"
|
||||
case dateArrived = "created_at"
|
||||
case jsonFeed = "json_feed"
|
||||
}
|
||||
|
||||
// GoogleReaderCompatible dates can't be decoded by the JSONDecoding 8601 decoding strategy. GoogleReaderCompatible
|
||||
// requires a very specific date formatter to work and even then it fails occasionally.
|
||||
// Rather than loose all the entries we only lose the one date by decoding as a string
|
||||
// and letting the one date fail when parsed.
|
||||
func parseDatePublished() -> Date? {
|
||||
if datePublished != nil {
|
||||
return GoogleReaderCompatibleDate.formatter.date(from: datePublished!)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct GoogleReaderCompatibleEntryJSONFeed: Codable {
|
||||
let jsonFeedAuthor: GoogleReaderCompatibleEntryJSONFeedAuthor?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case jsonFeedAuthor = "author"
|
||||
}
|
||||
}
|
||||
|
||||
struct GoogleReaderCompatibleEntryJSONFeedAuthor: Codable {
|
||||
let url: String?
|
||||
let avatarURL: String?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case url = "url"
|
||||
case avatarURL = "avatar"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// GoogleReaderCompatibleIcon.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 5/6/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct GoogleReaderCompatibleIcon: Codable {
|
||||
|
||||
let host: String
|
||||
let url: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case host
|
||||
case url
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// GoogleReaderCompatibleImportResult.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 5/17/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct GoogleReaderCompatibleImportResult: Codable {
|
||||
|
||||
let importResultID: Int
|
||||
let complete: Bool
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case importResultID = "id"
|
||||
case complete
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// GoogleReaderCompatibleStarredEntry.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 5/15/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct GoogleReaderCompatibleStarredEntry: Codable {
|
||||
|
||||
let starredEntries: [Int]
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case starredEntries = "starred_entries"
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
//
|
||||
// GoogleReaderCompatibleFeed.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Brent Simmons on 12/10/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RSCore
|
||||
import RSParser
|
||||
|
||||
struct GoogleReaderCompatibleSubscription: Codable {
|
||||
|
||||
let subscriptionID: Int
|
||||
let feedID: Int
|
||||
let name: String?
|
||||
let url: String
|
||||
let homePageURL: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case subscriptionID = "id"
|
||||
case feedID = "feed_id"
|
||||
case name = "title"
|
||||
case url = "feed_url"
|
||||
case homePageURL = "site_url"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct GoogleReaderCompatibleCreateSubscription: Codable {
|
||||
let feedURL: String
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case feedURL = "feed_url"
|
||||
}
|
||||
}
|
||||
|
||||
struct GoogleReaderCompatibleUpdateSubscription: Codable {
|
||||
let title: String
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case title
|
||||
}
|
||||
}
|
||||
|
||||
struct GoogleReaderCompatibleSubscriptionChoice: Codable {
|
||||
|
||||
let name: String?
|
||||
let url: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case name = "title"
|
||||
case url = "feed_url"
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
//
|
||||
// GoogleReaderCompatibleTag.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 5/5/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct GoogleReaderCompatibleTag: Codable {
|
||||
|
||||
let tagID: Int
|
||||
let name: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case tagID = "id"
|
||||
case name = "name"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct GoogleReaderCompatibleRenameTag: Codable {
|
||||
|
||||
let oldName: String
|
||||
let newName: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case oldName = "old_name"
|
||||
case newName = "new_name"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct GoogleReaderCompatibleDeleteTag: Codable {
|
||||
|
||||
let name: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
//
|
||||
// GoogleReaderCompatibleTagging.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Brent Simmons on 10/14/18.
|
||||
// Copyright © 2018 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct GoogleReaderCompatibleTagging: Codable {
|
||||
|
||||
let taggingID: Int
|
||||
let feedID: Int
|
||||
let name: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case taggingID = "id"
|
||||
case feedID = "feed_id"
|
||||
case name = "name"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct GoogleReaderCompatibleCreateTagging: Codable {
|
||||
|
||||
let feedID: Int
|
||||
let name: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case feedID = "feed_id"
|
||||
case name = "name"
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// GoogleReaderCompatibleUnreadEntry.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 5/15/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct GoogleReaderCompatibleUnreadEntry: Codable {
|
||||
|
||||
let unreadEntries: [Int]
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case unreadEntries = "unread_entries"
|
||||
}
|
||||
|
||||
}
|
|
@ -39,7 +39,7 @@ class AccountsAddViewController: NSViewController {
|
|||
extension AccountsAddViewController: NSTableViewDataSource {
|
||||
|
||||
func numberOfRows(in tableView: NSTableView) -> Int {
|
||||
return 2
|
||||
return 3
|
||||
}
|
||||
|
||||
func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
|
||||
|
@ -63,6 +63,9 @@ extension AccountsAddViewController: NSTableViewDelegate {
|
|||
case 1:
|
||||
cell.accountNameLabel?.stringValue = NSLocalizedString("Feedbin", comment: "Feedbin")
|
||||
cell.accountImageView?.image = AppAssets.accountFeedbin
|
||||
case 2:
|
||||
cell.accountNameLabel?.stringValue = NSLocalizedString("Google Reader API", comment: "Google Reader API")
|
||||
cell.accountImageView?.image = AppAssets.accountLocal
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
@ -87,6 +90,10 @@ extension AccountsAddViewController: NSTableViewDelegate {
|
|||
let accountsFeedbinWindowController = AccountsFeedbinWindowController()
|
||||
accountsFeedbinWindowController.runSheetOnWindow(self.view.window!)
|
||||
accountsAddWindowController = accountsFeedbinWindowController
|
||||
case 2:
|
||||
let accountsGoogleReaderCompatibleWindowController = AccountsGoogleReaderCompatibleWindowController()
|
||||
accountsGoogleReaderCompatibleWindowController.runSheetOnWindow(self.view.window!)
|
||||
accountsAddWindowController = accountsGoogleReaderCompatibleWindowController
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
|
@ -0,0 +1,210 @@
|
|||
<?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="AccountsGoogleReaderCompatibleWindowController" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="actionButton" destination="9mz-D9-krh" id="ozu-6Q-9Lb"/>
|
||||
<outlet property="apiURLTextField" destination="d7d-ZV-CcZ" id="Af4-uM-Dgd"/>
|
||||
<outlet property="errorMessageLabel" destination="byK-Sd-r7F" id="8zt-9d-dWQ"/>
|
||||
<outlet property="passwordTextField" destination="JSa-LY-zNQ" id="E9W-0F-69m"/>
|
||||
<outlet property="progressIndicator" destination="B0W-bh-Evv" id="Tiq-gx-s3F"/>
|
||||
<outlet property="usernameTextField" destination="78p-Cf-f55" id="RWd-0q-oAL"/>
|
||||
<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="2560" height="1417"/>
|
||||
<view key="contentView" id="se5-gp-TjO">
|
||||
<rect key="frame" x="0.0" y="0.0" width="433" height="283"/>
|
||||
<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="60" y="224" width="314" 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="accountLocal" 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="263" height="39"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="Google Reader API" 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="131"/>
|
||||
<rows>
|
||||
<gridRow id="DRl-lC-vUc"/>
|
||||
<gridRow id="eW8-uH-txq"/>
|
||||
<gridRow id="ebD-On-mOK"/>
|
||||
<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="112" 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="109" 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="78" 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="75" 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="ebD-On-mOK" column="fCQ-jY-Mts" id="xwR-xz-N6h">
|
||||
<textField key="contentView" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="H6f-t4-SMg">
|
||||
<rect key="frame" x="7" y="44" width="57" height="17"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="API URL:" id="zBm-dZ-EF1">
|
||||
<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="ebD-On-mOK" column="7CY-bX-6x4" id="Wd5-Zp-t61">
|
||||
<textField key="contentView" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="d7d-ZV-CcZ" userLabel="API URL Text Field">
|
||||
<rect key="frame" x="76" y="41" width="200" height="22"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="200" id="Mki-bb-tDu"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" drawsBackground="YES" id="0OO-BG-GXI">
|
||||
<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="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="200" 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="accountLocal" width="78" height="98"/>
|
||||
</resources>
|
||||
</document>
|
|
@ -0,0 +1,125 @@
|
|||
//
|
||||
// AccountsAddFeedbinWindowController.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Maurice Parker on 5/2/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import Account
|
||||
import RSWeb
|
||||
|
||||
class AccountsGoogleReaderCompatibleWindowController: NSWindowController {
|
||||
|
||||
@IBOutlet weak var progressIndicator: NSProgressIndicator!
|
||||
@IBOutlet weak var usernameTextField: NSTextField!
|
||||
@IBOutlet weak var apiURLTextField: 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("AccountsGoogleReaderCompatible"))
|
||||
}
|
||||
|
||||
override func windowDidLoad() {
|
||||
if let account = account, let credentials = try? account.retrieveBasicCredentials() {
|
||||
if case .basic(let username, let password) = credentials {
|
||||
usernameTextField.stringValue = username
|
||||
passwordTextField.stringValue = password
|
||||
}
|
||||
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 && !apiURLTextField.stringValue.isEmpty else {
|
||||
self.errorMessageLabel.stringValue = NSLocalizedString("Username, password & API URL are required.", comment: "Credentials Error")
|
||||
return
|
||||
}
|
||||
|
||||
actionButton.isEnabled = false
|
||||
progressIndicator.isHidden = false
|
||||
progressIndicator.startAnimation(self)
|
||||
|
||||
guard let apiURL = URL(string: apiURLTextField.stringValue) else {
|
||||
self.errorMessageLabel.stringValue = NSLocalizedString("Invalie API URL.", comment: "Credentials Error")
|
||||
return
|
||||
}
|
||||
|
||||
let credentials = Credentials.googleLogin(username: usernameTextField.stringValue, password: passwordTextField.stringValue, apiUrl: apiURL, apiKey: nil)
|
||||
Account.validateCredentials(type: .googleReaderCompatible, 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 authenticated):
|
||||
|
||||
if authenticated {
|
||||
|
||||
var newAccount = false
|
||||
if self.account == nil {
|
||||
self.account = AccountManager.shared.createAccount(type: .googleReaderCompatible)
|
||||
newAccount = true
|
||||
}
|
||||
|
||||
do {
|
||||
try self.account?.removeBasicCredentials()
|
||||
try self.account?.storeCredentials(credentials)
|
||||
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")
|
||||
}
|
||||
|
||||
} else {
|
||||
self.errorMessageLabel.stringValue = NSLocalizedString("Invalid email/password combination.", comment: "Credentials Error")
|
||||
}
|
||||
|
||||
case .failure:
|
||||
|
||||
self.errorMessageLabel.stringValue = NSLocalizedString("Network error. Try again later.", comment: "Credentials Error")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -107,6 +107,8 @@ extension AccountsPreferencesViewController: NSTableViewDelegate {
|
|||
cell.imageView?.image = AppAssets.accountLocal
|
||||
case .feedbin:
|
||||
cell.imageView?.image = NSImage(named: "accountFeedbin")
|
||||
case .googleReaderCompatible:
|
||||
cell.imageView?.image = AppAssets.accountLocal
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
|
@ -138,6 +138,8 @@ class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
|
|||
osType = "FWrg"
|
||||
case .newsBlur:
|
||||
osType = "NBlr"
|
||||
case .googleReaderCompatible:
|
||||
osType = "Grdr"
|
||||
}
|
||||
return osType.fourCharCode()
|
||||
}
|
||||
|
|
|
@ -146,6 +146,8 @@
|
|||
51F85BF92274AA7B00C787DC /* UIBarButtonItem-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BF82274AA7B00C787DC /* UIBarButtonItem-Extensions.swift */; };
|
||||
51F85BFB2275D85000C787DC /* Array-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BFA2275D85000C787DC /* Array-Extensions.swift */; };
|
||||
51F85BFD2275DCA800C787DC /* SingleLineUILabelSizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BFC2275DCA800C787DC /* SingleLineUILabelSizer.swift */; };
|
||||
55E15BCB229D65A900D6602A /* AccountsGoogleReaderCompatible.xib in Resources */ = {isa = PBXBuildFile; fileRef = 55E15BC1229D65A900D6602A /* AccountsGoogleReaderCompatible.xib */; };
|
||||
55E15BCC229D65A900D6602A /* AccountsGoogleReaderCompatibleWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E15BCA229D65A900D6602A /* AccountsGoogleReaderCompatibleWindowController.swift */; };
|
||||
6581C73820CED60100F4AD34 /* SafariExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6581C73720CED60100F4AD34 /* SafariExtensionHandler.swift */; };
|
||||
6581C73A20CED60100F4AD34 /* SafariExtensionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6581C73920CED60100F4AD34 /* SafariExtensionViewController.swift */; };
|
||||
6581C73D20CED60100F4AD34 /* SafariExtensionViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6581C73B20CED60100F4AD34 /* SafariExtensionViewController.xib */; };
|
||||
|
@ -739,6 +741,8 @@
|
|||
51F85BF82274AA7B00C787DC /* UIBarButtonItem-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem-Extensions.swift"; sourceTree = "<group>"; };
|
||||
51F85BFA2275D85000C787DC /* Array-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array-Extensions.swift"; sourceTree = "<group>"; };
|
||||
51F85BFC2275DCA800C787DC /* SingleLineUILabelSizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleLineUILabelSizer.swift; sourceTree = "<group>"; };
|
||||
55E15BC1229D65A900D6602A /* AccountsGoogleReaderCompatible.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountsGoogleReaderCompatible.xib; sourceTree = "<group>"; };
|
||||
55E15BCA229D65A900D6602A /* AccountsGoogleReaderCompatibleWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsGoogleReaderCompatibleWindowController.swift; sourceTree = "<group>"; };
|
||||
6581C73320CED60000F4AD34 /* Subscribe to Feed.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Subscribe to Feed.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
6581C73420CED60100F4AD34 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; };
|
||||
6581C73720CED60100F4AD34 /* SafariExtensionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariExtensionHandler.swift; sourceTree = "<group>"; };
|
||||
|
@ -1624,6 +1628,8 @@
|
|||
84C9FC6F22629E1200D921D6 /* Accounts */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
55E15BC1229D65A900D6602A /* AccountsGoogleReaderCompatible.xib */,
|
||||
55E15BCA229D65A900D6602A /* AccountsGoogleReaderCompatibleWindowController.swift */,
|
||||
84C9FC7022629E1200D921D6 /* AccountsTableViewBackgroundView.swift */,
|
||||
84C9FC7122629E1200D921D6 /* AccountsControlsBackgroundView.swift */,
|
||||
84C9FC7222629E1200D921D6 /* AccountsPreferencesViewController.swift */,
|
||||
|
@ -1942,12 +1948,12 @@
|
|||
ORGANIZATIONNAME = "Ranchero Software";
|
||||
TargetAttributes = {
|
||||
6581C73220CED60000F4AD34 = {
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
ProvisioningStyle = Manual;
|
||||
DevelopmentTeam = 96VR936H35;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
840D617B2029031C009BC708 = {
|
||||
CreatedOnToolsVersion = 9.3;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
DevelopmentTeam = 96VR936H35;
|
||||
ProvisioningStyle = Automatic;
|
||||
SystemCapabilities = {
|
||||
com.apple.BackgroundModes = {
|
||||
|
@ -1963,8 +1969,8 @@
|
|||
};
|
||||
849C645F1ED37A5D003D8FC0 = {
|
||||
CreatedOnToolsVersion = 8.2.1;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
ProvisioningStyle = Manual;
|
||||
DevelopmentTeam = 96VR936H35;
|
||||
ProvisioningStyle = Automatic;
|
||||
SystemCapabilities = {
|
||||
com.apple.HardenedRuntime = {
|
||||
enabled = 1;
|
||||
|
@ -1973,7 +1979,7 @@
|
|||
};
|
||||
849C64701ED37A5D003D8FC0 = {
|
||||
CreatedOnToolsVersion = 8.2.1;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
DevelopmentTeam = 96VR936H35;
|
||||
ProvisioningStyle = Automatic;
|
||||
TestTargetID = 849C645F1ED37A5D003D8FC0;
|
||||
};
|
||||
|
@ -2249,6 +2255,7 @@
|
|||
5144EA52227B8E4500D19003 /* AccountsFeedbin.xib in Resources */,
|
||||
8405DDA222168920008CE1BF /* TimelineTableView.xib in Resources */,
|
||||
8483630E2262A3FE00DA1D35 /* MainWindow.storyboard in Resources */,
|
||||
55E15BCB229D65A900D6602A /* AccountsGoogleReaderCompatible.xib in Resources */,
|
||||
84BAE64921CEDAF20046DB56 /* CrashReporterWindow.xib in Resources */,
|
||||
84C9FC8E22629E8F00D921D6 /* Credits.rtf in Resources */,
|
||||
84BBB12D20142A4700F054F5 /* Inspector.storyboard in Resources */,
|
||||
|
@ -2467,6 +2474,7 @@
|
|||
8477ACBE22238E9500DF7F37 /* SearchFeedDelegate.swift in Sources */,
|
||||
51E3EB33229AB02C00645299 /* ErrorHandler.swift in Sources */,
|
||||
8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */,
|
||||
55E15BCC229D65A900D6602A /* AccountsGoogleReaderCompatibleWindowController.swift in Sources */,
|
||||
5144EA382279FC6200D19003 /* AccountsAddLocalWindowController.swift in Sources */,
|
||||
84AD1EAA2031617300BC20B7 /* FolderPasteboardWriter.swift in Sources */,
|
||||
5144EA51227B8E4500D19003 /* AccountsFeedbinWindowController.swift in Sources */,
|
||||
|
|
Loading…
Reference in New Issue