commit
9d6c17e508
|
@ -16,7 +16,7 @@ public enum AccountError: LocalizedError {
|
||||||
case opmlImportInProgress
|
case opmlImportInProgress
|
||||||
case wrappedError(error: Error, account: Account)
|
case wrappedError(error: Error, account: Account)
|
||||||
|
|
||||||
public var acount: Account? {
|
public var account: Account? {
|
||||||
if case .wrappedError(_, let account) = self {
|
if case .wrappedError(_, let account) = self {
|
||||||
return account
|
return account
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -49,7 +49,7 @@ class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
os_log(.error, log: self.log, "Error occurred geting pending starred records: %@", error.localizedDescription)
|
os_log(.error, log: self.log, "Error occurred getting pending starred records: %@", error.localizedDescription)
|
||||||
completion(.failure(CloudKitZoneError.unknown))
|
completion(.failure(CloudKitZoneError.unknown))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import RSWeb
|
import RSWeb
|
||||||
|
|
||||||
// Combines the refresh progress of mutliple accounts into one struct,
|
// Combines the refresh progress of multiple accounts into one struct,
|
||||||
// for use by refresh status view and so on.
|
// for use by refresh status view and so on.
|
||||||
|
|
||||||
public struct CombinedRefreshProgress {
|
public struct CombinedRefreshProgress {
|
||||||
|
|
|
@ -20,7 +20,7 @@ struct FeedlyFeedResourceId: FeedlyResourceId {
|
||||||
let id: String
|
let id: String
|
||||||
|
|
||||||
/// The location of the kind of resource a concrete type represents.
|
/// The location of the kind of resource a concrete type represents.
|
||||||
/// If the conrete type cannot strip the resource type from the Id, it should just return the Id
|
/// If the concrete type cannot strip the resource type from the Id, it should just return the Id
|
||||||
/// since the Id is a legitimate URL.
|
/// since the Id is a legitimate URL.
|
||||||
/// This is basically assuming Feedly prefixes source feed URLs with `feed/`.
|
/// This is basically assuming Feedly prefixes source feed URLs with `feed/`.
|
||||||
/// It is not documented as such and could potentially change.
|
/// It is not documented as such and could potentially change.
|
||||||
|
|
|
@ -35,7 +35,7 @@ public protocol OAuthAcessTokenRefreshRequesting {
|
||||||
|
|
||||||
/// Access tokens expire. Perform a request for a fresh access token given the long life refresh token received when authorization was granted.
|
/// Access tokens expire. Perform a request for a fresh access token given the long life refresh token received when authorization was granted.
|
||||||
/// - Parameter refreshRequest: The refresh token and other information the authorization server requires to grant the client fresh access tokens on the user's behalf.
|
/// - Parameter refreshRequest: The refresh token and other information the authorization server requires to grant the client fresh access tokens on the user's behalf.
|
||||||
/// - Parameter completion: On success, the access token response appropriate for concrete type's service. Both the access and refresh token should be stored, preferrably on the Keychain. On failure, possibly a `URLError` or `OAuthAuthorizationErrorResponse` value.
|
/// - Parameter completion: On success, the access token response appropriate for concrete type's service. Both the access and refresh token should be stored, preferably on the Keychain. On failure, possibly a `URLError` or `OAuthAuthorizationErrorResponse` value.
|
||||||
func refreshAccessToken(_ refreshRequest: OAuthRefreshAccessTokenRequest, completion: @escaping (Result<AccessTokenResponse, Error>) -> ())
|
func refreshAccessToken(_ refreshRequest: OAuthRefreshAccessTokenRequest, completion: @escaping (Result<AccessTokenResponse, Error>) -> ())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ extension OAuthAuthorizationClient {
|
||||||
|
|
||||||
static var feedlyCloudClient: OAuthAuthorizationClient {
|
static var feedlyCloudClient: OAuthAuthorizationClient {
|
||||||
/// Models private NetNewsWire client secrets.
|
/// Models private NetNewsWire client secrets.
|
||||||
/// These placeholders are substitued at build time using a Run Script phase with build settings.
|
/// These placeholders are substituted 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
|
/// https://developer.feedly.com/v3/auth/#authenticating-a-user-and-obtaining-an-auth-code
|
||||||
return OAuthAuthorizationClient(id: SecretsManager.provider.feedlyClientId,
|
return OAuthAuthorizationClient(id: SecretsManager.provider.feedlyClientId,
|
||||||
redirectUri: "netnewswire://auth/feedly",
|
redirectUri: "netnewswire://auth/feedly",
|
||||||
|
|
|
@ -131,7 +131,7 @@ public struct OAuthAccessTokenRequest: Encodable {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Models the minimum subset of properties of a response in section 4.1.4 of the OAuth 2.0 Authorization Framework
|
/// Models the minimum subset of properties of a response in section 4.1.4 of the OAuth 2.0 Authorization Framework
|
||||||
/// Concrete types model other paramters beyond the scope of the OAuth spec.
|
/// Concrete types model other parameters beyond the scope of the OAuth spec.
|
||||||
/// For example, Feedly provides the ID of the user who has consented to the grant.
|
/// For example, Feedly provides the ID of the user who has consented to the grant.
|
||||||
/// https://tools.ietf.org/html/rfc6749#section-4.1.4
|
/// https://tools.ietf.org/html/rfc6749#section-4.1.4
|
||||||
public protocol OAuthAccessTokenResponse {
|
public protocol OAuthAccessTokenResponse {
|
||||||
|
|
|
@ -81,7 +81,7 @@ final class FeedlyCreateFeedsForCollectionFoldersOperation: FeedlyOperation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// no exsiting feed, create a new one
|
// no existing feed, create a new one
|
||||||
let parser = FeedlyFeedParser(feed: collectionFeed)
|
let parser = FeedlyFeedParser(feed: collectionFeed)
|
||||||
let feed = account.createWebFeed(with: parser.title,
|
let feed = account.createWebFeed(with: parser.title,
|
||||||
url: parser.url,
|
url: parser.url,
|
||||||
|
|
|
@ -61,7 +61,7 @@ class FeedlyGetStreamContentsOperationTests: XCTestCase {
|
||||||
service.mockResult = .success(mockStream)
|
service.mockResult = .success(mockStream)
|
||||||
service.getStreamContentsExpectation = expectation(description: "Did Call Service")
|
service.getStreamContentsExpectation = expectation(description: "Did Call Service")
|
||||||
service.parameterTester = { serviceResource, serviceContinuation, serviceNewerThan, serviceUnreadOnly in
|
service.parameterTester = { serviceResource, serviceContinuation, serviceNewerThan, serviceUnreadOnly in
|
||||||
// Verify these values given to the opeartion are passed to the service.
|
// Verify these values given to the operation are passed to the service.
|
||||||
XCTAssertEqual(serviceResource.id, resource.id)
|
XCTAssertEqual(serviceResource.id, resource.id)
|
||||||
XCTAssertEqual(serviceContinuation, continuation)
|
XCTAssertEqual(serviceContinuation, continuation)
|
||||||
XCTAssertEqual(serviceNewerThan, newerThan)
|
XCTAssertEqual(serviceNewerThan, newerThan)
|
||||||
|
|
|
@ -61,7 +61,7 @@ class FeedlyGetStreamIdsOperationTests: XCTestCase {
|
||||||
service.mockResult = .success(mockStreamIds)
|
service.mockResult = .success(mockStreamIds)
|
||||||
service.getStreamIdsExpectation = expectation(description: "Did Call Service")
|
service.getStreamIdsExpectation = expectation(description: "Did Call Service")
|
||||||
service.parameterTester = { serviceResource, serviceContinuation, serviceNewerThan, serviceUnreadOnly in
|
service.parameterTester = { serviceResource, serviceContinuation, serviceNewerThan, serviceUnreadOnly in
|
||||||
// Verify these values given to the opeartion are passed to the service.
|
// Verify these values given to the operation are passed to the service.
|
||||||
XCTAssertEqual(serviceResource.id, resource.id)
|
XCTAssertEqual(serviceResource.id, resource.id)
|
||||||
XCTAssertEqual(serviceContinuation, continuation)
|
XCTAssertEqual(serviceContinuation, continuation)
|
||||||
XCTAssertEqual(serviceNewerThan, newerThan)
|
XCTAssertEqual(serviceNewerThan, newerThan)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
-- This script creates an Excel spreadsheet with statistics about all the feeds in your NetNewsWire
|
-- This script creates an Excel spreadsheet with statistics about all the feeds in your NetNewsWire
|
||||||
|
|
||||||
-- the exportToExcel() function creats a single line of data in a spreadsheet
|
-- the exportToExcel() function creates a single line of data in a spreadsheet
|
||||||
|
|
||||||
to exportToExcel(docname, rowIndex, feedname, numArticles, numStars, numRead)
|
to exportToExcel(docname, rowIndex, feedname, numArticles, numStars, numRead)
|
||||||
tell application "Microsoft Excel"
|
tell application "Microsoft Excel"
|
||||||
|
|
|
@ -1408,7 +1408,7 @@ private extension MainWindowController {
|
||||||
let sidebarWidth: Int = widths[0]
|
let sidebarWidth: Int = widths[0]
|
||||||
let timelineWidth: Int = widths[1]
|
let timelineWidth: Int = widths[1]
|
||||||
|
|
||||||
// Make sure the detail view has its mimimum thickness, at least.
|
// Make sure the detail view has its minimum thickness, at least.
|
||||||
if windowWidth < sidebarWidth + dividerThickness + timelineWidth + dividerThickness + MainWindowController.detailViewMinimumThickness {
|
if windowWidth < sidebarWidth + dividerThickness + timelineWidth + dividerThickness + MainWindowController.detailViewMinimumThickness {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,7 +90,7 @@ function messageHandler(event) {
|
||||||
|
|
||||||
// There is a bug in Safari where the messageHandler is apparently held on to by Safari
|
// There is a bug in Safari where the messageHandler is apparently held on to by Safari
|
||||||
// even after an extension is disabled. So an effort to "ping" an extension's scripts will
|
// even after an extension is disabled. So an effort to "ping" an extension's scripts will
|
||||||
// succeed even if its been disabled and the page reloaded. Checking for the existance of
|
// succeed even if its been disabled and the page reloaded. Checking for the existence of
|
||||||
// document.location seems to ensure we have enough of a handle still on the document that
|
// document.location seems to ensure we have enough of a handle still on the document that
|
||||||
// we can do something useful with it.
|
// we can do something useful with it.
|
||||||
var shouldValidate = (document.location != null) && (thisPageLinkObjects.length > 0);
|
var shouldValidate = (document.location != null) && (thisPageLinkObjects.length > 0);
|
||||||
|
|
|
@ -156,7 +156,7 @@ typedef void (^OnePasswordExtensionItemCompletionBlock)(NSExtensionItem * __null
|
||||||
|
|
||||||
@param loginDetailsDictionary about the Login to be saved, including old password and the username, are stored in an dictionary and given to the 1Password Extension.
|
@param loginDetailsDictionary about the Login to be saved, including old password and the username, are stored in an dictionary and given to the 1Password Extension.
|
||||||
|
|
||||||
@param passwordGenerationOptions The Password generator options epresented in a dictionary form.
|
@param passwordGenerationOptions The Password generator options represented in a dictionary form.
|
||||||
|
|
||||||
@param viewController The view controller from which the 1Password Extension is invoked. Usually `self`
|
@param viewController The view controller from which the 1Password Extension is invoked. Usually `self`
|
||||||
|
|
||||||
|
@ -189,7 +189,7 @@ typedef void (^OnePasswordExtensionItemCompletionBlock)(NSExtensionItem * __null
|
||||||
/*!
|
/*!
|
||||||
Called in the UIActivityViewController completion block to find out whether or not the user selected the 1Password Extension activity.
|
Called in the UIActivityViewController completion block to find out whether or not the user selected the 1Password Extension activity.
|
||||||
|
|
||||||
@param activityType or the bundle identidier of the selected activity in the share sheet.
|
@param activityType or the bundle identifier of the selected activity in the share sheet.
|
||||||
|
|
||||||
@return isOnePasswordExtensionActivityType Returns YES if the selected activity is the 1Password extension, NO otherwise.
|
@return isOnePasswordExtensionActivityType Returns YES if the selected activity is the 1Password extension, NO otherwise.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -112,12 +112,12 @@ class ActivityManager {
|
||||||
|
|
||||||
if let folders = account.folders {
|
if let folders = account.folders {
|
||||||
for folder in folders {
|
for folder in folders {
|
||||||
ids.append(identifer(for: folder))
|
ids.append(identifier(for: folder))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for webFeed in account.flattenedWebFeeds() {
|
for webFeed in account.flattenedWebFeeds() {
|
||||||
ids.append(contentsOf: identifers(for: webFeed))
|
ids.append(contentsOf: identifiers(for: webFeed))
|
||||||
}
|
}
|
||||||
|
|
||||||
CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: ids)
|
CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: ids)
|
||||||
|
@ -125,17 +125,17 @@ class ActivityManager {
|
||||||
|
|
||||||
static func cleanUp(_ folder: Folder) {
|
static func cleanUp(_ folder: Folder) {
|
||||||
var ids = [String]()
|
var ids = [String]()
|
||||||
ids.append(identifer(for: folder))
|
ids.append(identifier(for: folder))
|
||||||
|
|
||||||
for webFeed in folder.flattenedWebFeeds() {
|
for webFeed in folder.flattenedWebFeeds() {
|
||||||
ids.append(contentsOf: identifers(for: webFeed))
|
ids.append(contentsOf: identifiers(for: webFeed))
|
||||||
}
|
}
|
||||||
|
|
||||||
CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: ids)
|
CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func cleanUp(_ webFeed: WebFeed) {
|
static func cleanUp(_ webFeed: WebFeed) {
|
||||||
CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: identifers(for: webFeed))
|
CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: identifiers(for: webFeed))
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@ -201,7 +201,7 @@ private extension ActivityManager {
|
||||||
|
|
||||||
activity.isEligibleForHandoff = true
|
activity.isEligibleForHandoff = true
|
||||||
|
|
||||||
activity.persistentIdentifier = ActivityManager.identifer(for: article)
|
activity.persistentIdentifier = ActivityManager.identifier(for: article)
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
activity.keywords = Set(makeKeywords(article))
|
activity.keywords = Set(makeKeywords(article))
|
||||||
|
@ -222,7 +222,7 @@ private extension ActivityManager {
|
||||||
attributeSet.title = ArticleStringFormatter.truncatedTitle(article)
|
attributeSet.title = ArticleStringFormatter.truncatedTitle(article)
|
||||||
attributeSet.contentDescription = article.summary
|
attributeSet.contentDescription = article.summary
|
||||||
attributeSet.keywords = makeKeywords(article)
|
attributeSet.keywords = makeKeywords(article)
|
||||||
attributeSet.relatedUniqueIdentifier = ActivityManager.identifer(for: article)
|
attributeSet.relatedUniqueIdentifier = ActivityManager.identifier(for: article)
|
||||||
|
|
||||||
if let iconImage = article.iconImage() {
|
if let iconImage = article.iconImage() {
|
||||||
attributeSet.thumbnailData = iconImage.image.pngData()
|
attributeSet.thumbnailData = iconImage.image.pngData()
|
||||||
|
@ -249,7 +249,7 @@ private extension ActivityManager {
|
||||||
let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeItem as String)
|
let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeItem as String)
|
||||||
attributeSet.title = feed.nameForDisplay
|
attributeSet.title = feed.nameForDisplay
|
||||||
attributeSet.keywords = makeKeywords(feed.nameForDisplay)
|
attributeSet.keywords = makeKeywords(feed.nameForDisplay)
|
||||||
attributeSet.relatedUniqueIdentifier = ActivityManager.identifer(for: feed)
|
attributeSet.relatedUniqueIdentifier = ActivityManager.identifier(for: feed)
|
||||||
|
|
||||||
if let iconImage = IconImageCache.shared.imageForFeed(feed) {
|
if let iconImage = IconImageCache.shared.imageForFeed(feed) {
|
||||||
attributeSet.thumbnailData = iconImage.image.dataRepresentation()
|
attributeSet.thumbnailData = iconImage.image.dataRepresentation()
|
||||||
|
@ -273,24 +273,24 @@ private extension ActivityManager {
|
||||||
activity.becomeCurrent()
|
activity.becomeCurrent()
|
||||||
}
|
}
|
||||||
|
|
||||||
static func identifer(for folder: Folder) -> String {
|
static func identifier(for folder: Folder) -> String {
|
||||||
return "account_\(folder.account!.accountID)_folder_\(folder.nameForDisplay)"
|
return "account_\(folder.account!.accountID)_folder_\(folder.nameForDisplay)"
|
||||||
}
|
}
|
||||||
|
|
||||||
static func identifer(for feed: WebFeed) -> String {
|
static func identifier(for feed: WebFeed) -> String {
|
||||||
return "account_\(feed.account!.accountID)_feed_\(feed.webFeedID)"
|
return "account_\(feed.account!.accountID)_feed_\(feed.webFeedID)"
|
||||||
}
|
}
|
||||||
|
|
||||||
static func identifer(for article: Article) -> String {
|
static func identifier(for article: Article) -> String {
|
||||||
return "account_\(article.accountID)_feed_\(article.webFeedID)_article_\(article.articleID)"
|
return "account_\(article.accountID)_feed_\(article.webFeedID)_article_\(article.articleID)"
|
||||||
}
|
}
|
||||||
|
|
||||||
static func identifers(for feed: WebFeed) -> [String] {
|
static func identifiers(for feed: WebFeed) -> [String] {
|
||||||
var ids = [String]()
|
var ids = [String]()
|
||||||
ids.append(identifer(for: feed))
|
ids.append(identifier(for: feed))
|
||||||
if let articles = try? feed.fetchArticles() {
|
if let articles = try? feed.fetchArticles() {
|
||||||
for article in articles {
|
for article in articles {
|
||||||
ids.append(identifer(for: article))
|
ids.append(identifier(for: article))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -69,7 +69,7 @@ public class ArticleThemeDownloader {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Performs a deep search of the unzipped direcotry to find the theme file.
|
/// Performs a deep search of the unzipped directory to find the theme file.
|
||||||
/// - Parameter searchPath: directory to search
|
/// - Parameter searchPath: directory to search
|
||||||
/// - Returns: optional `String`
|
/// - Returns: optional `String`
|
||||||
private func findThemeFile(in searchPath: String) -> String? {
|
private func findThemeFile(in searchPath: String) -> String? {
|
||||||
|
@ -84,13 +84,13 @@ public class ArticleThemeDownloader {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The download directory used by the theme downloader: `Application Suppport/NetNewsWire/Downloads`
|
/// The download directory used by the theme downloader: `Application Support/NetNewsWire/Downloads`
|
||||||
/// - Returns: `URL`
|
/// - Returns: `URL`
|
||||||
private func downloadDirectory() -> URL {
|
private func downloadDirectory() -> URL {
|
||||||
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("NetNewsWire/Downloads", isDirectory: true)
|
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("NetNewsWire/Downloads", isDirectory: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes downloaded themes, where themes == folders, from `Application Suppport/NetNewsWire/Downloads`.
|
/// Removes downloaded themes, where themes == folders, from `Application Support/NetNewsWire/Downloads`.
|
||||||
public func cleanUp() {
|
public func cleanUp() {
|
||||||
guard let filenames = try? FileManager.default.contentsOfDirectory(atPath: downloadDirectory().path) else {
|
guard let filenames = try? FileManager.default.contentsOfDirectory(atPath: downloadDirectory().path) else {
|
||||||
return
|
return
|
||||||
|
|
|
@ -7,11 +7,11 @@ uses `xcodebuild` to build the project after syncing the repository and the vari
|
||||||
The build itself focuses on the scheme NetNewsWire for macOS and NetNewsWire-iOS for iOS. Also it leverages the
|
The build itself focuses on the scheme NetNewsWire for macOS and NetNewsWire-iOS for iOS. Also it leverages the
|
||||||
`NetNewsWire.xcworkspace` configuration.
|
`NetNewsWire.xcworkspace` configuration.
|
||||||
|
|
||||||
Private keys, certificates and provisioning profiles are stored in Github under `buildscripts` folder. Decrypting neccessary certificates, copy to build machine keychain and delete the certificates are handled by the [`buildscripts/ci-build.sh`](https://github.com/Ranchero-Software/NetNewsWire/blob/main/buildscripts/ci-build.sh) script.
|
Private keys, certificates and provisioning profiles are stored in Github under `buildscripts` folder. Decrypting necessary certificates, copy to build machine keychain and delete the certificates are handled by the [`buildscripts/ci-build.sh`](https://github.com/Ranchero-Software/NetNewsWire/blob/main/buildscripts/ci-build.sh) script.
|
||||||
|
|
||||||
Each submodule also has it's own CI configuration, which are set up and built from
|
Each submodule also has it's own CI configuration, which are set up and built from
|
||||||
their own repositories. The submodule CI systems are entirely independent so that
|
their own repositories. The submodule CI systems are entirely independent so that
|
||||||
those libraries can grow and change, getting CI verification, indepdent of NetNewsWire.
|
those libraries can grow and change, getting CI verification, independent of NetNewsWire.
|
||||||
|
|
||||||
Build failures are notified to our slack group via [Notify Slack](https://github.com/8398a7/action-slack) GitHub action.
|
Build failures are notified to our slack group via [Notify Slack](https://github.com/8398a7/action-slack) GitHub action.
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ class AppleScriptXCTestCase: XCTestCase {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@function doIndividualScript
|
@function doIndividualScript
|
||||||
@param filename -- name of a .applescript (sans extention) in the test bundle's
|
@param filename -- name of a .applescript (sans extension) in the test bundle's
|
||||||
Resources/TestScripts directory
|
Resources/TestScripts directory
|
||||||
@brief given a file, loads the script and runs it. Expects a result from running
|
@brief given a file, loads the script and runs it. Expects a result from running
|
||||||
the script of the form
|
the script of the form
|
||||||
|
|
|
@ -76,7 +76,7 @@ class ScriptingTests: AppleScriptXCTestCase {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@function testCurrentArticleScripts
|
@function testCurrentArticleScripts
|
||||||
@brief the pices of the test are broken up into smaller pieces because of the
|
@brief the pieces of the test are broken up into smaller pieces because of the
|
||||||
way events are delivered to the app -- I tried one single script with all the
|
way events are delivered to the app -- I tried one single script with all the
|
||||||
actions and the keystrokes aren't delivered to the app right away, so the ui
|
actions and the keystrokes aren't delivered to the app right away, so the ui
|
||||||
isn't updated in time for 'current article' to be set. But, breaking them up
|
isn't updated in time for 'current article' to be set. But, breaking them up
|
||||||
|
@ -85,7 +85,7 @@ class ScriptingTests: AppleScriptXCTestCase {
|
||||||
July 30, 2019: There's an issue where in order for a script to send keystrokes,
|
July 30, 2019: There's an issue where in order for a script to send keystrokes,
|
||||||
The app has to be allowed to interact with the SystemEvents.app in
|
The app has to be allowed to interact with the SystemEvents.app in
|
||||||
System Preferences -> Security & Privacy -> Privacy -> Accessibility
|
System Preferences -> Security & Privacy -> Privacy -> Accessibility
|
||||||
and this premission needs to be renewed every time the app is recompiled unless
|
and this permission needs to be renewed every time the app is recompiled unless
|
||||||
the app is codesigned. Until we figure out how to get around this limitation,
|
the app is codesigned. Until we figure out how to get around this limitation,
|
||||||
this test is disabled.
|
this test is disabled.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -288,7 +288,7 @@ private extension AppDelegate {
|
||||||
|
|
||||||
func waitToComplete(completion: @escaping (Bool) -> Void) {
|
func waitToComplete(completion: @escaping (Bool) -> Void) {
|
||||||
guard UIApplication.shared.applicationState == .background else {
|
guard UIApplication.shared.applicationState == .background else {
|
||||||
os_log("App came back to forground, no longer waiting.", log: self.log, type: .info)
|
os_log("App came back to foreground, no longer waiting.", log: self.log, type: .info)
|
||||||
completion(false)
|
completion(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,14 +23,14 @@ open class ImageScrollView: UIScrollView {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc public enum Offset: Int {
|
@objc public enum Offset: Int {
|
||||||
case begining
|
case beginning
|
||||||
case center
|
case center
|
||||||
}
|
}
|
||||||
|
|
||||||
static let kZoomInFactorFromMinWhenDoubleTap: CGFloat = 2
|
static let kZoomInFactorFromMinWhenDoubleTap: CGFloat = 2
|
||||||
|
|
||||||
@objc open var imageContentMode: ScaleMode = .widthFill
|
@objc open var imageContentMode: ScaleMode = .widthFill
|
||||||
@objc open var initialOffset: Offset = .begining
|
@objc open var initialOffset: Offset = .beginning
|
||||||
|
|
||||||
@objc public private(set) var zoomView: UIImageView? = nil
|
@objc public private(set) var zoomView: UIImageView? = nil
|
||||||
|
|
||||||
|
@ -201,7 +201,7 @@ open class ImageScrollView: UIScrollView {
|
||||||
zoomScale = minimumZoomScale
|
zoomScale = minimumZoomScale
|
||||||
|
|
||||||
switch initialOffset {
|
switch initialOffset {
|
||||||
case .begining:
|
case .beginning:
|
||||||
contentOffset = CGPoint.zero
|
contentOffset = CGPoint.zero
|
||||||
case .center:
|
case .center:
|
||||||
let xOffset = contentSize.width < bounds.width ? 0 : (contentSize.width - bounds.width)/2
|
let xOffset = contentSize.width < bounds.width ? 0 : (contentSize.width - bounds.width)/2
|
||||||
|
|
|
@ -424,7 +424,7 @@ extension WebViewController: WKUIDelegate {
|
||||||
|
|
||||||
func webView(_ webView: WKWebView, contextMenuForElement elementInfo: WKContextMenuElementInfo, willCommitWithAnimator animator: UIContextMenuInteractionCommitAnimating) {
|
func webView(_ webView: WKWebView, contextMenuForElement elementInfo: WKContextMenuElementInfo, willCommitWithAnimator animator: UIContextMenuInteractionCommitAnimating) {
|
||||||
// We need to have at least an unimplemented WKUIDelegate assigned to the WKWebView. This makes the
|
// We need to have at least an unimplemented WKUIDelegate assigned to the WKWebView. This makes the
|
||||||
// link preview launch Safari when the link preview is tapped. In theory, you shoud be able to get
|
// link preview launch Safari when the link preview is tapped. In theory, you should be able to get
|
||||||
// the link from the elementInfo above and transition to SFSafariViewController instead of launching
|
// the link from the elementInfo above and transition to SFSafariViewController instead of launching
|
||||||
// Safari. As the time of this writing, the link in elementInfo is always nil. ¯\_(ツ)_/¯
|
// Safari. As the time of this writing, the link in elementInfo is always nil. ¯\_(ツ)_/¯
|
||||||
}
|
}
|
||||||
|
|
|
@ -1319,7 +1319,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
||||||
|
|
||||||
/// This will dismiss the foremost view controller if the user
|
/// This will dismiss the foremost view controller if the user
|
||||||
/// has launched from an external action (i.e., a widget tap, or
|
/// has launched from an external action (i.e., a widget tap, or
|
||||||
/// selecting an artice via a notification).
|
/// selecting an article via a notification).
|
||||||
///
|
///
|
||||||
/// The dismiss is only applicable if the view controller is a
|
/// The dismiss is only applicable if the view controller is a
|
||||||
/// `SFSafariViewController` or `SettingsViewController`,
|
/// `SFSafariViewController` or `SettingsViewController`,
|
||||||
|
@ -1402,7 +1402,7 @@ extension SceneCoordinator: UINavigationControllerDelegate {
|
||||||
// If we are using a phone and navigate away from the detail, clear up the article resources (including activity).
|
// If we are using a phone and navigate away from the detail, clear up the article resources (including activity).
|
||||||
// Don't clear it if we have pushed an ArticleViewController, but don't yet see it on the navigation stack.
|
// Don't clear it if we have pushed an ArticleViewController, but don't yet see it on the navigation stack.
|
||||||
// This happens when we are going to the next unread and we need to grab another timeline to continue. The
|
// This happens when we are going to the next unread and we need to grab another timeline to continue. The
|
||||||
// ArticleViewController will be pushed, but we will breifly show the Timeline. Don't clear things out when that happens.
|
// ArticleViewController will be pushed, but we will briefly show the Timeline. Don't clear things out when that happens.
|
||||||
if viewController === masterTimelineViewController && !isThreePanelMode && rootSplitViewController.isCollapsed && !isArticleViewControllerPending {
|
if viewController === masterTimelineViewController && !isThreePanelMode && rootSplitViewController.isCollapsed && !isArticleViewControllerPending {
|
||||||
currentArticle = nil
|
currentArticle = nil
|
||||||
masterTimelineViewController?.updateArticleSelection(animations: [.scroll, .select, .navigation])
|
masterTimelineViewController?.updateArticleSelection(animations: [.scroll, .select, .navigation])
|
||||||
|
|
|
@ -60,7 +60,7 @@ private extension UIViewController {
|
||||||
let title = NSLocalizedString("Account Error", comment: "Account Error")
|
let title = NSLocalizedString("Account Error", comment: "Account Error")
|
||||||
let alertController = UIAlertController(title: title, message: error.localizedDescription, preferredStyle: .alert)
|
let alertController = UIAlertController(title: title, message: error.localizedDescription, preferredStyle: .alert)
|
||||||
|
|
||||||
if error.acount?.type == .feedbin {
|
if error.account?.type == .feedbin {
|
||||||
|
|
||||||
let credentialsTitle = NSLocalizedString("Update Credentials", comment: "Update Credentials")
|
let credentialsTitle = NSLocalizedString("Update Credentials", comment: "Update Credentials")
|
||||||
let credentialsAction = UIAlertAction(title: credentialsTitle, style: .default) { [weak self] _ in
|
let credentialsAction = UIAlertAction(title: credentialsTitle, style: .default) { [weak self] _ in
|
||||||
|
@ -69,7 +69,7 @@ private extension UIViewController {
|
||||||
let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "FeedbinAccountNavigationViewController") as! UINavigationController
|
let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "FeedbinAccountNavigationViewController") as! UINavigationController
|
||||||
navController.modalPresentationStyle = .formSheet
|
navController.modalPresentationStyle = .formSheet
|
||||||
let addViewController = navController.topViewController as! FeedbinAccountViewController
|
let addViewController = navController.topViewController as! FeedbinAccountViewController
|
||||||
addViewController.account = error.acount
|
addViewController.account = error.account
|
||||||
self?.present(navController, animated: true)
|
self?.present(navController, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue