This commit is contained in:
Brent Simmons 2019-11-13 21:59:45 -08:00
commit b763a4bb52
52 changed files with 619 additions and 770 deletions

View File

@ -49,4 +49,14 @@ jobs:
SCHEME: ${{ matrix.run-config['scheme'] }}
DESTINATION: ${{ matrix.run-config['destination'] }}
run: buildscripts/ci-build.sh
run: buildscripts/ci-build.sh
- name: Notify Slack
uses: 8398a7/action-slack@v2.4.2
with:
status: ${{ job.status }}
author_name: GitHub Actions CI
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
if: failure()

View File

@ -308,19 +308,16 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
}
}
public static func oauthAuthorizationClient(for type: AccountType) -> OAuthAuthorizationClient {
let grantingType: OAuthAuthorizationGranting.Type
internal static func oauthAuthorizationClient(for type: AccountType) -> OAuthAuthorizationClient {
switch type {
case .feedly:
grantingType = FeedlyAccountDelegate.self
return FeedlyAccountDelegate.environment.oauthAuthorizationClient
default:
fatalError("\(type) does not support OAuth authorization code granting.")
fatalError("\(type) is not a client for OAuth authorization code granting.")
}
return grantingType.oauthAuthorizationClient
}
public static func oauthAuthorizationCodeGrantRequest(for type: AccountType, client: OAuthAuthorizationClient) -> URLRequest {
public static func oauthAuthorizationCodeGrantRequest(for type: AccountType) -> URLRequest {
let grantingType: OAuthAuthorizationGranting.Type
switch type {
case .feedly:
@ -329,7 +326,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
fatalError("\(type) does not support OAuth authorization code granting.")
}
return grantingType.oauthAuthorizationCodeGrantRequest(for: client)
return grantingType.oauthAuthorizationCodeGrantRequest()
}
public static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse,
@ -346,7 +343,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
fatalError("\(accountType) does not support OAuth authorization code granting.")
}
grantingType.requestOAuthAccessToken(with: response, client: client, transport: transport, completionHandler: completionHandler)
grantingType.requestOAuthAccessToken(with: response, transport: transport, completionHandler: completionHandler)
}
public func refreshAll(completion: @escaping (Result<Void, Error>) -> Void) {

View File

@ -120,6 +120,8 @@
9E85C8E82366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E85C8E72366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift */; };
9E85C8EB236700E600D0F1F7 /* FeedlyGetEntriesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E85C8E9236700AD00D0F1F7 /* FeedlyGetEntriesOperation.swift */; };
9E85C8ED2367020700D0F1F7 /* FeedlyGetEntriesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E85C8EC2367020700D0F1F7 /* FeedlyGetEntriesService.swift */; };
9E964EB823754AC400A7AF2E /* OAuthAuthorizationClient+Feedly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E964E9E23754AC400A7AF2E /* OAuthAuthorizationClient+Feedly.swift */; };
9E964EBA23754B4000A7AF2E /* OAuthAccountAuthorizationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E964EB923754B4000A7AF2E /* OAuthAccountAuthorizationOperation.swift */; };
9EA3133B231E368100268BA0 /* FeedlyAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA3133A231E368100268BA0 /* FeedlyAccountDelegate.swift */; };
9EAEC60C2332FE830085D7C9 /* FeedlyCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC60B2332FE830085D7C9 /* FeedlyCollection.swift */; };
9EAEC60E2332FEC20085D7C9 /* FeedlyFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC60D2332FEC20085D7C9 /* FeedlyFeed.swift */; };
@ -320,6 +322,8 @@
9E85C8E72366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGetPagedStreamContentsService.swift; sourceTree = "<group>"; };
9E85C8E9236700AD00D0F1F7 /* FeedlyGetEntriesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetEntriesOperation.swift; sourceTree = "<group>"; };
9E85C8EC2367020700D0F1F7 /* FeedlyGetEntriesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetEntriesService.swift; sourceTree = "<group>"; };
9E964E9E23754AC400A7AF2E /* OAuthAuthorizationClient+Feedly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OAuthAuthorizationClient+Feedly.swift"; sourceTree = "<group>"; };
9E964EB923754B4000A7AF2E /* OAuthAccountAuthorizationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthAccountAuthorizationOperation.swift; sourceTree = "<group>"; };
9EA3133A231E368100268BA0 /* FeedlyAccountDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedlyAccountDelegate.swift; sourceTree = "<group>"; };
9EAEC60B2332FE830085D7C9 /* FeedlyCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCollection.swift; sourceTree = "<group>"; };
9EAEC60D2332FEC20085D7C9 /* FeedlyFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyFeed.swift; sourceTree = "<group>"; };
@ -616,7 +620,9 @@
9EE4CCF9234F106600FBAE4B /* FeedlyFeedContainerValidator.swift */,
9ECC9A84234DC16E009B5144 /* FeedlyAccountDelegateError.swift */,
9EC688EB232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift */,
9E964E9E23754AC400A7AF2E /* OAuthAuthorizationClient+Feedly.swift */,
9EC688ED232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift */,
9E964EB923754B4000A7AF2E /* OAuthAccountAuthorizationOperation.swift */,
9E672395236F7E68000BE141 /* OAuthAcessTokenRefreshing.swift */,
9EC688E9232B973C00A8D0A2 /* FeedlyAPICaller.swift */,
9E510D6D234F16A8002E6F1A /* FeedlyAddFeedRequest.swift */,
@ -716,7 +722,9 @@
isa = PBXNativeTarget;
buildConfigurationList = 8489350A1F62485000CEBD24 /* Build configuration list for PBXNativeTarget "Account" */;
buildPhases = (
9E964EBB2375512300A7AF2E /* Run Script: Update OAuthAuthorizationClient+Feedly.swift */,
848934F11F62484F00CEBD24 /* Sources */,
9E964EBC2375517100A7AF2E /* Run Script: Reset OAuthAuthorizationClient+Feedly.swift */,
848934F21F62484F00CEBD24 /* Frameworks */,
848934F31F62484F00CEBD24 /* Headers */,
848934F41F62484F00CEBD24 /* Resources */,
@ -889,6 +897,42 @@
shellPath = /bin/sh;
shellScript = "xcrun -sdk macosx swiftc -target x86_64-macosx10.11 ../../buildscripts/VerifyNoBuildSettings.swift -o $CONFIGURATION_TEMP_DIR/VerifyNoBS\n$CONFIGURATION_TEMP_DIR/VerifyNoBS ${PROJECT_NAME}.xcodeproj/project.pbxproj\n";
};
9E964EBB2375512300A7AF2E /* Run Script: Update OAuthAuthorizationClient+Feedly.swift */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Run Script: Update OAuthAuthorizationClient+Feedly.swift";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "FAILED=false\n\nif [ -z \"${FEEDLY_CLIENT_ID}\" ]; then\necho \"Missing Feedly Client ID\"\nFAILED=true\nfi\n\nif [ -z \"${FEEDLY_CLIENT_SECRET}\" ]; then\necho \"Missing Feedly Client Secret\"\nFAILED=true\nfi\n\nFEEDLY_CLIENT_SOURCE=\"${SRCROOT}/Feedly/OAuthAuthorizationClient+Feedly.swift\"\n\nif [ \"$FAILED\" = true ]; then\necho \"Missing Feedly client ID or secret. ${FEEDLY_CLIENT_SOURCE} not changed.\"\nexit 0\nfi\n\n# echo \"Substituting variables in: ${FEEDLY_CLIENT_SOURCE}\"\n\nif [ -e \"${FEEDLY_CLIENT_SOURCE}\" ]\nthen\n sed -i .tmp \"s|{FEEDLY_CLIENT_ID}|${FEEDLY_CLIENT_ID}|g; s|{FEEDLY_CLIENT_SECRET}|${FEEDLY_CLIENT_SECRET}|g\" $FEEDLY_CLIENT_SOURCE\n # echo \"`git diff ${FEEDLY_CLIENT_SOURCE}`\"\n rm -f \"${FEEDLY_CLIENT_SOURCE}.tmp\"\nelse\n echo \"File does not exist at ${FEEDLY_CLIENT_SOURCE}. Has it been moved or renamed?\"\n exit -1\nfi\n\necho \"All env values found!\"\n";
};
9E964EBC2375517100A7AF2E /* Run Script: Reset OAuthAuthorizationClient+Feedly.swift */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Run Script: Reset OAuthAuthorizationClient+Feedly.swift";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "git checkout \"${SRCROOT}/Feedly/OAuthAuthorizationClient+Feedly.swift\"\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@ -932,6 +976,7 @@
9E85C8EB236700E600D0F1F7 /* FeedlyGetEntriesOperation.swift in Sources */,
9E1D154D233370D800F4944C /* FeedlySyncAllOperation.swift in Sources */,
844B297D2106C7EC004020B3 /* Feed.swift in Sources */,
9E964EBA23754B4000A7AF2E /* OAuthAccountAuthorizationOperation.swift in Sources */,
9E1D15572334355900F4944C /* FeedlyRequestStreamsOperation.swift in Sources */,
9E1D15512334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift in Sources */,
9E1773D7234575AB0056A5A8 /* FeedlyTag.swift in Sources */,
@ -972,6 +1017,7 @@
9EC688EA232B973C00A8D0A2 /* FeedlyAPICaller.swift in Sources */,
9E1773D32345700F0056A5A8 /* FeedlyLink.swift in Sources */,
9EAEC62823331C350085D7C9 /* FeedlyCategory.swift in Sources */,
9E964EB823754AC400A7AF2E /* OAuthAuthorizationClient+Feedly.swift in Sources */,
9EF1B10923590E93000A486A /* FeedlyStreamIds.swift in Sources */,
84D09623217418DC00D77525 /* FeedbinTagging.swift in Sources */,
84CAD7161FDF2E22000F0755 /* FeedbinEntry.swift in Sources */,

View File

@ -130,7 +130,7 @@ class FeedlySyncAllOperationTests: XCTestCase {
OperationQueue.main.addOperation(syncAll)
waitForExpectations(timeout: 2)
waitForExpectations(timeout: 5)
}
func performInitialSync() {

View File

@ -20,11 +20,12 @@ public struct CredentialsManager {
}()
public static func storeCredentials(_ credentials: Credentials, server: String) throws {
var query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
kSecAttrAccount as String: credentials.username,
kSecAttrServer as String: server]
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
kSecAttrAccount as String: credentials.username,
kSecAttrServer as String: server]
if credentials.type != .basic {
query[kSecAttrSecurityDomain as String] = credentials.type.rawValue
}
@ -32,26 +33,25 @@ public struct CredentialsManager {
if let securityGroup = keychainGroup {
query[kSecAttrAccessGroup as String] = securityGroup
}
let secretData = credentials.secret.data(using: String.Encoding.utf8)!
let attributes: [String: Any] = [kSecValueData as String: secretData]
let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
query[kSecValueData as String] = secretData
let status = SecItemAdd(query as CFDictionary, nil)
switch status {
case errSecSuccess:
return
case errSecItemNotFound:
case errSecDuplicateItem:
break
default:
throw CredentialsError.unhandledError(status: status)
}
guard status == errSecItemNotFound else {
return
}
var deleteQuery = query
deleteQuery.removeValue(forKey: kSecAttrAccessible as String)
SecItemDelete(deleteQuery as CFDictionary)
query[kSecValueData as String] = secretData
let addStatus = SecItemAdd(query as CFDictionary, nil)
if addStatus != errSecSuccess {
throw CredentialsError.unhandledError(status: status)

View File

@ -179,7 +179,8 @@ public final class Feed: DisplayNameProvider, Renamable, UnreadCountProvider, De
account.renameFeed(self, to: newName, completion: completion)
}
// MARK: - PathIDUserInfoProvider
// MARK: - DeepLinkProvider
public var deepLinkUserInfo: [AnyHashable : Any] {
return [
DeepLinkKey.accountID.rawValue: account?.accountID ?? "",

View File

@ -78,7 +78,6 @@ final class FeedbinAccountDelegate: AccountDelegate {
}
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
retrieveCredentialsIfNecessary(account)
refreshProgress.addToNumberOfTasksAndRemaining(5)
@ -112,7 +111,6 @@ final class FeedbinAccountDelegate: AccountDelegate {
}
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
retrieveCredentialsIfNecessary(account)
os_log(.debug, log: log, "Sending article statuses...")
@ -169,7 +167,6 @@ final class FeedbinAccountDelegate: AccountDelegate {
}
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
retrieveCredentialsIfNecessary(account)
os_log(.debug, log: log, "Refreshing article statuses...")
@ -216,7 +213,6 @@ final class FeedbinAccountDelegate: AccountDelegate {
}
func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void) {
retrieveCredentialsIfNecessary(account)
var fileData: Data?
@ -263,7 +259,6 @@ final class FeedbinAccountDelegate: AccountDelegate {
}
func addFolder(for account: Account, name: String, completion: @escaping (Result<Folder, Error>) -> Void) {
retrieveCredentialsIfNecessary(account)
if let folder = account.ensureFolder(with: name) {
completion(.success(folder))
} else {
@ -272,7 +267,6 @@ final class FeedbinAccountDelegate: AccountDelegate {
}
func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
retrieveCredentialsIfNecessary(account)
guard folder.hasAtLeastOneFeed() else {
folder.name = name
@ -300,7 +294,6 @@ final class FeedbinAccountDelegate: AccountDelegate {
}
func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
retrieveCredentialsIfNecessary(account)
// Feedbin uses tags and if at least one feed isn't tagged, then the folder doesn't exist on their system
guard folder.hasAtLeastOneFeed() else {
@ -364,7 +357,6 @@ final class FeedbinAccountDelegate: AccountDelegate {
}
func createFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
retrieveCredentialsIfNecessary(account)
refreshProgress.addToNumberOfTasksAndRemaining(1)
caller.createSubscription(url: url) { result in
@ -397,7 +389,6 @@ final class FeedbinAccountDelegate: AccountDelegate {
}
func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
retrieveCredentialsIfNecessary(account)
// This error should never happen
guard let subscriptionID = feed.subscriptionID else {
@ -433,7 +424,6 @@ final class FeedbinAccountDelegate: AccountDelegate {
}
func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
retrieveCredentialsIfNecessary(account)
if from is Account {
addFeed(for: account, with: feed, to: to, completion: completion)
} else {
@ -449,7 +439,6 @@ final class FeedbinAccountDelegate: AccountDelegate {
}
func addFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
retrieveCredentialsIfNecessary(account)
if let folder = container as? Folder, let feedID = Int(feed.feedID) {
refreshProgress.addToNumberOfTasksAndRemaining(1)
@ -482,7 +471,6 @@ final class FeedbinAccountDelegate: AccountDelegate {
}
func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
retrieveCredentialsIfNecessary(account)
if let existingFeed = account.existingFeed(withURL: feed.url) {
account.addFeed(existingFeed, to: container) { result in
@ -507,7 +495,6 @@ final class FeedbinAccountDelegate: AccountDelegate {
}
func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
retrieveCredentialsIfNecessary(account)
let group = DispatchGroup()
@ -536,7 +523,6 @@ final class FeedbinAccountDelegate: AccountDelegate {
}
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
retrieveCredentialsIfNecessary(account)
let syncStatuses = articles.map { article in
return SyncStatus(articleID: article.articleID, key: statusKey, flag: flag)
@ -1308,11 +1294,5 @@ private extension FeedbinAccountDelegate {
}
}
func retrieveCredentialsIfNecessary(_ account: Account) {
if credentials == nil {
credentials = try? account.retrieveCredentials(type: .basic)
}
}
}

View File

@ -31,20 +31,10 @@ final class FeedlyAPICaller {
var oauthAuthorizationClient: OAuthAuthorizationClient {
switch self {
case .cloud:
/// Models private NetNewsWire client secrets.
/// https://developer.feedly.com/v3/auth/#authenticating-a-user-and-obtaining-an-auth-code
return OAuthAuthorizationClient(id: "{FEEDLY-ID}",
redirectUri: "{FEEDLY-REDIRECT-URI}",
state: nil,
secret: "{FEEDLY-SECRET}")
case .sandbox:
/// Models public sandbox API values found at:
/// https://groups.google.com/forum/#!topic/feedly-cloud/WwQWMgDmOuw
return OAuthAuthorizationClient(id: "sandbox",
redirectUri: "http://localhost",
state: nil,
secret: "ReVGXA6WekanCxbf")
return .feedlySandboxClient
case .cloud:
return .feedlyCloudClient
}
}
}

View File

@ -27,11 +27,8 @@ extension FeedlyAccountDelegate: OAuthAuthorizationGranting {
private static let oauthAuthorizationGrantScope = "https://cloud.feedly.com/subscriptions"
static var oauthAuthorizationClient: OAuthAuthorizationClient {
return environment.oauthAuthorizationClient
}
static func oauthAuthorizationCodeGrantRequest(for client: OAuthAuthorizationClient) -> URLRequest {
static func oauthAuthorizationCodeGrantRequest() -> URLRequest {
let client = environment.oauthAuthorizationClient
let authorizationRequest = OAuthAuthorizationRequest(clientId: client.id,
redirectUri: client.redirectUri,
scope: oauthAuthorizationGrantScope,
@ -40,7 +37,8 @@ extension FeedlyAccountDelegate: OAuthAuthorizationGranting {
return FeedlyAPICaller.authorizationCodeUrlRequest(for: authorizationRequest, baseUrlComponents: baseURLComponents)
}
static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, client: OAuthAuthorizationClient, transport: Transport, completionHandler: @escaping (Result<OAuthAuthorizationGrant, Error>) -> ()) {
static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, transport: Transport, completionHandler: @escaping (Result<OAuthAuthorizationGrant, Error>) -> ()) {
let client = environment.oauthAuthorizationClient
let request = OAuthAccessTokenRequest(authorizationResponse: response,
scope: oauthAuthorizationGrantScope,
client: client)

View File

@ -17,17 +17,10 @@ final class FeedlyAccountDelegate: AccountDelegate {
/// Feedly has a sandbox API and a production API.
/// This property is referred to when clients need to know which environment it should be pointing to.
/// The value of this proptery must match any `OAuthAuthorizationClient` used.
/// Currently this is always returning the cloud API, but we are leaving it stubbed out for now.
static var environment: FeedlyAPICaller.API {
#if DEBUG
// https://developer.feedly.com/v3/developer/
if let token = ProcessInfo.processInfo.environment["FEEDLY_DEV_ACCESS_TOKEN"], !token.isEmpty {
return .cloud
}
return .sandbox
#else
return .cloud
#endif
}
// TODO: Kiel, if you decide not to support OPML import you will have to disallow it in the behaviors
@ -53,6 +46,8 @@ final class FeedlyAccountDelegate: AccountDelegate {
}
}
let oauthAuthorizationClient: OAuthAuthorizationClient
var accountMetadata: AccountMetadata?
var refreshProgress = DownloadProgress(numberOfTasks: 0)
@ -97,6 +92,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
let databaseFilePath = (dataFolder as NSString).appendingPathComponent("Sync.sqlite3")
self.database = SyncDatabase(databaseFilePath: databaseFilePath)
self.oauthAuthorizationClient = api.oauthAuthorizationClient
}
// MARK: Account API
@ -128,7 +124,9 @@ final class FeedlyAccountDelegate: AccountDelegate {
let date = Date()
operation.syncCompletionHandler = { [weak self] result in
self?.accountMetadata?.lastArticleFetch = date
if case .success = result {
self?.accountMetadata?.lastArticleFetch = date
}
os_log(.debug, log: log, "Sync took %{public}.3f seconds", -date.timeIntervalSinceNow)
progress.completeTask()
@ -484,8 +482,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
func accountDidInitialize(_ account: Account) {
credentials = try? account.retrieveCredentials(type: .oauthAccessToken)
let client = FeedlyAccountDelegate.oauthAuthorizationClient
let refreshAccessToken = FeedlyRefreshAccessTokenOperation(account: account, service: self, oauthClient: client, log: log)
let refreshAccessToken = FeedlyRefreshAccessTokenOperation(account: account, service: self, oauthClient: oauthAuthorizationClient, log: log)
operationQueue.addOperation(refreshAccessToken)
}

View File

@ -0,0 +1,187 @@
//
// OAuthAccountAuthorizationOperation.swift
// NetNewsWire
//
// Created by Kiel Gillard on 8/11/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
import AuthenticationServices
public protocol OAuthAccountAuthorizationOperationDelegate: class {
func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didCreate account: Account)
func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didFailWith error: Error)
}
public final class OAuthAccountAuthorizationOperation: Operation, ASWebAuthenticationPresentationContextProviding {
public weak var presentationAnchor: ASPresentationAnchor?
public weak var delegate: OAuthAccountAuthorizationOperationDelegate?
private let accountType: AccountType
private let oauthClient: OAuthAuthorizationClient
private var session: ASWebAuthenticationSession?
public init(accountType: AccountType) {
self.accountType = accountType
self.oauthClient = Account.oauthAuthorizationClient(for: accountType)
}
override public func main() {
assert(Thread.isMainThread)
assert(presentationAnchor != nil, "\(self) outlived presentation anchor.")
guard !isCancelled else {
didFinish()
return
}
let request = Account.oauthAuthorizationCodeGrantRequest(for: accountType)
guard let url = request.url else {
return DispatchQueue.main.async {
self.didEndAuthentication(url: nil, error: URLError(.badURL))
}
}
guard let redirectUri = URL(string: oauthClient.redirectUri), let scheme = redirectUri.scheme else {
assertionFailure("Could not get callback URL scheme from \(oauthClient.redirectUri)")
return DispatchQueue.main.async {
self.didEndAuthentication(url: nil, error: URLError(.badURL))
}
}
let session = ASWebAuthenticationSession(url: url, callbackURLScheme: scheme) { url, error in
DispatchQueue.main.async { [weak self] in
self?.didEndAuthentication(url: url, error: error)
}
}
self.session = session
session.presentationContextProvider = self
session.start()
}
override public func cancel() {
session?.cancel()
super.cancel()
}
private func didEndAuthentication(url: URL?, error: Error?) {
guard !isCancelled else {
didFinish()
return
}
do {
guard let url = url else {
if let error = error {
throw error
}
throw URLError(.badURL)
}
let response = try OAuthAuthorizationResponse(url: url, client: oauthClient)
Account.requestOAuthAccessToken(with: response, client: oauthClient, accountType: accountType, completionHandler: didEndRequestingAccessToken(_:))
} catch is ASWebAuthenticationSessionError {
didFinish() // Primarily, cancellation.
} catch {
didFinish(error)
}
}
public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
guard let anchor = presentationAnchor else {
fatalError("\(self) has outlived presentation anchor.")
}
return anchor
}
private func didEndRequestingAccessToken(_ result: Result<OAuthAuthorizationGrant, Error>) {
guard !isCancelled else {
didFinish()
return
}
switch result {
case .success(let tokenResponse):
saveAccount(for: tokenResponse)
case .failure(let error):
didFinish(error)
}
}
private func saveAccount(for grant: OAuthAuthorizationGrant) {
// TODO: Find an already existing account for this username?
let account = AccountManager.shared.createAccount(type: .feedly)
do {
// Store the refresh token first because it sends this token to the account delegate.
if let token = grant.refreshToken {
try account.storeCredentials(token)
}
// Now store the access token because we want the account delegate to use it.
try account.storeCredentials(grant.accessToken)
delegate?.oauthAccountAuthorizationOperation(self, didCreate: account)
didFinish()
} catch {
didFinish(error)
}
}
// MARK: Managing Operation State
private func didFinish() {
assert(Thread.isMainThread)
assert(!isFinished, "Finished operation is attempting to finish again.")
self.isExecutingOperation = false
self.isFinishedOperation = true
}
private func didFinish(_ error: Error) {
assert(Thread.isMainThread)
assert(!isFinished, "Finished operation is attempting to finish again.")
delegate?.oauthAccountAuthorizationOperation(self, didFailWith: error)
didFinish()
}
override public func start() {
isExecutingOperation = true
DispatchQueue.main.async {
self.main()
}
}
override public var isExecuting: Bool {
return isExecutingOperation
}
private var isExecutingOperation = false {
willSet {
willChangeValue(for: \.isExecuting)
}
didSet {
didChangeValue(for: \.isExecuting)
}
}
override public var isFinished: Bool {
return isFinishedOperation
}
private var isFinishedOperation = false {
willSet {
willChangeValue(for: \.isFinished)
}
didSet {
didChangeValue(for: \.isFinished)
}
}
}

View File

@ -0,0 +1,34 @@
//
// OAuthAuthorizationClient+NetNewsWire.swift
// Account
//
// Created by Kiel Gillard on 8/11/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
extension OAuthAuthorizationClient {
static var feedlyCloudClient: OAuthAuthorizationClient {
/// Models private NetNewsWire client secrets.
/// These placeholders are substitued at build time using a Run Script phase with build settings.
/// https://developer.feedly.com/v3/auth/#authenticating-a-user-and-obtaining-an-auth-code
return OAuthAuthorizationClient(id: "{FEEDLY_CLIENT_ID}",
redirectUri: "netnewswire://auth/feedly",
state: nil,
secret: "{FEEDLY_CLIENT_SECRET}")
}
static var feedlySandboxClient: OAuthAuthorizationClient {
/// We use this funky redirect URI because ASWebAuthenticationSession will try to load http://localhost URLs.
/// See https://developer.feedly.com/v3/sandbox/ for more information.
/// The return value models public sandbox API values found at:
/// https://groups.google.com/forum/#!topic/feedly-cloud/WwQWMgDmOuw
/// They are due to expire on November 30 2019.
return OAuthAuthorizationClient(id: "sandbox",
redirectUri: "urn:ietf:wg:oauth:2.0:oob",
state: nil,
secret: "ReVGXA6WekanCxbf")
}
}

View File

@ -61,7 +61,7 @@ public struct OAuthAuthorizationResponse {
public extension OAuthAuthorizationResponse {
init(url: URL, client: OAuthAuthorizationClient) throws {
guard let host = url.host, client.redirectUri.contains(host) else {
guard let scheme = url.scheme, client.redirectUri.hasPrefix(scheme) else {
throw URLError(.unsupportedURL)
}
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
@ -165,10 +165,8 @@ public protocol OAuthAuthorizationCodeGrantRequesting {
}
protocol OAuthAuthorizationGranting: AccountDelegate {
static func oauthAuthorizationCodeGrantRequest() -> URLRequest
static var oauthAuthorizationClient: OAuthAuthorizationClient { get }
static func oauthAuthorizationCodeGrantRequest(for client: OAuthAuthorizationClient) -> URLRequest
static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, client: OAuthAuthorizationClient, transport: Transport, completionHandler: @escaping (Result<OAuthAuthorizationGrant, Error>) -> ())
static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, transport: Transport, completionHandler: @escaping (Result<OAuthAuthorizationGrant, Error>) -> ())
}

View File

@ -33,7 +33,8 @@ public final class Folder: DisplayNameProvider, Renamable, Container, UnreadCoun
return name ?? Folder.untitledName
}
// MARK: - PathIDUserInfoProvider
// MARK: - DeepLinkProvider
public var deepLinkUserInfo: [AnyHashable : Any] {
return [
DeepLinkKey.accountID.rawValue: account?.accountID ?? "",

View File

@ -7,9 +7,10 @@ PROVISIONING_PROFILE_SPECIFIER =
// DeveloperSettings.xcconfig is #included here
#include? "../../../SharedXcodeSettings/DeveloperSettings.xcconfig"
#include? "../../../SharedXcodeSettings/ProjectSettings.xcconfig"
SDKROOT = macosx
MACOSX_DEPLOYMENT_TARGET = 10.14
MACOSX_DEPLOYMENT_TARGET = 10.15
IPHONEOS_DEPLOYMENT_TARGET = 13.0
SUPPORTED_PLATFORMS = macosx iphoneos iphonesimulator
@ -18,7 +19,6 @@ SWIFT_VERSION = 5.1
COMBINE_HIDPI_IMAGES = YES
COPY_PHASE_STRIP = NO
MACOSX_DEPLOYMENT_TARGET = 10.14
ALWAYS_SEARCH_USER_PATHS = NO
CURRENT_PROJECT_VERSION = 1
VERSION_INFO_PREFIX =

View File

@ -16,12 +16,6 @@ extension NSImage.Name {
struct AppAssets {
static var genericFeedImage: IconImage? = {
let path = "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/BookmarkIcon.icns"
let image = RSImage(contentsOfFile: path)
return image != nil ? IconImage(image!) : nil
}()
static var timelineStar: RSImage! = {
return RSImage(named: .timelineStar)
}()
@ -50,6 +44,14 @@ struct AppAssets {
return RSImage(named: "articleExtractorError")
}()
static var articleExtractorInactiveDark: RSImage! = {
return RSImage(named: "articleExtractorInactiveDark")
}()
static var articleExtractorInactiveLight: RSImage! = {
return RSImage(named: "articleExtractorInactiveLight")
}()
static var articleExtractorProgress1: RSImage! = {
return RSImage(named: "articleExtractorProgress1")
}()

View File

@ -12,7 +12,7 @@ import Account
final class FeedInspectorViewController: NSViewController, Inspector {
@IBOutlet weak var imageView: NSImageView?
@IBOutlet weak var iconView: IconView!
@IBOutlet weak var nameTextField: NSTextField?
@IBOutlet weak var homePageURLTextField: NSTextField?
@IBOutlet weak var urlTextField: NSTextField?
@ -43,11 +43,7 @@ final class FeedInspectorViewController: NSViewController, Inspector {
// MARK: NSViewController
override func viewDidLoad() {
imageView!.wantsLayer = true
imageView!.layer?.cornerRadius = 4.0
updateUI()
NotificationCenter.default.addObserver(self, selector: #selector(imageDidBecomeAvailable(_:)), name: .ImageDidBecomeAvailable, object: nil)
}
@ -101,25 +97,21 @@ private extension FeedInspectorViewController {
}
func updateImage() {
guard let feed = feed else {
imageView?.image = nil
guard let feed = feed, let iconView = iconView else {
return
}
if let feedIcon = appDelegate.feedIconDownloader.icon(for: feed) {
imageView?.image = feedIcon.image
iconView.iconImage = feedIcon
return
}
if let favicon = appDelegate.faviconDownloader.favicon(for: feed)?.image {
if favicon.size.height < 16.0 && favicon.size.width < 16.0 {
favicon.size = NSSize(width: 16, height: 16)
}
imageView?.image = favicon
if let favicon = appDelegate.faviconDownloader.favicon(for: feed) {
iconView.iconImage = favicon
return
}
imageView?.image = AppAssets.genericFeedImage?.image
iconView.iconImage = feed.smallIcon
}
func updateName() {

View File

@ -1,8 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="15504" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="cfG-Pn-VJS">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="15505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="cfG-Pn-VJS">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15504"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15505"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@ -37,14 +36,6 @@
<rect key="frame" x="0.0" y="0.0" width="256" height="332"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="H9X-OG-K0p">
<rect key="frame" x="104" y="264" width="48" height="48"/>
<constraints>
<constraint firstAttribute="width" constant="48" id="1Cy-0w-dBg"/>
<constraint firstAttribute="height" constant="48" id="edb-lw-Ict"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="NSNetwork" id="MZ2-89-Bje"/>
</imageView>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="IWu-80-XC5">
<rect key="frame" x="20" y="200" width="216" height="56"/>
<constraints>
@ -114,13 +105,19 @@ Field</string>
<action selector="isNotifyAboutNewArticlesChanged:" target="sfH-oR-GXm" id="Vx9-pQ-RnP"/>
</connections>
</button>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="I6k-QR-VmV" customClass="IconView" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="104" y="264" width="48" height="48"/>
<constraints>
<constraint firstAttribute="height" constant="48" id="Faa-nE-lkA"/>
<constraint firstAttribute="width" constant="48" id="esD-dT-oWU"/>
</constraints>
</customView>
</subviews>
<constraints>
<constraint firstItem="zm0-15-BFy" firstAttribute="top" secondItem="2WO-Iu-p5e" secondAttribute="bottom" constant="4" id="2fb-QO-XIm"/>
<constraint firstItem="IWu-80-XC5" firstAttribute="top" secondItem="H9X-OG-K0p" secondAttribute="bottom" constant="8" symbolic="YES" id="4WB-WJ-3Z4"/>
<constraint firstItem="ZBX-E8-k9c" firstAttribute="top" secondItem="IWu-80-XC5" secondAttribute="bottom" constant="20" id="5L7-aZ-vdg"/>
<constraint firstItem="nH2-ab-KJ5" firstAttribute="leading" secondItem="ecA-UY-KEd" secondAttribute="leading" constant="20" symbolic="YES" id="8pK-lW-xQk"/>
<constraint firstItem="H9X-OG-K0p" firstAttribute="centerX" secondItem="ecA-UY-KEd" secondAttribute="centerX" id="9CA-KA-HEg"/>
<constraint firstItem="IWu-80-XC5" firstAttribute="top" secondItem="I6k-QR-VmV" secondAttribute="bottom" constant="8" symbolic="YES" id="Bea-j0-QMb"/>
<constraint firstItem="nH2-ab-KJ5" firstAttribute="top" secondItem="ZBX-E8-k9c" secondAttribute="bottom" constant="20" id="CpA-X9-EbP"/>
<constraint firstAttribute="bottom" secondItem="Vvk-KG-JlG" secondAttribute="bottom" constant="20" id="IxJ-5N-NhL"/>
<constraint firstAttribute="trailing" secondItem="ju6-Zo-8X4" secondAttribute="trailing" constant="20" symbolic="YES" id="Jzi-tP-TIw"/>
@ -128,11 +125,12 @@ Field</string>
<constraint firstItem="ju6-Zo-8X4" firstAttribute="leading" secondItem="ecA-UY-KEd" secondAttribute="leading" constant="20" symbolic="YES" id="NwI-2x-dAr"/>
<constraint firstItem="ju6-Zo-8X4" firstAttribute="top" secondItem="zm0-15-BFy" secondAttribute="bottom" constant="20" id="PFv-jF-JIZ"/>
<constraint firstItem="2WO-Iu-p5e" firstAttribute="leading" secondItem="ecA-UY-KEd" secondAttribute="leading" constant="20" symbolic="YES" id="PeT-mm-2HJ"/>
<constraint firstItem="I6k-QR-VmV" firstAttribute="top" secondItem="ecA-UY-KEd" secondAttribute="top" constant="20" symbolic="YES" id="URB-DN-7vz"/>
<constraint firstAttribute="trailing" secondItem="IWu-80-XC5" secondAttribute="trailing" constant="20" symbolic="YES" id="WW6-xR-Zue"/>
<constraint firstItem="H9X-OG-K0p" firstAttribute="top" secondItem="ecA-UY-KEd" secondAttribute="top" constant="20" symbolic="YES" id="Z6q-PN-wOC"/>
<constraint firstItem="zm0-15-BFy" firstAttribute="leading" secondItem="ecA-UY-KEd" secondAttribute="leading" constant="20" symbolic="YES" id="aho-BJ-kmB"/>
<constraint firstItem="ZBX-E8-k9c" firstAttribute="leading" secondItem="ecA-UY-KEd" secondAttribute="leading" constant="20" symbolic="YES" id="cjR-0i-YNG"/>
<constraint firstAttribute="trailing" secondItem="2WO-Iu-p5e" secondAttribute="trailing" constant="20" symbolic="YES" id="dLU-a6-nfx"/>
<constraint firstItem="I6k-QR-VmV" firstAttribute="centerX" secondItem="ecA-UY-KEd" secondAttribute="centerX" id="gFG-ZY-eNp"/>
<constraint firstAttribute="trailing" secondItem="zm0-15-BFy" secondAttribute="trailing" constant="20" symbolic="YES" id="js6-b2-FIR"/>
<constraint firstItem="IWu-80-XC5" firstAttribute="leading" secondItem="ecA-UY-KEd" secondAttribute="leading" constant="20" symbolic="YES" id="r6h-Z0-g7b"/>
<constraint firstItem="2WO-Iu-p5e" firstAttribute="top" secondItem="nH2-ab-KJ5" secondAttribute="bottom" constant="20" id="rRv-qO-dPa"/>
@ -142,7 +140,7 @@ Field</string>
</view>
<connections>
<outlet property="homePageURLTextField" destination="zm0-15-BFy" id="0Jh-yy-mnF"/>
<outlet property="imageView" destination="H9X-OG-K0p" id="Rm6-X6-csH"/>
<outlet property="iconView" destination="I6k-QR-VmV" id="zrk-zx-zk7"/>
<outlet property="isNotifyAboutNewArticlesCheckBox" destination="ZBX-E8-k9c" id="FWc-Ds-LUy"/>
<outlet property="isReaderViewAlwaysOnCheckBox" destination="nH2-ab-KJ5" id="xPg-P5-3cr"/>
<outlet property="nameTextField" destination="IWu-80-XC5" id="zg4-5h-hoP"/>
@ -151,7 +149,7 @@ Field</string>
</viewController>
<customObject id="1ho-ZO-Gkb" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="67" y="69.5"/>
<point key="canvasLocation" x="67" y="69"/>
</scene>
<!--Folder-->
<scene sceneID="8By-fa-WDQ">
@ -294,7 +292,6 @@ Field</string>
</scenes>
<resources>
<image name="NSFolder" width="32" height="32"/>
<image name="NSNetwork" width="32" height="32"/>
<image name="NSSmartBadgeTemplate" width="14" height="14"/>
</resources>
</document>

View File

@ -59,7 +59,15 @@ class ArticleExtractorButton: NSButton {
case isInProgress:
addAnimatedSublayer(to: hostedLayer)
default:
addImageSublayer(to: hostedLayer, image: AppAssets.articleExtractor, opacity: opacity)
if NSApplication.shared.isActive {
addImageSublayer(to: hostedLayer, image: AppAssets.articleExtractor, opacity: opacity)
} else {
if NSApplication.shared.effectiveAppearance.isDarkMode {
addImageSublayer(to: hostedLayer, image: AppAssets.articleExtractorInactiveDark, opacity: opacity)
} else {
addImageSublayer(to: hostedLayer, image: AppAssets.articleExtractorInactiveLight, opacity: opacity)
}
}
}
}

View File

@ -39,6 +39,7 @@ final class DetailWebViewController: NSViewController, WKUIDelegate {
}
#endif
private let articleIconSchemeHandler = ArticleIconSchemeHandler()
private var waitingForFirstReload = false
private let keyboardDelegate = DetailKeyboardDelegate()
@ -65,6 +66,7 @@ final class DetailWebViewController: NSViewController, WKUIDelegate {
let configuration = WKWebViewConfiguration()
configuration.preferences = preferences
configuration.setURLSchemeHandler(articleIconSchemeHandler, forURLScheme: ArticleRenderer.imageIconScheme)
let userContentController = WKUserContentController()
userContentController.add(self, name: MessageName.mouseDidEnter)
@ -185,8 +187,10 @@ private extension DetailWebViewController {
case .loading:
rendering = ArticleRenderer.loadingHTML(style: style)
case .article(let article):
articleIconSchemeHandler.currentArticle = article
rendering = ArticleRenderer.articleHTML(article: article, style: style)
case .extracted(let article, let extractedArticle):
articleIconSchemeHandler.currentArticle = article
rendering = ArticleRenderer.articleHTML(article: article, extractedArticle: extractedArticle, style: style)
}

View File

@ -8,7 +8,7 @@
import AppKit
final class TimelineIconView: NSView {
final class IconView: NSView {
var iconImage: IconImage? = nil {
didSet {
@ -71,17 +71,18 @@ final class TimelineIconView: NSView {
return
}
let color = NSApplication.shared.effectiveAppearance.isDarkMode ? TimelineIconView.darkBackgroundColor : TimelineIconView.lightBackgroundColor
let color = NSApplication.shared.effectiveAppearance.isDarkMode ? IconView.darkBackgroundColor : IconView.lightBackgroundColor
color.set()
dirtyRect.fill()
}
}
private extension TimelineIconView {
private extension IconView {
func commonInit() {
addSubview(imageView)
wantsLayer = true
layer?.cornerRadius = 4.0
}
func rectForImageView() -> NSRect {

View File

@ -41,7 +41,7 @@ import RSCore
return nil
}
let image = sendToCommand.image ?? AppAssets.genericFeedImage?.image ?? NSImage()
let image = sendToCommand.image ?? NSImage()
return NSSharingService(title: sendToCommand.title, image: image, alternateImage: nil) {
sendToCommand.sendObject(object, selectedText: nil)
}

View File

@ -81,8 +81,7 @@ class SidebarCell : NSTableCellView {
}()
private let faviconImageView: NSImageView = {
let iconImage = AppAssets.genericFeedImage
let imageView = iconImage != nil ? NSImageView(image: iconImage!.image) : NSImageView(frame: NSRect.zero)
let imageView = NSImageView(frame: NSRect.zero)
imageView.animates = false
imageView.imageAlignment = .alignCenter
imageView.imageScaling = .scaleProportionallyDown

View File

@ -18,7 +18,7 @@ class TimelineTableCellView: NSTableCellView {
private let dateView = TimelineTableCellView.singleLineTextField()
private let feedNameView = TimelineTableCellView.singleLineTextField()
private lazy var iconView = TimelineIconView()
private lazy var iconView = IconView()
private let starView = TimelineTableCellView.imageView(with: AppAssets.timelineStar, scaling: .scaleNone)
private let separatorView = TimelineTableCellView.separatorView()

View File

@ -101,9 +101,10 @@ extension AccountsAddViewController: NSTableViewDelegate {
accountsReaderAPIWindowController.runSheetOnWindow(self.view.window!)
accountsAddWindowController = accountsReaderAPIWindowController
case .feedly:
let accountsFeedlyWindowController = AccountsFeedlyWebWindowController()
accountsFeedlyWindowController.runSheetOnWindow(self.view.window!)
accountsAddWindowController = accountsFeedlyWindowController
let addAccount = OAuthAccountAuthorizationOperation(accountType: .feedly)
addAccount.delegate = self
addAccount.presentationAnchor = self.view.window!
OperationQueue.main.addOperation(addAccount)
default:
break
}
@ -113,3 +114,23 @@ extension AccountsAddViewController: NSTableViewDelegate {
}
}
// MARK: OAuthAccountAuthorizationOperationDelegate
extension AccountsAddViewController: OAuthAccountAuthorizationOperationDelegate {
func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didCreate account: Account) {
account.refreshAll { [weak self] result in
switch result {
case .success:
break
case .failure(let error):
self?.presentError(error)
}
}
}
func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didFailWith error: Error) {
view.window?.presentError(error)
}
}

View File

@ -1,66 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14865.1" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14865.1"/>
<plugIn identifier="com.apple.WebKit2IBPlugin" version="14865.1"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="AccountsFeedlyWebWindowController" customModule="NetNewsWire" customModuleProvider="target">
<connections>
<outlet property="webView" destination="W4c-Xp-rpq" id="l11-5B-8yc"/>
<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="480" height="708"/>
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
<view key="contentView" id="se5-gp-TjO">
<rect key="frame" x="0.0" y="0.0" width="480" height="708"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<wkWebView wantsLayer="YES" translatesAutoresizingMaskIntoConstraints="NO" id="W4c-Xp-rpq">
<rect key="frame" x="0.0" y="61" width="480" height="647"/>
<wkWebViewConfiguration key="configuration">
<audiovisualMediaTypes key="mediaTypesRequiringUserActionForPlayback" none="YES"/>
<wkPreferences key="preferences"/>
</wkWebViewConfiguration>
<connections>
<outlet property="navigationDelegate" destination="-2" id="wAp-Oh-5EK"/>
</connections>
</wkWebView>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="PD2-Zk-3yM">
<rect key="frame" x="384" y="13" width="82" height="32"/>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="IEi-N0-sbw">
<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="5BT-to-e4W"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="W4c-Xp-rpq" secondAttribute="trailing" id="D9j-IU-BZj"/>
<constraint firstAttribute="bottom" secondItem="PD2-Zk-3yM" secondAttribute="bottom" constant="20" symbolic="YES" id="Qdc-tu-9kO"/>
<constraint firstItem="W4c-Xp-rpq" firstAttribute="top" secondItem="se5-gp-TjO" secondAttribute="top" id="V7Q-kM-JDA"/>
<constraint firstAttribute="trailing" secondItem="PD2-Zk-3yM" secondAttribute="trailing" constant="20" symbolic="YES" id="bQS-L4-jbx"/>
<constraint firstItem="W4c-Xp-rpq" firstAttribute="leading" secondItem="se5-gp-TjO" secondAttribute="leading" id="ec6-U0-t8X"/>
<constraint firstItem="PD2-Zk-3yM" firstAttribute="top" secondItem="W4c-Xp-rpq" secondAttribute="bottom" constant="20" symbolic="YES" id="zlA-8I-aKr"/>
</constraints>
</view>
<connections>
<outlet property="delegate" destination="-2" id="0bl-1N-AYu"/>
</connections>
<point key="canvasLocation" x="134" y="556"/>
</window>
</objects>
</document>

View File

@ -1,100 +0,0 @@
//
// AccountsFeedlyWebWindowController.swift
// NetNewsWire
//
// Created by Kiel Gillard on 30/8/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Cocoa
import Account
import WebKit
class AccountsFeedlyWebWindowController: NSWindowController, WKNavigationDelegate {
@IBOutlet private weak var webView: WKWebView!
private weak var hostWindow: NSWindow?
convenience init() {
self.init(windowNibName: NSNib.Name("AccountsFeedlyWeb"))
}
// MARK: API
func runSheetOnWindow(_ hostWindow: NSWindow, completionHandler handler: ((NSApplication.ModalResponse) -> Void)? = nil) {
self.hostWindow = hostWindow
hostWindow.beginSheet(window!, completionHandler: handler)
beginAuthorization()
}
// MARK: Requesting an Access Token
let client = Account.oauthAuthorizationClient(for: .feedly)
private func beginAuthorization() {
let request = Account.oauthAuthorizationCodeGrantRequest(for: .feedly, client: client)
webView.load(request)
}
private func requestAccessToken(for response: OAuthAuthorizationResponse) {
Account.requestOAuthAccessToken(with: response, client: client, accountType: .feedly) { [weak self] result in
switch result {
case .success(let tokenResponse):
self?.saveAccount(for: tokenResponse)
case .failure(let error):
NSApplication.shared.presentError(error)
}
}
}
// MARK: Actions
@IBAction func cancel(_ sender: Any) {
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel)
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
do {
guard let url = navigationAction.request.url else { return }
let response = try OAuthAuthorizationResponse(url: url, client: client)
requestAccessToken(for: response)
// No point the web view trying to load this.
return decisionHandler(.cancel)
} catch let error as OAuthAuthorizationErrorResponse {
NSApplication.shared.presentError(error)
} catch {
print(error)
}
decisionHandler(.allow)
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
print(error)
}
private func saveAccount(for grant: OAuthAuthorizationGrant) {
// TODO: Find an already existing account for this username?
let account = AccountManager.shared.createAccount(type: .feedly)
do {
// Store the refresh token first because it sends this token to the account delegate.
if let token = grant.refreshToken {
try account.storeCredentials(token)
}
// Now store the access token because we want the account delegate to use it.
try account.storeCredentials(grant.accessToken)
self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK)
} catch {
NSApplication.shared.presentError(error)
}
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ArticleExtractorInactiveDark.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ArticleExtractorInactiveLight.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -108,19 +108,18 @@
5183CCE5226F4DFA0010922C /* RefreshInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCE4226F4DFA0010922C /* RefreshInterval.swift */; };
5183CCE6226F4E110010922C /* RefreshInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCE4226F4DFA0010922C /* RefreshInterval.swift */; };
5183CCE8226F68D90010922C /* AccountRefreshTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCE7226F68D90010922C /* AccountRefreshTimer.swift */; };
5183CCE9226F68D90010922C /* AccountRefreshTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCE7226F68D90010922C /* AccountRefreshTimer.swift */; };
518651B223555EB20078E021 /* NNW3Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518651AB23555EB20078E021 /* NNW3Document.swift */; };
518651DA235621840078E021 /* ImageTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518651D9235621840078E021 /* ImageTransition.swift */; };
5186A635235EF3A800C97195 /* VibrantLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5186A634235EF3A800C97195 /* VibrantLabel.swift */; };
518B2EE82351B45600400001 /* NetNewsWire_iOSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840D61952029031D009BC708 /* NetNewsWire_iOSTests.swift */; };
518C3193237B00D9004D740F /* ArticleIconSchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5141E7552374A2890013FF27 /* ArticleIconSchemeHandler.swift */; };
518C3194237B00DA004D740F /* ArticleIconSchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5141E7552374A2890013FF27 /* ArticleIconSchemeHandler.swift */; };
51934CCB230F599B006127BE /* ThemedNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51934CC1230F5963006127BE /* ThemedNavigationController.swift */; };
51934CCE2310792F006127BE /* ActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51934CCD2310792F006127BE /* ActivityManager.swift */; };
51938DF2231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */; };
51938DF3231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */; };
519B8D332143397200FA689C /* SharingServiceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519B8D322143397200FA689C /* SharingServiceDelegate.swift */; };
519D740623243CC0008BB345 /* RefreshInterval-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519D740523243CC0008BB345 /* RefreshInterval-Extensions.swift */; };
519E743D22C663F900A78E47 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519E743422C663F900A78E47 /* SceneDelegate.swift */; };
51A16997235E10D700EB091F /* RefreshIntervalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A1698D235E10D600EB091F /* RefreshIntervalViewController.swift */; };
51A16999235E10D700EB091F /* LocalAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A1698F235E10D600EB091F /* LocalAccountViewController.swift */; };
51A1699A235E10D700EB091F /* Settings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 51A16990235E10D600EB091F /* Settings.storyboard */; };
51A1699B235E10D700EB091F /* AccountInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16991235E10D600EB091F /* AccountInspectorViewController.swift */; };
@ -128,7 +127,6 @@
51A1699D235E10D700EB091F /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16993235E10D600EB091F /* SettingsViewController.swift */; };
51A1699F235E10D700EB091F /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16995235E10D600EB091F /* AboutViewController.swift */; };
51A169A0235E10D700EB091F /* FeedbinAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16996235E10D700EB091F /* FeedbinAccountViewController.swift */; };
51AF460E232488C6001742EF /* Account-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AF460D232488C6001742EF /* Account-Extensions.swift */; };
51B62E68233186730085F949 /* IconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B62E67233186730085F949 /* IconView.swift */; };
51BB7C272335A8E5008E8144 /* ArticleActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */; };
51BB7C312335ACDE008E8144 /* page.html in Resources */ = {isa = PBXBuildFile; fileRef = 51BB7C302335ACDE008E8144 /* page.html */; };
@ -257,7 +255,7 @@
6581C74220CED60100F4AD34 /* ToolbarItemIcon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 6581C74120CED60100F4AD34 /* ToolbarItemIcon.pdf */; };
65ED3FB7235DEF6C0081F399 /* ArticleArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F204DF1FAACBB30076E152 /* ArticleArray.swift */; };
65ED3FB8235DEF6C0081F399 /* CrashReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848B937121C8C5540038DC0D /* CrashReporter.swift */; };
65ED3FB9235DEF6C0081F399 /* TimelineIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847CD6C9232F4CBF00FAC46D /* TimelineIconView.swift */; };
65ED3FB9235DEF6C0081F399 /* IconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847CD6C9232F4CBF00FAC46D /* IconView.swift */; };
65ED3FBA235DEF6C0081F399 /* ArticleExtractorConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A92332C2FD0090D516 /* ArticleExtractorConfig.swift */; };
65ED3FBB235DEF6C0081F399 /* InspectorWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BBB12C20142A4700F054F5 /* InspectorWindowController.swift */; };
65ED3FBC235DEF6C0081F399 /* ColorHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F78227716380050506E /* ColorHash.swift */; };
@ -276,7 +274,6 @@
65ED3FC9235DEF6C0081F399 /* SmartFeedPasteboardWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AD1EB92031649C00BC20B7 /* SmartFeedPasteboardWriter.swift */; };
65ED3FCA235DEF6C0081F399 /* SmartFeedsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CC88171FE59CBF00644329 /* SmartFeedsController.swift */; };
65ED3FCB235DEF6C0081F399 /* SidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97621ED9EB96007D329B /* SidebarViewController.swift */; };
65ED3FCC235DEF6C0081F399 /* AccountsFeedlyWebWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA33BB72318F8C10097B644 /* AccountsFeedlyWebWindowController.swift */; };
65ED3FCD235DEF6C0081F399 /* SidebarOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97601ED9EB96007D329B /* SidebarOutlineView.swift */; };
65ED3FCE235DEF6C0081F399 /* DetailKeyboardDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5127B236222B4849006D641D /* DetailKeyboardDelegate.swift */; };
65ED3FCF235DEF6C0081F399 /* TimelineContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405DD9822153B6B008CE1BF /* TimelineContainerView.swift */; };
@ -307,7 +304,6 @@
65ED3FE8235DEF6C0081F399 /* ArticleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97871ED9ECEF007D329B /* ArticleStyle.swift */; };
65ED3FE9235DEF6C0081F399 /* FaviconURLFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */; };
65ED3FEA235DEF6C0081F399 /* SidebarViewController+ContextualMenus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7178B201E66580091657D /* SidebarViewController+ContextualMenus.swift */; };
65ED3FEB235DEF6C0081F399 /* (null) in Sources */ = {isa = PBXBuildFile; };
65ED3FEC235DEF6C0081F399 /* RSHTMLMetadata+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842611A11FCB769D0086A189 /* RSHTMLMetadata+Extension.swift */; };
65ED3FED235DEF6C0081F399 /* SendToMarsEditCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A1500420048DDF0046AD9A /* SendToMarsEditCommand.swift */; };
65ED3FEE235DEF6C0081F399 /* UserNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FE10022345529D0056195D /* UserNotificationManager.swift */; };
@ -407,7 +403,6 @@
65ED4050235DEF6C0081F399 /* DetailKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5127B237222B4849006D641D /* DetailKeyboardShortcuts.plist */; };
65ED4051235DEF6C0081F399 /* TimelineKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 845479871FEB77C000AD8B59 /* TimelineKeyboardShortcuts.plist */; };
65ED4052235DEF6C0081F399 /* template.html in Resources */ = {isa = PBXBuildFile; fileRef = 848362FE2262A30E00DA1D35 /* template.html */; };
65ED4053235DEF6C0081F399 /* AccountsFeedlyWeb.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9EA33BB82318F8C10097B644 /* AccountsFeedlyWeb.xib */; };
65ED4054235DEF6C0081F399 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 848363062262A3DD00DA1D35 /* Main.storyboard */; };
65ED4055235DEF6C0081F399 /* AccountsAdd.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51EF0F8D2279C9260050506E /* AccountsAdd.xib */; };
65ED4056235DEF6C0081F399 /* NetNewsWire.sdef in Resources */ = {isa = PBXBuildFile; fileRef = 84C9FC8A22629E8F00D921D6 /* NetNewsWire.sdef */; };
@ -494,7 +489,7 @@
84702AA41FA27AC0006B8943 /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */; };
8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8472058020142E8900AD578B /* FeedInspectorViewController.swift */; };
8477ACBE22238E9500DF7F37 /* SearchFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8477ACBD22238E9500DF7F37 /* SearchFeedDelegate.swift */; };
847CD6CA232F4CBF00FAC46D /* TimelineIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847CD6C9232F4CBF00FAC46D /* TimelineIconView.swift */; };
847CD6CA232F4CBF00FAC46D /* IconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847CD6C9232F4CBF00FAC46D /* IconView.swift */; };
847E64A02262783000E00365 /* NSAppleEventDescriptor+UserRecordFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847E64942262782F00E00365 /* NSAppleEventDescriptor+UserRecordFields.swift */; };
848362FD2262A30800DA1D35 /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 848362FC2262A30800DA1D35 /* styleSheet.css */; };
848362FF2262A30E00DA1D35 /* template.html in Resources */ = {isa = PBXBuildFile; fileRef = 848362FE2262A30E00DA1D35 /* template.html */; };
@ -615,8 +610,6 @@
84F9EAF4213660A100CF2DE4 /* testGenericScript.applescript in Sources */ = {isa = PBXBuildFile; fileRef = 84F9EAE1213660A100CF2DE4 /* testGenericScript.applescript */; };
84F9EAF5213660A100CF2DE4 /* establishMainWindowStartingState.applescript in Sources */ = {isa = PBXBuildFile; fileRef = 84F9EAE2213660A100CF2DE4 /* establishMainWindowStartingState.applescript */; };
84FF69B11FC3793300DC198E /* FaviconURLFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */; };
9EA33BB92318F8C10097B644 /* AccountsFeedlyWebWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA33BB72318F8C10097B644 /* AccountsFeedlyWebWindowController.swift */; };
9EA33BBA2318F8C10097B644 /* AccountsFeedlyWeb.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9EA33BB82318F8C10097B644 /* AccountsFeedlyWeb.xib */; };
B528F81E23333C7E00E735DD /* page.html in Resources */ = {isa = PBXBuildFile; fileRef = B528F81D23333C7E00E735DD /* page.html */; };
D553738B20186C20006D8857 /* Article+Scriptability.swift in Sources */ = {isa = PBXBuildFile; fileRef = D553737C20186C1F006D8857 /* Article+Scriptability.swift */; };
D57BE6E0204CD35F00D11AAC /* NSScriptCommand+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57BE6DF204CD35F00D11AAC /* NSScriptCommand+NetNewsWire.swift */; };
@ -1284,9 +1277,7 @@
51934CCD2310792F006127BE /* ActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityManager.swift; sourceTree = "<group>"; };
51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTimelineFeedDelegate.swift; sourceTree = "<group>"; };
519B8D322143397200FA689C /* SharingServiceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingServiceDelegate.swift; sourceTree = "<group>"; };
519D740523243CC0008BB345 /* RefreshInterval-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RefreshInterval-Extensions.swift"; sourceTree = "<group>"; };
519E743422C663F900A78E47 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
51A1698D235E10D600EB091F /* RefreshIntervalViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RefreshIntervalViewController.swift; sourceTree = "<group>"; };
51A1698F235E10D600EB091F /* LocalAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalAccountViewController.swift; sourceTree = "<group>"; };
51A16990235E10D600EB091F /* Settings.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Settings.storyboard; sourceTree = "<group>"; };
51A16991235E10D600EB091F /* AccountInspectorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountInspectorViewController.swift; sourceTree = "<group>"; };
@ -1294,7 +1285,6 @@
51A16993235E10D600EB091F /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
51A16995235E10D600EB091F /* AboutViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = "<group>"; };
51A16996235E10D700EB091F /* FeedbinAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedbinAccountViewController.swift; sourceTree = "<group>"; };
51AF460D232488C6001742EF /* Account-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account-Extensions.swift"; sourceTree = "<group>"; };
51B62E67233186730085F949 /* IconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconView.swift; sourceTree = "<group>"; };
51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleActivityItemSource.swift; sourceTree = "<group>"; };
51BB7C302335ACDE008E8144 /* page.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = page.html; sourceTree = "<group>"; };
@ -1416,7 +1406,7 @@
8472058020142E8900AD578B /* FeedInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedInspectorViewController.swift; sourceTree = "<group>"; };
847752FE2008879500D93690 /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; };
8477ACBD22238E9500DF7F37 /* SearchFeedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFeedDelegate.swift; sourceTree = "<group>"; };
847CD6C9232F4CBF00FAC46D /* TimelineIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineIconView.swift; sourceTree = "<group>"; };
847CD6C9232F4CBF00FAC46D /* IconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconView.swift; sourceTree = "<group>"; };
847E64942262782F00E00365 /* NSAppleEventDescriptor+UserRecordFields.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAppleEventDescriptor+UserRecordFields.swift"; sourceTree = "<group>"; };
848362FC2262A30800DA1D35 /* styleSheet.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = styleSheet.css; sourceTree = "<group>"; };
848362FE2262A30E00DA1D35 /* template.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = template.html; sourceTree = "<group>"; };
@ -1538,8 +1528,6 @@
84F9EAE2213660A100CF2DE4 /* establishMainWindowStartingState.applescript */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.applescript; path = establishMainWindowStartingState.applescript; sourceTree = "<group>"; };
84F9EAE4213660A100CF2DE4 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconURLFinder.swift; sourceTree = "<group>"; };
9EA33BB72318F8C10097B644 /* AccountsFeedlyWebWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsFeedlyWebWindowController.swift; sourceTree = "<group>"; };
9EA33BB82318F8C10097B644 /* AccountsFeedlyWeb.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AccountsFeedlyWeb.xib; sourceTree = "<group>"; };
B24EFD482330FF99006C6242 /* NetNewsWire-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NetNewsWire-Bridging-Header.h"; sourceTree = "<group>"; };
B24EFD5923310109006C6242 /* WKPreferencesPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WKPreferencesPrivate.h; sourceTree = "<group>"; };
B528F81D23333C7E00E735DD /* page.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = page.html; sourceTree = "<group>"; };
@ -1807,7 +1795,6 @@
51A16990235E10D600EB091F /* Settings.storyboard */,
51A16995235E10D600EB091F /* AboutViewController.swift */,
51A16992235E10D600EB091F /* AddAccountViewController.swift */,
51A1698D235E10D600EB091F /* RefreshIntervalViewController.swift */,
516A09382360A2AE00EAE89B /* SettingsAccountTableViewCell.swift */,
516A091D23609A3600EAE89B /* SettingsAccountTableViewCell.xib */,
516A093A2360A4A000EAE89B /* SettingsTableViewCell.xib */,
@ -1838,15 +1825,6 @@
path = Activity;
sourceTree = "<group>";
};
519D740423243C68008BB345 /* Model Extensions */ = {
isa = PBXGroup;
children = (
519D740523243CC0008BB345 /* RefreshInterval-Extensions.swift */,
51AF460D232488C6001742EF /* Account-Extensions.swift */,
);
path = "Model Extensions";
sourceTree = "<group>";
};
51C45245226506C800C03939 /* UIKit Extensions */ = {
isa = PBXGroup;
children = (
@ -1930,7 +1908,6 @@
children = (
51C4527E2265092C00C03939 /* ArticleViewController.swift */,
517630222336657E00E15FFF /* ArticleViewControllerWebViewProvider.swift */,
5141E7552374A2890013FF27 /* ArticleIconSchemeHandler.swift */,
51102164233A7D6C0007A5F7 /* ArticleExtractorButton.swift */,
5142192923522B5500E07E2C /* ImageViewController.swift */,
514219362352510100E07E2C /* ImageScrollView.swift */,
@ -1954,10 +1931,11 @@
51C452A822650DA100C03939 /* Article Rendering */ = {
isa = PBXGroup;
children = (
49F40DEF2335B71000552BF4 /* newsfoot.js */,
5141E7552374A2890013FF27 /* ArticleIconSchemeHandler.swift */,
849A977D1ED9EC42007D329B /* ArticleRenderer.swift */,
848362FE2262A30E00DA1D35 /* template.html */,
517630032336215100E15FFF /* main.js */,
49F40DEF2335B71000552BF4 /* newsfoot.js */,
848362FE2262A30E00DA1D35 /* template.html */,
);
path = "Article Rendering";
sourceTree = "<group>";
@ -2088,6 +2066,7 @@
519B8D322143397200FA689C /* SharingServiceDelegate.swift */,
849EE72020391F560082A1EA /* SharingServicePickerDelegate.swift */,
51FA73B62332D5F70090D516 /* ArticleExtractorButton.swift */,
847CD6C9232F4CBF00FAC46D /* IconView.swift */,
844B5B6B1FEA224B00C7C76A /* Keyboard */,
849A975F1ED9EB95007D329B /* Sidebar */,
849A97681ED9EBC8007D329B /* Timeline */,
@ -2265,7 +2244,6 @@
84E185C2203BB12600F69BFA /* MultilineTextFieldSizer.swift */,
849A97711ED9EC04007D329B /* TimelineCellData.swift */,
849A97751ED9EC04007D329B /* UnreadIndicatorView.swift */,
847CD6C9232F4CBF00FAC46D /* TimelineIconView.swift */,
);
path = Cell;
sourceTree = "<group>";
@ -2510,8 +2488,6 @@
5144EA2E2279FAB600D19003 /* AccountsDetailViewController.swift */,
5144EA50227B8E4500D19003 /* AccountsFeedbin.xib */,
5144EA4F227B8E4500D19003 /* AccountsFeedbinWindowController.swift */,
9EA33BB82318F8C10097B644 /* AccountsFeedlyWeb.xib */,
9EA33BB72318F8C10097B644 /* AccountsFeedlyWebWindowController.swift */,
55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */,
55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */,
5144EA352279FC3D00D19003 /* AccountsAddLocal.xib */,
@ -2566,7 +2542,6 @@
5123DB95233EC69300282CC9 /* Inspector */,
513145F9235A55A700387FDC /* Intents */,
5183CCEB227117C70010922C /* Settings */,
519D740423243C68008BB345 /* Model Extensions */,
51C45245226506C800C03939 /* UIKit Extensions */,
513C5CE7232571C2003D4054 /* ShareExtension */,
51314643235A7C2300387FDC /* IntentsExtension */,
@ -3359,7 +3334,6 @@
65ED4050235DEF6C0081F399 /* DetailKeyboardShortcuts.plist in Resources */,
65ED4051235DEF6C0081F399 /* TimelineKeyboardShortcuts.plist in Resources */,
65ED4052235DEF6C0081F399 /* template.html in Resources */,
65ED4053235DEF6C0081F399 /* AccountsFeedlyWeb.xib in Resources */,
65ED4054235DEF6C0081F399 /* Main.storyboard in Resources */,
65ED4055235DEF6C0081F399 /* AccountsAdd.xib in Resources */,
65ED4056235DEF6C0081F399 /* NetNewsWire.sdef in Resources */,
@ -3445,7 +3419,6 @@
5127B23A222B4849006D641D /* DetailKeyboardShortcuts.plist in Resources */,
845479881FEB77C000AD8B59 /* TimelineKeyboardShortcuts.plist in Resources */,
848362FF2262A30E00DA1D35 /* template.html in Resources */,
9EA33BBA2318F8C10097B644 /* AccountsFeedlyWeb.xib in Resources */,
848363082262A3DD00DA1D35 /* Main.storyboard in Resources */,
51EF0F8E2279C9260050506E /* AccountsAdd.xib in Resources */,
84C9FC8F22629E8F00D921D6 /* NetNewsWire.sdef in Resources */,
@ -3743,7 +3716,7 @@
files = (
65ED3FB7235DEF6C0081F399 /* ArticleArray.swift in Sources */,
65ED3FB8235DEF6C0081F399 /* CrashReporter.swift in Sources */,
65ED3FB9235DEF6C0081F399 /* TimelineIconView.swift in Sources */,
65ED3FB9235DEF6C0081F399 /* IconView.swift in Sources */,
65ED3FBA235DEF6C0081F399 /* ArticleExtractorConfig.swift in Sources */,
65ED3FBB235DEF6C0081F399 /* InspectorWindowController.swift in Sources */,
65ED3FBC235DEF6C0081F399 /* ColorHash.swift in Sources */,
@ -3762,7 +3735,6 @@
65ED3FC9235DEF6C0081F399 /* SmartFeedPasteboardWriter.swift in Sources */,
65ED3FCA235DEF6C0081F399 /* SmartFeedsController.swift in Sources */,
65ED3FCB235DEF6C0081F399 /* SidebarViewController.swift in Sources */,
65ED3FCC235DEF6C0081F399 /* AccountsFeedlyWebWindowController.swift in Sources */,
65ED3FCD235DEF6C0081F399 /* SidebarOutlineView.swift in Sources */,
65ED3FCE235DEF6C0081F399 /* DetailKeyboardDelegate.swift in Sources */,
65ED3FCF235DEF6C0081F399 /* TimelineContainerView.swift in Sources */,
@ -3793,8 +3765,8 @@
65ED3FE8235DEF6C0081F399 /* ArticleStyle.swift in Sources */,
65ED3FE9235DEF6C0081F399 /* FaviconURLFinder.swift in Sources */,
65ED3FEA235DEF6C0081F399 /* SidebarViewController+ContextualMenus.swift in Sources */,
65ED3FEB235DEF6C0081F399 /* (null) in Sources */,
65ED3FEC235DEF6C0081F399 /* RSHTMLMetadata+Extension.swift in Sources */,
518C3194237B00DA004D740F /* ArticleIconSchemeHandler.swift in Sources */,
65ED3FED235DEF6C0081F399 /* SendToMarsEditCommand.swift in Sources */,
65ED3FEE235DEF6C0081F399 /* UserNotificationManager.swift in Sources */,
65ED3FEF235DEF6C0081F399 /* ScriptingObjectContainer.swift in Sources */,
@ -3913,7 +3885,6 @@
51F85BFD2275DCA800C787DC /* SingleLineUILabelSizer.swift in Sources */,
517630232336657E00E15FFF /* ArticleViewControllerWebViewProvider.swift in Sources */,
51C4528F226509BD00C03939 /* UnreadFeed.swift in Sources */,
51AF460E232488C6001742EF /* Account-Extensions.swift in Sources */,
51FD413B2342BD0500880194 /* MasterTimelineUnreadCountView.swift in Sources */,
513146B2235A81A400387FDC /* AddFeedIntentHandler.swift in Sources */,
51D87EE12311D34700E63F03 /* ActivityType.swift in Sources */,
@ -3988,20 +3959,17 @@
51A1699B235E10D700EB091F /* AccountInspectorViewController.swift in Sources */,
51C452762265091600C03939 /* MasterTimelineViewController.swift in Sources */,
5108F6D823763094001ABC45 /* TickMarkSlider.swift in Sources */,
5183CCE9226F68D90010922C /* AccountRefreshTimer.swift in Sources */,
51C452882265093600C03939 /* AddFeedViewController.swift in Sources */,
51A169A0235E10D700EB091F /* FeedbinAccountViewController.swift in Sources */,
51934CCE2310792F006127BE /* ActivityManager.swift in Sources */,
5108F6B72375E612001ABC45 /* CacheCleaner.swift in Sources */,
518651DA235621840078E021 /* ImageTransition.swift in Sources */,
514219372352510100E07E2C /* ImageScrollView.swift in Sources */,
51A16997235E10D700EB091F /* RefreshIntervalViewController.swift in Sources */,
516AE9B32371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift in Sources */,
51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */,
84DEE56622C32CA4005FC42C /* SmartFeedDelegate.swift in Sources */,
512E09012268907400BDCFDD /* MasterFeedTableViewSectionHeader.swift in Sources */,
516AE9E02372269A007DEEAA /* IconImage.swift in Sources */,
519D740623243CC0008BB345 /* RefreshInterval-Extensions.swift in Sources */,
51C45268226508F600C03939 /* MasterFeedUnreadCountView.swift in Sources */,
5183CCD0226E1E880010922C /* NonIntrinsicLabel.swift in Sources */,
51C4529F22650A1900C03939 /* AuthorAvatarDownloader.swift in Sources */,
@ -4032,7 +4000,7 @@
files = (
84F204E01FAACBB30076E152 /* ArticleArray.swift in Sources */,
848B937221C8C5540038DC0D /* CrashReporter.swift in Sources */,
847CD6CA232F4CBF00FAC46D /* TimelineIconView.swift in Sources */,
847CD6CA232F4CBF00FAC46D /* IconView.swift in Sources */,
51FA73AA2332C2FD0090D516 /* ArticleExtractorConfig.swift in Sources */,
84BBB12E20142A4700F054F5 /* InspectorWindowController.swift in Sources */,
51EF0F7A22771B890050506E /* ColorHash.swift in Sources */,
@ -4053,7 +4021,6 @@
849C78922362AB04009A71E4 /* ExportOPMLWindowController.swift in Sources */,
84CC88181FE59CBF00644329 /* SmartFeedsController.swift in Sources */,
849A97661ED9EB96007D329B /* SidebarViewController.swift in Sources */,
9EA33BB92318F8C10097B644 /* AccountsFeedlyWebWindowController.swift in Sources */,
849A97641ED9EB96007D329B /* SidebarOutlineView.swift in Sources */,
5127B238222B4849006D641D /* DetailKeyboardDelegate.swift in Sources */,
8405DD9922153B6B008CE1BF /* TimelineContainerView.swift in Sources */,
@ -4128,6 +4095,7 @@
848D578E21543519005FFAD5 /* PasteboardFeed.swift in Sources */,
5144EA2F2279FAB600D19003 /* AccountsDetailViewController.swift in Sources */,
849A97801ED9EC42007D329B /* DetailViewController.swift in Sources */,
518C3193237B00D9004D740F /* ArticleIconSchemeHandler.swift in Sources */,
84C9FC6722629B9000D921D6 /* AppDelegate.swift in Sources */,
84C9FC7A22629E1200D921D6 /* AccountsTableViewBackgroundView.swift in Sources */,
84CAFCAF22BC8C35007694F0 /* FetchRequestOperation.swift in Sources */,

View File

@ -31,9 +31,8 @@ struct ArticleRenderer {
private let title: String
private let body: String
private let baseURL: String?
private let useImageIcon: Bool
private init(article: Article?, extractedArticle: ExtractedArticle?, style: ArticleStyle, useImageIcon: Bool = false) {
private init(article: Article?, extractedArticle: ExtractedArticle?, style: ArticleStyle) {
self.article = article
self.extractedArticle = extractedArticle
self.articleStyle = style
@ -45,13 +44,12 @@ struct ArticleRenderer {
self.body = article?.body ?? ""
self.baseURL = article?.baseURL?.absoluteString
}
self.useImageIcon = useImageIcon
}
// MARK: - API
static func articleHTML(article: Article, extractedArticle: ExtractedArticle? = nil, style: ArticleStyle, useImageIcon: Bool = false) -> Rendering {
let renderer = ArticleRenderer(article: article, extractedArticle: extractedArticle, style: style, useImageIcon: useImageIcon)
let renderer = ArticleRenderer(article: article, extractedArticle: extractedArticle, style: style)
return (renderer.styleString(), renderer.articleHTML)
}
@ -104,9 +102,6 @@ private extension ArticleRenderer {
return renderHTML(withBody: "")
}
static var faviconImgTagCache = [Feed: String]()
static var feedIconImgTagCache = [Feed: String]()
static var defaultStyleSheet: String = {
let path = Bundle.main.path(forResource: "styleSheet", ofType: "css")!
let s = try! NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue)
@ -146,13 +141,7 @@ private extension ArticleRenderer {
d["title"] = title
d["body"] = body
d["avatars"] = ""
var didAddAvatar = false
if let avatarHTML = avatarImgTag() {
d["avatars"] = "<td class=\"header rightAlign avatar\">\(avatarHTML)</td>";
didAddAvatar = true
}
d["avatars"] = "<td class=\"header rightAlign avatar\"><img src=\"\(ArticleRenderer.imageIconScheme)://\" height=48 width=48 /></td>";
var feedLink = ""
if let feedTitle = article.feed?.nameForDisplay {
@ -163,12 +152,6 @@ private extension ArticleRenderer {
}
d["feedlink"] = feedLink
if !didAddAvatar, let feed = article.feed {
if let favicon = faviconImgTag(forFeed: feed) {
d["avatars"] = "<td class=\"header rightAlign\">\(favicon)</td>";
}
}
let datePublished = article.logicalDatePublished
let longDate = dateString(datePublished, .long, .medium)
let mediumDate = dateString(datePublished, .medium, .short)
@ -200,111 +183,6 @@ private extension ArticleRenderer {
return permalink != preferredLink // Make date a link if its a different link from the titles link
}
func faviconImgTag(forFeed feed: Feed) -> String? {
if let cachedImgTag = ArticleRenderer.faviconImgTagCache[feed] {
return cachedImgTag
}
if let iconImage = appDelegate.faviconDownloader.faviconAsIcon(for: feed) {
if let s = base64String(forImage: iconImage.image) {
var dimension = min(iconImage.image.size.height, CGFloat(ArticleRenderer.avatarDimension)) // Assuming square images.
dimension = max(dimension, 16) // Some favicons say theyre < 16. Force them larger.
if dimension >= CGFloat(ArticleRenderer.avatarDimension) * 0.8 { //Close enough to scale up.
dimension = CGFloat(ArticleRenderer.avatarDimension)
}
let imgTag: String
if dimension >= CGFloat(ArticleRenderer.avatarDimension) {
// Use rounded corners.
imgTag = "<img src=\"data:image/tiff;base64, " + s + "\" height=\(Int(dimension)) width=\(Int(dimension)) style=\"border-radius:4px\" />"
}
else {
imgTag = "<img src=\"data:image/tiff;base64, " + s + "\" height=\(Int(dimension)) width=\(Int(dimension)) />"
}
ArticleRenderer.faviconImgTagCache[feed] = imgTag
return imgTag
}
}
return nil
}
func feedIconImgTag(forFeed feed: Feed) -> String? {
if let cachedImgTag = ArticleRenderer.feedIconImgTagCache[feed] {
return cachedImgTag
}
if useImageIcon {
return "<img src=\"\(ArticleRenderer.imageIconScheme)://article.png\" height=48 width=48 />"
}
if let iconImage = appDelegate.feedIconDownloader.icon(for: feed) {
if let s = base64String(forImage: iconImage.image) {
#if os(macOS)
let imgTag = "<img src=\"data:image/tiff;base64, " + s + "\" height=48 width=48 />"
#else
let imgTag = "<img src=\"data:image/png;base64, " + s + "\" height=48 width=48 />"
#endif
ArticleRenderer.feedIconImgTagCache[feed] = imgTag
return imgTag
}
}
return nil
}
func base64String(forImage image: RSImage) -> String? {
return image.dataRepresentation()?.base64EncodedString()
}
func singleArticleSpecifiedAuthor() -> Author? {
// The author of this article, if just one.
if let authors = article?.authors, authors.count == 1 {
return authors.first!
}
return nil
}
func singleFeedSpecifiedAuthor() -> Author? {
if let authors = article?.feed?.authors, authors.count == 1 {
return authors.first!
}
return nil
}
static let avatarDimension = 48
struct Avatar {
let imageURL: String
let url: String?
func html(dimension: Int) -> String {
let imageTag = "<img src=\"\(imageURL)\" width=\(dimension) height=\(dimension) />"
if let url = url {
return imageTag.htmlByAddingLink(url)
}
return imageTag
}
}
func avatarImgTag() -> String? {
if let author = singleArticleSpecifiedAuthor(), let authorImageURL = author.avatarURL {
let imageURL = useImageIcon ? ArticleRenderer.imageIconScheme : authorImageURL
return Avatar(imageURL: imageURL, url: author.url).html(dimension: ArticleRenderer.avatarDimension)
}
if let feed = article?.feed, let imgTag = feedIconImgTag(forFeed: feed) {
return imgTag
}
if let feedIconURL = article?.feed?.iconURL {
return Avatar(imageURL: feedIconURL, url: article?.feed?.homePageURL ?? article?.feed?.url).html(dimension: ArticleRenderer.avatarDimension)
}
if let author = singleFeedSpecifiedAuthor(), let imageURL = author.avatarURL {
return Avatar(imageURL: imageURL, url: author.url).html(dimension: ArticleRenderer.avatarDimension)
}
return nil
}
func byline() -> String {
guard let authors = article?.authors ?? article?.feed?.authors, !authors.isEmpty else {
return ""

View File

@ -94,7 +94,7 @@ extension Article {
}
}
// MARK: PathIDUserInfoProvider
// MARK: DeepLinkProvider
extension Article: DeepLinkProvider {

View File

@ -22,11 +22,7 @@ extension Feed: SmallIconProvider {
if let iconImage = appDelegate.faviconDownloader.favicon(for: self) {
return iconImage
}
#if os(macOS)
return AppAssets.genericFeedImage
#else
return FaviconGenerator.favicon(self)
#endif
}
}

View File

@ -23,6 +23,10 @@ struct AppAssets {
return UIImage(named: "accountFeedbin")!
}()
static var accountFeedlyImage: UIImage = {
return UIImage(named: "accountFeedly")!
}()
static var accountFreshRSSImage: UIImage = {
return UIImage(named: "accountFreshRSS")!
}()
@ -200,6 +204,8 @@ struct AppAssets {
}
case .feedbin:
return AppAssets.accountFeedbinImage
case .feedly:
return AppAssets.accountFeedlyImage
case .freshRSS:
return AppAssets.accountFreshRSSImage
default:

View File

@ -24,7 +24,6 @@ struct AppDefaults {
static let timelineIconSize = "timelineIconSize"
static let timelineSortDirection = "timelineSortDirection"
static let displayUndoAvailableTip = "displayUndoAvailableTip"
static let refreshInterval = "refreshInterval"
static let lastRefresh = "lastRefresh"
}
@ -45,16 +44,6 @@ struct AppDefaults {
}
}
static var refreshInterval: RefreshInterval {
get {
let rawValue = AppDefaults.shared.integer(forKey: Key.refreshInterval)
return RefreshInterval(rawValue: rawValue) ?? RefreshInterval.everyHour
}
set {
AppDefaults.shared.set(newValue.rawValue, forKey: Key.refreshInterval)
}
}
static var timelineGroupByFeed: Bool {
get {
return bool(for: Key.timelineGroupByFeed)
@ -112,7 +101,6 @@ struct AppDefaults {
static func registerDefaults() {
let defaults: [String : Any] = [Key.lastImageCacheFlushDate: Date(),
Key.refreshInterval: RefreshInterval.everyHour.rawValue,
Key.timelineGroupByFeed: false,
Key.timelineNumberOfLines: 2,
Key.timelineIconSize: MasterTimelineIconSize.medium.rawValue,

View File

@ -290,9 +290,9 @@ private extension AppDelegate {
/// Schedules a background app refresh based on `AppDefaults.refreshInterval`.
func scheduleBackgroundFeedRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.ranchero.NetNewsWire.FeedRefresh")
request.earliestBeginDate = Date(timeIntervalSinceNow: AppDefaults.refreshInterval.inSeconds())
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
// We send this to a dedicated seria queue because as of 11/05/19 on iOS 13.2 the call to the
// We send this to a dedicated serial queue because as of 11/05/19 on iOS 13.2 the call to the
// task scheduler can hang indefinitely.
bgTaskDispatchQueue.async {
do {

View File

@ -11,7 +11,7 @@ import Account
class FeedInspectorViewController: UITableViewController {
static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 400.0)
static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 500.0)
var feed: Feed!
@IBOutlet weak var nameTextField: UITextField!

View File

@ -58,17 +58,17 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
NotificationCenter.default.addObserver(self, selector: #selector(feedMetadataDidChange(_:)), name: .FeedMetadataDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(userDidAddFeed(_:)), name: .UserDidAddFeed, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
refreshControl = UIRefreshControl()
refreshControl!.addTarget(self, action: #selector(refreshAccounts(_:)), for: .valueChanged)
configureToolbar()
becomeFirstResponder()
}
override func viewWillAppear(_ animated: Bool) {
navigationController?.title = NSLocalizedString("Feeds", comment: "Feeds")
applyChanges(animate: false)
updateUI()
super.viewWillAppear(animated)
}
@ -99,8 +99,17 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
node = coordinator.rootNode.descendantNodeRepresentingObject(representedObject as AnyObject)
}
if let node = node, dataSource.indexPath(for: node) != nil {
reloadNode(node)
// Only do the reload of the node when absolutely necessary. It can stop programatic scrolling from
// completing if called to soon after a selectRow where scrolling is necessary. See discloseFeed.
if let node = node,
let indexPath = dataSource.indexPath(for: node),
let cell = tableView.cellForRow(at: indexPath) as? MasterFeedTableViewCell,
let unreadCountProvider = node.representedObject as? UnreadCountProvider {
if cell.unreadCount != unreadCountProvider.unreadCount {
self.reloadNode(node)
}
}
}
@ -140,6 +149,10 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
applyChanges(animate: false)
}
@objc func willEnterForeground(_ note: Notification) {
updateUI()
}
// MARK: Table View
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
@ -457,14 +470,14 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
}
}
func updateFeedSelection() {
func updateFeedSelection(animated: Bool) {
if dataSource.snapshot().numberOfItems > 0 {
if let indexPath = coordinator.currentFeedIndexPath {
if tableView.indexPathForSelectedRow != indexPath {
tableView.selectRowAndScrollIfNotVisible(at: indexPath, animated: true)
tableView.selectRowAndScrollIfNotVisible(at: indexPath, animated: animated)
}
} else {
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
tableView.selectRow(at: nil, animated: animated, scrollPosition: .none)
}
}
}
@ -1017,7 +1030,7 @@ private extension MasterFeedViewController {
deleteCommand.perform()
if indexPath == coordinator.currentFeedIndexPath {
coordinator.selectFeed(nil)
coordinator.selectFeed(nil, animated: false)
}
}

View File

@ -25,6 +25,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
weak var coordinator: SceneCoordinator!
var undoableCommands = [UndoableCommand]()
let scrollPositionQueue = CoalescingQueue(name: "Scroll Position", interval: 0.3, maxInterval: 1.0)
private let keyboardManager = KeyboardManager(type: .timeline)
override var keyCommands: [UIKeyCommand]? {
@ -68,14 +69,14 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
resetEstimatedRowHeight()
resetUI()
}
override func viewWillAppear(_ animated: Bool) {
applyChanges(animate: false)
if dataSource.snapshot().numberOfItems < 1 {
navigationItem.searchController?.isActive = false
// Restore the scroll position if we have one stored
if let restoreIndexPath = coordinator.timelineMiddleIndexPath {
tableView.scrollToRow(at: restoreIndexPath, at: .middle, animated: false)
}
}
// MARK: Actions
@ -288,6 +289,10 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
coordinator.selectArticle(article, animated: true)
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollPositionQueue.add(self, #selector(scrollPositionDidChange))
}
// MARK: Notifications
@objc dynamic func unreadCountDidChange(_ notification: Notification) {
@ -366,6 +371,10 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
titleView?.label.text = coordinator.timelineName
}
@objc func scrollPositionDidChange() {
coordinator.timelineMiddleIndexPath = tableView.middleVisibleRow()
}
// MARK: Reloading
func queueReloadAvailableCells() {
@ -583,9 +592,8 @@ private extension MasterTimelineViewController {
func discloseFeedAction(_ article: Article) -> UIAction? {
guard let feed = article.feed else { return nil }
let title = NSLocalizedString("Select Feed", comment: "Select Feed")
let title = NSLocalizedString("Go to Feed", comment: "Go to Feed")
let action = UIAction(title: title, image: AppAssets.openInSidebarImage) { [weak self] action in
self?.coordinator.selectFeed(nil, animated: true)
self?.coordinator.discloseFeed(feed, animated: true)
}
return action
@ -594,9 +602,8 @@ private extension MasterTimelineViewController {
func discloseFeedAlertAction(_ article: Article, completionHandler: @escaping (Bool) -> Void) -> UIAlertAction? {
guard let feed = article.feed else { return nil }
let title = NSLocalizedString("Select Feed", comment: "Select Feed")
let title = NSLocalizedString("Go to Feed", comment: "Go to Feed")
let action = UIAlertAction(title: title, style: .default) { [weak self] action in
self?.coordinator.selectFeed(nil, animated: true)
self?.coordinator.discloseFeed(feed, animated: true)
completionHandler(true)
}

View File

@ -1,22 +0,0 @@
//
// Account-Extensions.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 9/7/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
import Account
extension AccountType: Identifiable {
public var id: Int {
return rawValue
}
}
extension Account: Identifiable {
public var id: String {
return accountID
}
}

View File

@ -1,15 +0,0 @@
//
// RefreshInterval-Extensions.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 9/7/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
extension RefreshInterval: Identifiable {
var id: Int {
return rawValue
}
}

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "accountFeedly.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -89,9 +89,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
}
private let treeControllerDelegate = FeedTreeControllerDelegate()
private lazy var treeController: TreeController = {
return TreeController(delegate: treeControllerDelegate)
}()
private let treeController: TreeController
var stateRestorationActivity: NSUserActivity? {
return activityManager.stateRestorationActivity
@ -135,6 +133,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
var timelineFetcher: ArticleFetcher? {
didSet {
timelineMiddleIndexPath = nil
if timelineFetcher is Feed {
showFeedNames = false
} else {
@ -153,6 +153,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
}
}
var timelineMiddleIndexPath: IndexPath?
private(set) var showFeedNames = false
private(set) var showIcons = false
@ -276,6 +278,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
}
override init() {
treeController = TreeController(delegate: treeControllerDelegate)
super.init()
for section in treeController.rootNode.childNodes {
@ -311,10 +315,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
masterFeedViewController = UIStoryboard.main.instantiateController(ofType: MasterFeedViewController.self)
masterFeedViewController.coordinator = self
masterNavigationController.pushViewController(masterFeedViewController, animated: false)
masterFeedViewController.reloadFeeds()
let articleViewController = UIStoryboard.main.instantiateController(ofType: ArticleViewController.self)
articleViewController.coordinator = self
let detailNavigationController = addNavControllerIfNecessary(articleViewController, showButton: false)
let detailNavigationController = addNavControllerIfNecessary(articleViewController, showButton: true)
rootSplitViewController.showDetailViewController(detailNavigationController, sender: self)
configureThreePanelMode(for: size)
@ -323,7 +328,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
}
func handle(_ activity: NSUserActivity) {
selectFeed(nil)
selectFeed(nil, animated: false)
guard let activityType = ActivityType(rawValue: activity.activityType) else { return }
switch activityType {
@ -367,12 +372,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
}
func selectFirstUnreadInAllUnread() {
selectFeed(IndexPath(row: 1, section: 0))
selectFeed(IndexPath(row: 1, section: 0), animated: false)
selectFirstUnreadArticleInTimeline()
}
func showSearch() {
selectFeed(nil)
selectFeed(nil, animated: false)
installTimelineControllerIfNecessary(animated: false)
DispatchQueue.main.asyncAfter(deadline: .now()) {
self.masterTimelineViewController!.showSearchAll()
@ -528,13 +533,13 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
return indexPathFor(node)
}
func selectFeed(_ indexPath: IndexPath?, animated: Bool = false) {
guard indexPath != currentFeedIndexPath else { return }
func selectFeed(_ indexPath: IndexPath?, animated: Bool) {
guard indexPath != currentFeedIndexPath else { return }
selectArticle(nil)
currentFeedIndexPath = indexPath
masterFeedViewController.updateFeedSelection()
masterFeedViewController.updateFeedSelection(animated: animated)
if let ip = indexPath, let node = nodeFor(ip), let fetcher = node.representedObject as? ArticleFetcher {
timelineFetcher = fetcher
@ -552,31 +557,31 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
func selectPrevFeed() {
if let indexPath = prevFeedIndexPath {
selectFeed(indexPath)
selectFeed(indexPath, animated: true)
}
}
func selectNextFeed() {
if let indexPath = nextFeedIndexPath {
selectFeed(indexPath)
selectFeed(indexPath, animated: true)
}
}
func selectTodayFeed() {
masterFeedViewController?.ensureSectionIsExpanded(0) {
self.selectFeed(IndexPath(row: 0, section: 0))
self.selectFeed(IndexPath(row: 0, section: 0), animated: true)
}
}
func selectAllUnreadFeed() {
masterFeedViewController?.ensureSectionIsExpanded(0) {
self.selectFeed(IndexPath(row: 1, section: 0))
self.selectFeed(IndexPath(row: 1, section: 0), animated: true)
}
}
func selectStarredFeed() {
masterFeedViewController?.ensureSectionIsExpanded(0) {
self.selectFeed(IndexPath(row: 2, section: 0))
self.selectFeed(IndexPath(row: 2, section: 0), animated: true)
}
}
@ -819,14 +824,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
let feedInspectorNavController =
UIStoryboard.inspector.instantiateViewController(identifier: "FeedInspectorNavigationViewController") as! UINavigationController
let feedInspectorController = feedInspectorNavController.topViewController as! FeedInspectorViewController
feedInspectorController.modalPresentationStyle = .formSheet
feedInspectorController.preferredContentSize = FeedInspectorViewController.preferredContentSizeForFormSheetDisplay
feedInspectorNavController.modalPresentationStyle = .formSheet
feedInspectorNavController.preferredContentSize = FeedInspectorViewController.preferredContentSizeForFormSheetDisplay
feedInspectorController.feed = feed
rootSplitViewController.present(feedInspectorNavController, animated: true)
}
func showAdd(_ type: AddControllerType, initialFeed: String? = nil, initialFeedName: String? = nil) {
selectFeed(nil)
selectFeed(nil, animated: false)
let addViewController = UIStoryboard.add.instantiateInitialViewController() as! UINavigationController
@ -965,7 +970,7 @@ extension SceneCoordinator: UINavigationControllerDelegate {
// If we are showing the Feeds and only the feeds start clearing stuff
if viewController === masterFeedViewController && !isThreePanelMode && !isTimelineViewControllerPending {
activityManager.invalidateCurrentActivities()
selectFeed(nil)
selectFeed(nil, animated: true)
return
}
@ -1200,7 +1205,7 @@ private extension SceneCoordinator {
}
if unreadCountProvider.unreadCount > 0 {
selectFeed(prevIndexPath)
selectFeed(prevIndexPath, animated: true)
return true
}
@ -1306,7 +1311,7 @@ private extension SceneCoordinator {
}
if unreadCountProvider.unreadCount > 0 {
selectFeed(nextIndexPath)
selectFeed(nextIndexPath, animated: true)
return true
}
@ -1488,13 +1493,15 @@ private extension SceneCoordinator {
// MARK: Double Split
func installTimelineControllerIfNecessary(animated: Bool) {
isTimelineViewControllerPending = true
if navControllerForTimeline().viewControllers.filter({ $0 is MasterTimelineViewController }).count < 1 {
isTimelineViewControllerPending = true
masterTimelineViewController = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self)
masterTimelineViewController!.coordinator = self
navControllerForTimeline().pushViewController(masterTimelineViewController!, animated: animated)
masterTimelineViewController?.reloadArticles(animate: false)
}
}
@ -1589,7 +1596,7 @@ private extension SceneCoordinator {
subSplitViewController!.showDetailViewController(navController, sender: self)
masterFeedViewController.restoreSelectionIfNecessary(adjustScroll: true)
masterTimelineViewController!.restoreSelectionIfNecessary(adjustScroll: true)
masterTimelineViewController!.restoreSelectionIfNecessary(adjustScroll: false)
// We made sure this was there above when we called configureDoubleSplit
return subSplitViewController!
@ -1642,19 +1649,19 @@ private extension SceneCoordinator {
func handleSelectToday() {
if let indexPath = indexPathFor(SmartFeedsController.shared.todayFeed) {
selectFeed(indexPath)
selectFeed(indexPath, animated: false)
}
}
func handleSelectAllUnread() {
if let indexPath = indexPathFor(SmartFeedsController.shared.unreadFeed) {
selectFeed(indexPath)
selectFeed(indexPath, animated: false)
}
}
func handleSelectStarred() {
if let indexPath = indexPathFor(SmartFeedsController.shared.starredFeed) {
selectFeed(indexPath)
selectFeed(indexPath, animated: false)
}
}
@ -1663,7 +1670,7 @@ private extension SceneCoordinator {
return
}
if let indexPath = indexPathFor(folderNode) {
selectFeed(indexPath)
selectFeed(indexPath, animated: false)
}
}

View File

@ -38,6 +38,11 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate
let addViewController = navController.topViewController as! FeedbinAccountViewController
addViewController.delegate = self
present(navController, animated: true)
case 2:
let addAccount = OAuthAccountAuthorizationOperation(accountType: .feedly)
addAccount.delegate = self
addAccount.presentationAnchor = self.view.window!
OperationQueue.main.addOperation(addAccount)
default:
break
}
@ -48,3 +53,28 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate
}
}
extension AddAccountViewController: OAuthAccountAuthorizationOperationDelegate {
func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didCreate account: Account) {
let rootViewController = view.window?.rootViewController
account.refreshAll { result in
switch result {
case .success:
break
case .failure(let error):
guard let viewController = rootViewController else {
return
}
viewController.presentError(error)
}
}
dismiss()
}
func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didFailWith error: Error) {
presentError(error)
}
}

View File

@ -1,111 +0,0 @@
//
// RefreshIntervalViewController.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/25/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
class RefreshIntervalViewController: UITableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 7
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.adjustsFontForContentSizeCategory = true
let userRefreshInterval = AppDefaults.refreshInterval
switch indexPath.row {
case 0:
cell.textLabel?.text = RefreshInterval.manually.description()
if userRefreshInterval == RefreshInterval.manually {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
case 1:
cell.textLabel?.text = RefreshInterval.every10Minutes.description()
if userRefreshInterval == RefreshInterval.every10Minutes {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
case 2:
cell.textLabel?.text = RefreshInterval.every30Minutes.description()
if userRefreshInterval == RefreshInterval.every30Minutes {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
case 3:
cell.textLabel?.text = RefreshInterval.everyHour.description()
if userRefreshInterval == RefreshInterval.everyHour {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
case 4:
cell.textLabel?.text = RefreshInterval.every2Hours.description()
if userRefreshInterval == RefreshInterval.every2Hours {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
case 5:
cell.textLabel?.text = RefreshInterval.every4Hours.description()
if userRefreshInterval == RefreshInterval.every4Hours {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
default:
cell.textLabel?.text = RefreshInterval.every8Hours.description()
if userRefreshInterval == RefreshInterval.every8Hours {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let refreshInterval: RefreshInterval
switch indexPath.row {
case 0:
refreshInterval = RefreshInterval.manually
case 1:
refreshInterval = RefreshInterval.every10Minutes
case 2:
refreshInterval = RefreshInterval.every30Minutes
case 3:
refreshInterval = RefreshInterval.everyHour
case 4:
refreshInterval = RefreshInterval.every2Hours
case 5:
refreshInterval = RefreshInterval.every4Hours
default:
refreshInterval = RefreshInterval.every8Hours
}
AppDefaults.refreshInterval = refreshInterval
self.navigationController?.popViewController(animated: true)
}
}

View File

@ -60,32 +60,8 @@
</tableViewSection>
<tableViewSection headerTitle="Feeds" id="hAC-uA-RbS">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="qur-cL-wrM" detailTextLabel="qIl-N6-6wQ" style="IBUITableViewCellStyleValue1" id="z1J-VF-St0" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="255.5" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="z1J-VF-St0" id="Y8U-Ka-GeZ">
<rect key="frame" x="0.0" y="0.0" width="343" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Refresh Interval" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="qur-cL-wrM">
<rect key="frame" x="20" y="12" width="119.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="qIl-N6-6wQ" customClass="VibrantLabel" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="291" y="12" width="44" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="4Hg-B3-zAE" style="IBUITableViewCellStyleDefault" id="glf-Pg-s3P" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="299.5" width="374" height="44"/>
<rect key="frame" x="20" y="255.5" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="glf-Pg-s3P" id="bPA-43-Oqh">
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
@ -102,7 +78,7 @@
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="25J-iX-3at" style="IBUITableViewCellStyleDefault" id="qke-Ha-PXl" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="343.5" width="374" height="44"/>
<rect key="frame" x="20" y="299.5" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="qke-Ha-PXl" id="pZi-ck-RV5">
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
@ -119,7 +95,7 @@
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="dXN-Mw-yf2" style="IBUITableViewCellStyleDefault" id="F0L-Ut-reX" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="387.5" width="374" height="44"/>
<rect key="frame" x="20" y="343.5" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="F0L-Ut-reX" id="5SX-M2-2jR">
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
@ -140,7 +116,7 @@
<tableViewSection headerTitle="Timeline" id="9Pk-Y8-JVJ">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="MpA-w1-Wwh" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="487.5" width="374" height="44"/>
<rect key="frame" x="20" y="443.5" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="MpA-w1-Wwh" id="GhU-ib-Mz8">
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
@ -171,7 +147,7 @@
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="f7r-AZ-aDn" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="531.5" width="374" height="44"/>
<rect key="frame" x="20" y="487.5" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="f7r-AZ-aDn" id="KHC-cc-tOC">
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
@ -202,7 +178,7 @@
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="6C6-JQ-lfQ" style="IBUITableViewCellStyleDefault" id="5wo-fM-0l6" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="575.5" width="374" height="44"/>
<rect key="frame" x="20" y="531.5" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="5wo-fM-0l6" id="XAn-lK-LoN">
<rect key="frame" x="0.0" y="0.0" width="343" height="44"/>
@ -223,7 +199,7 @@
<tableViewSection headerTitle="About" id="TkH-4v-yhk">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="2o6-8W-nyK" style="IBUITableViewCellStyleDefault" id="he9-Ql-yfa" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="675.5" width="374" height="44"/>
<rect key="frame" x="20" y="631.5" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="he9-Ql-yfa" id="q6L-C8-H9a">
<rect key="frame" x="0.0" y="0.0" width="343" height="44"/>
@ -240,7 +216,7 @@
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="lOk-Dh-GfZ" style="IBUITableViewCellStyleDefault" id="GWZ-jk-qU6" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="719.5" width="374" height="44"/>
<rect key="frame" x="20" y="675.5" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="GWZ-jk-qU6" id="ZgS-bo-xDl">
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
@ -257,7 +233,7 @@
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="Pm8-6D-fdE" style="IBUITableViewCellStyleDefault" id="3cU-BG-6kK" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="763.5" width="374" height="44"/>
<rect key="frame" x="20" y="719.5" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="3cU-BG-6kK" id="Qm0-SY-0vx">
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
@ -274,7 +250,7 @@
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="TEA-EG-V6d" style="IBUITableViewCellStyleDefault" id="4yc-ig-I61" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="807.5" width="374" height="44"/>
<rect key="frame" x="20" y="763.5" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="4yc-ig-I61" id="uQl-VP-9p9">
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
@ -291,7 +267,7 @@
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="Q9a-Pi-uCc" style="IBUITableViewCellStyleDefault" id="mSW-A7-8lf" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="851.5" width="374" height="44"/>
<rect key="frame" x="20" y="807.5" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="mSW-A7-8lf" id="shF-ro-Zpx">
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
@ -308,7 +284,7 @@
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="dWz-1o-EpJ" style="IBUITableViewCellStyleDefault" id="2MG-qn-idJ" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="895.5" width="374" height="44"/>
<rect key="frame" x="0.0" y="851.5" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="2MG-qn-idJ" id="gP9-ry-keC">
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
@ -424,6 +400,39 @@
</constraints>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="56" id="zcM-qz-glk" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="129" width="374" height="56"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="zcM-qz-glk" id="3VG-Ax-7gi">
<rect key="frame" x="0.0" y="0.0" width="374" height="56"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="cXZ-17-bhe">
<rect key="frame" x="20" y="12" width="128" height="32"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="accountFeedly" translatesAutoresizingMaskIntoConstraints="NO" id="fAO-P0-gtD">
<rect key="frame" x="0.0" y="0.0" width="32" height="32"/>
<color key="tintColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
<constraints>
<constraint firstAttribute="width" constant="32" id="581-u2-SxX"/>
<constraint firstAttribute="height" constant="32" id="onv-oj-10a"/>
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Feedly" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="u2M-c5-ujy">
<rect key="frame" x="48" y="0.0" width="80" height="32"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle1"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
</subviews>
<constraints>
<constraint firstItem="cXZ-17-bhe" firstAttribute="leading" secondItem="3VG-Ax-7gi" secondAttribute="leading" constant="20" symbolic="YES" id="BYO-oH-a6T"/>
<constraint firstItem="cXZ-17-bhe" firstAttribute="centerY" secondItem="3VG-Ax-7gi" secondAttribute="centerY" id="r36-pZ-Siw"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
@ -441,35 +450,6 @@
</objects>
<point key="canvasLocation" x="983" y="151"/>
</scene>
<!--Refresh Interval-->
<scene sceneID="5WY-bu-OPU">
<objects>
<tableViewController storyboardIdentifier="RefreshIntervalViewController" title="Refresh Interval" useStoryboardIdentifierAsRestorationIdentifier="YES" id="Vd0-lF-iff" customClass="RefreshIntervalViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="KyE-ob-CYm">
<rect key="frame" x="0.0" y="0.0" width="414" height="808"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" id="91W-kj-0Dw" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="55.5" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="91W-kj-0Dw" id="AXy-Ti-xiS">
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
</tableViewCellContentView>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="Vd0-lF-iff" id="ZDd-4x-0M5"/>
<outlet property="delegate" destination="Vd0-lF-iff" id="3tH-oh-oZ3"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Refresh Interval" id="lIq-gS-6ui"/>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" translucent="NO" prompted="NO"/>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="PkF-Up-3qC" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1655" y="151"/>
</scene>
<!--About-->
<scene sceneID="pWd-ql-XAA">
<objects>
@ -647,7 +627,7 @@
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="kRt-nH-nOf" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2330" y="151"/>
<point key="canvasLocation" x="1680" y="151"/>
</scene>
<!--Timeline Layout-->
<scene sceneID="XRu-Jc-bbU">
@ -750,7 +730,7 @@
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iNo-Vj-YZx" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2995.6521739130435" y="150.66964285714286"/>
<point key="canvasLocation" x="2346" y="151"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="Ezn-Ny-zye">
@ -806,11 +786,12 @@
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Oq6-5f-Oa7" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="3697" y="151"/>
<point key="canvasLocation" x="3096" y="151"/>
</scene>
</scenes>
<resources>
<image name="accountFeedbin" width="120" height="102"/>
<image name="accountFeedly" width="138" height="123"/>
<image name="accountLocal" width="99" height="77"/>
<namedColor name="primaryAccentColor">
<color red="0.031372549019607843" green="0.41568627450980394" blue="0.93333333333333335" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>

View File

@ -104,16 +104,6 @@ class SettingsViewController: UITableViewController {
cell = acctCell
}
case 2:
if indexPath.row == 0 {
cell = tableView.dequeueReusableCell(withIdentifier: "SettingsTableViewCell", for: indexPath)
cell.textLabel?.text = NSLocalizedString("Refresh Interval", comment: "Refresh Interval")
cell.detailTextLabel?.text = AppDefaults.refreshInterval.description()
} else {
cell = super.tableView(tableView, cellForRowAt: indexPath)
}
default:
cell = super.tableView(tableView, cellForRowAt: indexPath)
@ -141,21 +131,18 @@ class SettingsViewController: UITableViewController {
case 2:
switch indexPath.row {
case 0:
let timeline = UIStoryboard.settings.instantiateController(ofType: RefreshIntervalViewController.self)
self.navigationController?.pushViewController(timeline, animated: true)
case 1:
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
if let sourceView = tableView.cellForRow(at: indexPath) {
let sourceRect = tableView.rectForRow(at: indexPath)
importOPML(sourceView: sourceView, sourceRect: sourceRect)
}
case 2:
case 1:
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
if let sourceView = tableView.cellForRow(at: indexPath) {
let sourceRect = tableView.rectForRow(at: indexPath)
exportOPML(sourceView: sourceView, sourceRect: sourceRect)
}
case 3:
case 2:
addFeed()
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
default:

@ -1 +1 @@
Subproject commit 972ff3237f819a2250e0bc1ca2814bafe328fa69
Subproject commit ba7bbb2ce10ee04a730c0a1e425a1b2e9d338520