diff --git a/Account/Sources/Account/FeedFinder/FeedFinder.swift b/Account/Sources/Account/FeedFinder/FeedFinder.swift index d743b629a..e81c5160c 100644 --- a/Account/Sources/Account/FeedFinder/FeedFinder.swift +++ b/Account/Sources/Account/FeedFinder/FeedFinder.swift @@ -20,7 +20,7 @@ class FeedFinder { if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), urlComponents.host == "micro.blog" { urlComponents.path = "\(urlComponents.path).json" if let newURLString = urlComponents.url?.absoluteString { - let microblogFeedSpecifier = FeedSpecifier(title: nil, urlString: newURLString, source: .HTMLLink) + let microblogFeedSpecifier = FeedSpecifier(title: nil, urlString: newURLString, source: .HTMLLink, orderFound: 1) completion(.success(Set([microblogFeedSpecifier]))) } } else { @@ -45,7 +45,7 @@ class FeedFinder { } if FeedFinder.isFeed(data, url.absoluteString) { - let feedSpecifier = FeedSpecifier(title: nil, urlString: url.absoluteString, source: .UserEntered) + let feedSpecifier = FeedSpecifier(title: nil, urlString: url.absoluteString, source: .UserEntered, orderFound: 1) completion(.success(Set([feedSpecifier]))) return } @@ -120,11 +120,11 @@ private extension FeedFinder { // It’s also fairly common for /index.xml to work. if let url = URL(string: urlString) { let feedURL = url.appendingPathComponent("feed", isDirectory: true) - let wordpressFeedSpecifier = FeedSpecifier(title: nil, urlString: feedURL.absoluteString, source: .HTMLLink) + let wordpressFeedSpecifier = FeedSpecifier(title: nil, urlString: feedURL.absoluteString, source: .HTMLLink, orderFound: 1) feedSpecifiers.insert(wordpressFeedSpecifier) let indexXMLURL = url.appendingPathComponent("index.xml", isDirectory: false) - let indexXMLFeedSpecifier = FeedSpecifier(title: nil, urlString: indexXMLURL.absoluteString, source: .HTMLLink) + let indexXMLFeedSpecifier = FeedSpecifier(title: nil, urlString: indexXMLURL.absoluteString, source: .HTMLLink, orderFound: 1) feedSpecifiers.insert(indexXMLFeedSpecifier) } } diff --git a/Account/Sources/Account/FeedFinder/FeedSpecifier.swift b/Account/Sources/Account/FeedFinder/FeedSpecifier.swift index 5b48b1b98..4487065bb 100644 --- a/Account/Sources/Account/FeedFinder/FeedSpecifier.swift +++ b/Account/Sources/Account/FeedFinder/FeedSpecifier.swift @@ -21,6 +21,7 @@ struct FeedSpecifier: Hashable { public let title: String? public let urlString: String public let source: Source + public let orderFound: Int public var score: Int { return calculatedScore() } @@ -30,8 +31,9 @@ struct FeedSpecifier: Hashable { let mergedTitle = title ?? feedSpecifier.title let mergedSource = source.equalToOrBetterThan(feedSpecifier.source) ? source : feedSpecifier.source + let mergedOrderFound = orderFound < feedSpecifier.orderFound ? orderFound : feedSpecifier.orderFound - return FeedSpecifier(title: mergedTitle, urlString: urlString, source: mergedSource) + return FeedSpecifier(title: mergedTitle, urlString: urlString, source: mergedSource, orderFound: mergedOrderFound) } public static func bestFeed(in feedSpecifiers: Set) -> FeedSpecifier? { @@ -69,6 +71,8 @@ private extension FeedSpecifier { score = score + 50 } + score = score - ((orderFound - 1) * 5) + if urlString.caseInsensitiveContains("comments") { score = score - 10 } diff --git a/Account/Sources/Account/FeedFinder/HTMLFeedFinder.swift b/Account/Sources/Account/FeedFinder/HTMLFeedFinder.swift index 1592afa66..7e776f6c0 100644 --- a/Account/Sources/Account/FeedFinder/HTMLFeedFinder.swift +++ b/Account/Sources/Account/FeedFinder/HTMLFeedFinder.swift @@ -21,22 +21,24 @@ class HTMLFeedFinder { init(parserData: ParserData) { let metadata = RSHTMLMetadataParser.htmlMetadata(with: parserData) - + var orderFound = 0 + for oneFeedLink in metadata.feedLinks { if let oneURLString = oneFeedLink.urlString?.normalizedURL { - let oneFeedSpecifier = FeedSpecifier(title: oneFeedLink.title, urlString: oneURLString, source: .HTMLHead) + orderFound = orderFound + 1 + let oneFeedSpecifier = FeedSpecifier(title: oneFeedLink.title, urlString: oneURLString, source: .HTMLHead, orderFound: orderFound) addFeedSpecifier(oneFeedSpecifier) } } let bodyLinks = RSHTMLLinkParser.htmlLinks(with: parserData) - for oneBodyLink in bodyLinks { - - if linkMightBeFeed(oneBodyLink), let normalizedURL = oneBodyLink.urlString?.normalizedURL { - let oneFeedSpecifier = FeedSpecifier(title: oneBodyLink.text, urlString: normalizedURL, source: .HTMLLink) - addFeedSpecifier(oneFeedSpecifier) - } + for oneBodyLink in bodyLinks { + if linkMightBeFeed(oneBodyLink), let normalizedURL = oneBodyLink.urlString?.normalizedURL { + orderFound = orderFound + 1 + let oneFeedSpecifier = FeedSpecifier(title: oneBodyLink.text, urlString: normalizedURL, source: .HTMLLink, orderFound: orderFound) + addFeedSpecifier(oneFeedSpecifier) } + } } } diff --git a/Account/Sources/Account/Feedbin/FeedbinAPICaller.swift b/Account/Sources/Account/Feedbin/FeedbinAPICaller.swift index f5bd56477..8ff2028ae 100644 --- a/Account/Sources/Account/Feedbin/FeedbinAPICaller.swift +++ b/Account/Sources/Account/Feedbin/FeedbinAPICaller.swift @@ -34,6 +34,7 @@ final class FeedbinAPICaller: NSObject { private let feedbinBaseURL = URL(string: "https://api.feedbin.com/v2/")! private var transport: Transport! private var suspended = false + private var lastBackdateStartTime: Date? var credentials: Credentials? weak var accountMetadata: AccountMetadata? @@ -486,10 +487,26 @@ final class FeedbinAPICaller: NSObject { } func retrieveEntries(completion: @escaping (Result<([FeedbinEntry]?, String?, Date?, Int?), Error>) -> Void) { - + + // If this is an initial sync, go and grab the previous 3 months of entries. If not, use the last + // article fetch to only get the articles **published** since the last article fetch. + // + // We do a backdate fetch every launch or every 24 hours. This will help with + // getting **updated** articles that normally wouldn't be found with a regular fetch. + // https://github.com/Ranchero-Software/NetNewsWire/issues/2549#issuecomment-722341356 let since: Date = { if let lastArticleFetch = accountMetadata?.lastArticleFetchStartTime { - return lastArticleFetch + if let lastBackdateStartTime = lastBackdateStartTime { + if lastBackdateStartTime.byAdding(days: 1) < lastArticleFetch { + self.lastBackdateStartTime = lastArticleFetch + return lastArticleFetch.bySubtracting(days: 1) + } else { + return lastArticleFetch + } + } else { + self.lastBackdateStartTime = lastArticleFetch + return lastArticleFetch.bySubtracting(days: 1) + } } else { return Calendar.current.date(byAdding: .month, value: -3, to: Date()) ?? Date() } diff --git a/Account/Sources/Account/Feedbin/FeedbinAccountDelegate.swift b/Account/Sources/Account/Feedbin/FeedbinAccountDelegate.swift index d934b0162..4b51cc5c6 100644 --- a/Account/Sources/Account/Feedbin/FeedbinAccountDelegate.swift +++ b/Account/Sources/Account/Feedbin/FeedbinAccountDelegate.swift @@ -986,27 +986,22 @@ private extension FeedbinAccountDelegate { } func decideBestFeedChoice(account: Account, url: String, name: String?, container: Container, choices: [FeedbinSubscriptionChoice], completion: @escaping (Result) -> Void) { + var orderFound = 0 let feedSpecifiers: [FeedSpecifier] = choices.map { choice in let source = url == choice.url ? FeedSpecifier.Source.UserEntered : FeedSpecifier.Source.HTMLLink - let specifier = FeedSpecifier(title: choice.name, urlString: choice.url, source: source) + orderFound = orderFound + 1 + let specifier = FeedSpecifier(title: choice.name, urlString: choice.url, source: source, orderFound: orderFound) return specifier } if let bestSpecifier = FeedSpecifier.bestFeed(in: Set(feedSpecifiers)) { - if let bestSubscription = choices.filter({ bestSpecifier.urlString == $0.url }).first { - createWebFeed(for: account, url: bestSubscription.url, name: name, container: container, completion: completion) - } else { - DispatchQueue.main.async { - completion(.failure(FeedbinAccountDelegateError.invalidParameter)) - } - } + createWebFeed(for: account, url: bestSpecifier.urlString, name: name, container: container, completion: completion) } else { DispatchQueue.main.async { completion(.failure(FeedbinAccountDelegateError.invalidParameter)) } } - } func createFeed( account: Account, subscription sub: FeedbinSubscription, name: String?, container: Container, completion: @escaping (Result) -> Void) { diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift b/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift index 89bfa3ea4..b2164481a 100644 --- a/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift +++ b/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift @@ -235,6 +235,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate { switch result { case .success: DispatchQueue.main.async { + folder.externalID = "user/-/label/\(name)" folder.name = name completion(.success(())) } @@ -298,13 +299,18 @@ final class ReaderAPIAccountDelegate: AccountDelegate { } group.notify(queue: DispatchQueue.main) { - self.caller.deleteTag(folder: folder) { result in - switch result { - case .success: - account.removeFolder(folder) - completion(.success(())) - case .failure(let error): - completion(.failure(error)) + if self.variant == .theOldReader { + account.removeFolder(folder) + completion(.success(())) + } else { + self.caller.deleteTag(folder: folder) { result in + switch result { + case .success: + account.removeFolder(folder) + completion(.success(())) + case .failure(let error): + completion(.failure(error)) + } } } } @@ -821,30 +827,6 @@ private extension ReaderAPIAccountDelegate { feed.folderRelationship = [folderExternalID: feedExternalID] } } - - func decideBestFeedChoice(account: Account, url: String, name: String?, container: Container, choices: [ReaderAPISubscriptionChoice], completion: @escaping (Result) -> Void) { - - let feedSpecifiers: [FeedSpecifier] = choices.map { choice in - let source = url == choice.url ? FeedSpecifier.Source.UserEntered : FeedSpecifier.Source.HTMLLink - let specifier = FeedSpecifier(title: choice.name, urlString: choice.url, source: source) - return specifier - } - - if let bestSpecifier = FeedSpecifier.bestFeed(in: Set(feedSpecifiers)) { - if let bestSubscription = choices.filter({ bestSpecifier.urlString == $0.url }).first { - createWebFeed(for: account, url: bestSubscription.url, name: name, container: container, completion: completion) - } else { - DispatchQueue.main.async { - completion(.failure(ReaderAPIAccountDelegateError.invalidParameter)) - } - } - } else { - DispatchQueue.main.async { - completion(.failure(ReaderAPIAccountDelegateError.invalidParameter)) - } - } - - } func createFeed( account: Account, subscription sub: ReaderAPISubscription, name: String?, container: Container, completion: @escaping (Result) -> Void) { @@ -857,7 +839,7 @@ private extension ReaderAPIAccountDelegate { switch result { case .success: if let name = name { - account.renameWebFeed(feed, to: name) { result in + self.renameWebFeed(for: account, with: feed, to: name) { result in switch result { case .success: self.initialFeedDownload(account: account, feed: feed, completion: completion) diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift b/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift index 714403ebd..48784e9f8 100644 --- a/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift +++ b/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift @@ -49,20 +49,22 @@ final class ReaderAPICaller: NSObject { } private var transport: Transport! - - var variant: ReaderAPIVariant = .generic - var credentials: Credentials? + private let uriComponentAllowed: CharacterSet + private var accessToken: String? weak var accountMetadata: AccountMetadata? + var variant: ReaderAPIVariant = .generic + var credentials: Credentials? + var server: String? { get { - return APIBaseURL?.host + return apiBaseURL?.host } } - private var APIBaseURL: URL? { + private var apiBaseURL: URL? { get { switch variant { case .generic, .freshRSS: @@ -77,8 +79,13 @@ final class ReaderAPICaller: NSObject { } init(transport: Transport) { - super.init() self.transport = transport + + var urlHostAllowed = CharacterSet.urlHostAllowed + urlHostAllowed.remove("+") + urlHostAllowed.remove("&") + uriComponentAllowed = urlHostAllowed + super.init() } func cancelAll() { @@ -170,7 +177,7 @@ final class ReaderAPICaller: NSObject { func retrieveTags(completion: @escaping (Result<[ReaderAPITag]?, Error>) -> Void) { - guard let baseURL = APIBaseURL else { + guard let baseURL = apiBaseURL else { completion(.failure(CredentialsError.incompleteCredentials)) return } @@ -199,7 +206,7 @@ final class ReaderAPICaller: NSObject { } func renameTag(oldName: String, newName: String, completion: @escaping (Result) -> Void) { - guard let baseURL = APIBaseURL else { + guard let baseURL = apiBaseURL else { completion(.failure(CredentialsError.incompleteCredentials)) return } @@ -212,8 +219,13 @@ final class ReaderAPICaller: NSObject { request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") request.httpMethod = "POST" - let oldTagName = "user/-/label/\(oldName)" - let newTagName = "user/-/label/\(newName)" + guard let encodedOldName = self.encodeForURLPath(oldName), let encodedNewName = self.encodeForURLPath(newName) else { + completion(.failure(ReaderAPIAccountDelegateError.invalidParameter)) + return + } + + let oldTagName = "user/-/label/\(encodedOldName)" + let newTagName = "user/-/label/\(encodedNewName)" let postData = "T=\(token)&s=\(oldTagName)&dest=\(newTagName)".data(using: String.Encoding.utf8) self.transport.send(request: request, method: HTTPMethod.post, payload: postData!, completion: { (result) in @@ -235,7 +247,7 @@ final class ReaderAPICaller: NSObject { } func deleteTag(folder: Folder, completion: @escaping (Result) -> Void) { - guard let baseURL = APIBaseURL else { + guard let baseURL = apiBaseURL else { completion(.failure(CredentialsError.incompleteCredentials)) return } @@ -274,7 +286,7 @@ final class ReaderAPICaller: NSObject { } func retrieveSubscriptions(completion: @escaping (Result<[ReaderAPISubscription]?, Error>) -> Void) { - guard let baseURL = APIBaseURL else { + guard let baseURL = apiBaseURL else { completion(.failure(CredentialsError.incompleteCredentials)) return } @@ -302,7 +314,7 @@ final class ReaderAPICaller: NSObject { } func createSubscription(url: String, name: String?, folder: Folder?, completion: @escaping (Result) -> Void) { - guard let baseURL = APIBaseURL else { + guard let baseURL = apiBaseURL else { completion(.failure(CredentialsError.incompleteCredentials)) return } @@ -376,10 +388,10 @@ final class ReaderAPICaller: NSObject { request.httpMethod = "POST" var postString = "T=\(token)&ac=subscribe&s=\(streamId)" - if let folder = folder { - postString += "&a=user/-/label/\(folder.nameForDisplay)" + if let folderName = self.encodeForURLPath(folder?.nameForDisplay) { + postString += "&a=user/-/label/\(folderName)" } - if let name = name { + if let name = self.encodeForURLPath(name) { postString += "&t=\(name)" } @@ -411,7 +423,7 @@ final class ReaderAPICaller: NSObject { } func renameSubscription(subscriptionID: String, newName: String, completion: @escaping (Result) -> Void) { - guard let baseURL = APIBaseURL else { + guard let baseURL = apiBaseURL else { completion(.failure(CredentialsError.incompleteCredentials)) return } @@ -424,7 +436,12 @@ final class ReaderAPICaller: NSObject { request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") request.httpMethod = "POST" - let postData = "T=\(token)&s=\(subscriptionID)&ac=edit&t=\(newName)".data(using: String.Encoding.utf8) + guard let encodedNewName = self.encodeForURLPath(newName) else { + completion(.failure(ReaderAPIAccountDelegateError.invalidParameter)) + return + } + + let postData = "T=\(token)&s=\(subscriptionID)&ac=edit&t=\(encodedNewName)".data(using: String.Encoding.utf8) self.transport.send(request: request, method: HTTPMethod.post, payload: postData!, completion: { (result) in switch result { @@ -445,7 +462,7 @@ final class ReaderAPICaller: NSObject { } func deleteSubscription(subscriptionID: String, completion: @escaping (Result) -> Void) { - guard let baseURL = APIBaseURL else { + guard let baseURL = apiBaseURL else { completion(.failure(CredentialsError.incompleteCredentials)) return } @@ -479,7 +496,7 @@ final class ReaderAPICaller: NSObject { func createTagging(subscriptionID: String, tagName: String, completion: @escaping (Result) -> Void) { - guard let baseURL = APIBaseURL else { + guard let baseURL = apiBaseURL else { completion(.failure(CredentialsError.incompleteCredentials)) return } @@ -492,8 +509,12 @@ final class ReaderAPICaller: NSObject { request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") request.httpMethod = "POST" - let tagName = "user/-/label/\(tagName)" - let postData = "T=\(token)&s=\(subscriptionID)&ac=edit&a=\(tagName)".data(using: String.Encoding.utf8) + guard let tagName = self.encodeForURLPath(tagName) else { + completion(.failure(ReaderAPIAccountDelegateError.invalidParameter)) + return + } + + let postData = "T=\(token)&s=\(subscriptionID)&ac=edit&a=user/-/label/\(tagName)".data(using: String.Encoding.utf8) self.transport.send(request: request, method: HTTPMethod.post, payload: postData!, completion: { (result) in switch result { @@ -514,7 +535,7 @@ final class ReaderAPICaller: NSObject { } func deleteTagging(subscriptionID: String, tagName: String, completion: @escaping (Result) -> Void) { - guard let baseURL = APIBaseURL else { + guard let baseURL = apiBaseURL else { completion(.failure(CredentialsError.incompleteCredentials)) return } @@ -554,7 +575,7 @@ final class ReaderAPICaller: NSObject { return } - guard let baseURL = APIBaseURL else { + guard let baseURL = apiBaseURL else { completion(.failure(CredentialsError.incompleteCredentials)) return } @@ -603,7 +624,7 @@ final class ReaderAPICaller: NSObject { } func retrieveItemIDs(type: ItemIDType, webFeedID: String? = nil, completion: @escaping ((Result<[String], Error>) -> Void)) { - guard let baseURL = APIBaseURL else { + guard let baseURL = apiBaseURL else { completion(.failure(CredentialsError.incompleteCredentials)) return } @@ -734,6 +755,11 @@ final class ReaderAPICaller: NSObject { private extension ReaderAPICaller { + func encodeForURLPath(_ pathComponent: String?) -> String? { + guard let pathComponent = pathComponent else { return nil } + return pathComponent.addingPercentEncoding(withAllowedCharacters: uriComponentAllowed) + } + func storeConditionalGet(key: String, headers: [AnyHashable : Any]) { if var conditionalGet = accountMetadata?.conditionalGetInfo { conditionalGet[key] = HTTPConditionalGetInfo(headers: headers) @@ -749,7 +775,7 @@ private extension ReaderAPICaller { } private func updateStateToEntries(entries: [String], state: ReaderState, add: Bool, completion: @escaping (Result) -> Void) { - guard let baseURL = APIBaseURL else { + guard let baseURL = apiBaseURL else { completion(.failure(CredentialsError.incompleteCredentials)) return } diff --git a/Mac/AppDefaults.swift b/Mac/AppDefaults.swift index caed0f08c..7be80dee5 100644 --- a/Mac/AppDefaults.swift +++ b/Mac/AppDefaults.swift @@ -30,6 +30,7 @@ final class AppDefaults { static let timelineGroupByFeed = "timelineGroupByFeed" static let detailFontSize = "detailFontSize" static let openInBrowserInBackground = "openInBrowserInBackground" + static let articleTextSize = "articleTextSize" static let refreshInterval = "refreshInterval" static let addWebFeedAccountID = "addWebFeedAccountID" static let addWebFeedFolderName = "addWebFeedFolderName" @@ -244,6 +245,16 @@ final class AppDefaults { return AppDefaults.bool(for: Key.timelineShowsSeparators) } + var articleTextSize: ArticleTextSize { + get { + let rawValue = UserDefaults.standard.integer(forKey: Key.articleTextSize) + return ArticleTextSize(rawValue: rawValue) ?? ArticleTextSize.large + } + set { + UserDefaults.standard.set(newValue.rawValue, forKey: Key.articleTextSize) + } + } + var refreshInterval: RefreshInterval { get { let rawValue = UserDefaults.standard.integer(forKey: Key.refreshInterval) diff --git a/Mac/Base.lproj/Preferences.storyboard b/Mac/Base.lproj/Preferences.storyboard index 4fbf41e07..2ad8500d2 100644 --- a/Mac/Base.lproj/Preferences.storyboard +++ b/Mac/Base.lproj/Preferences.storyboard @@ -25,21 +25,115 @@ - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -47,7 +141,7 @@ - + @@ -81,51 +175,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -133,7 +184,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - + + + + + - - + + + - + - - + - + - + + - @@ -219,29 +242,29 @@ + - - + - + - + - + @@ -249,7 +272,7 @@ - + @@ -267,7 +290,7 @@ - + @@ -298,7 +321,7 @@ - + @@ -316,7 +339,7 @@ + + + + + + @@ -354,6 +383,8 @@ + + @@ -363,15 +394,19 @@ + + + - + + @@ -407,22 +442,22 @@ - + - - - + + + - - + + - + @@ -435,7 +470,7 @@ - + @@ -447,7 +482,7 @@ - + @@ -523,7 +558,7 @@ - + @@ -578,22 +613,22 @@ - + - - - + + + - - + + - + @@ -606,7 +641,7 @@ - + @@ -618,7 +653,7 @@ - + @@ -690,7 +725,7 @@ - + diff --git a/Mac/MainWindow/Detail/DetailWebViewController.swift b/Mac/MainWindow/Detail/DetailWebViewController.swift index 28f85df91..9e13e9129 100644 --- a/Mac/MainWindow/Detail/DetailWebViewController.swift +++ b/Mac/MainWindow/Detail/DetailWebViewController.swift @@ -39,6 +39,14 @@ final class DetailWebViewController: NSViewController, WKUIDelegate { return nil } } + + private var articleTextSize = AppDefaults.shared.articleTextSize { + didSet { + if articleTextSize != oldValue { + reloadHTML() + } + } + } #if !MAC_APP_STORE private var webInspectorEnabled: Bool { @@ -64,7 +72,7 @@ final class DetailWebViewController: NSViewController, WKUIDelegate { // Wrap the webview in a box configured with the same background color that the web view uses let box = NSBox(frame: .zero) box.boxType = .custom - box.borderType = .noBorder + box.isTransparent = true box.titlePosition = .noTitle box.contentViewMargins = .zero box.fillColor = NSColor(named: "webviewBackgroundColor")! @@ -118,7 +126,7 @@ final class DetailWebViewController: NSViewController, WKUIDelegate { NotificationCenter.default.addObserver(self, selector: #selector(webFeedIconDidBecomeAvailable(_:)), name: .WebFeedIconDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(avatarDidBecomeAvailable(_:)), name: .AvatarDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil) webView.loadFileURL(ArticleRenderer.blank.url, allowingReadAccessTo: ArticleRenderer.blank.baseURL) } @@ -137,6 +145,10 @@ final class DetailWebViewController: NSViewController, WKUIDelegate { reloadArticleImage() } + @objc func userDefaultsDidChange(_ note: Notification) { + self.articleTextSize = AppDefaults.shared.articleTextSize + } + // MARK: Media Functions func stopMediaPlayback() { diff --git a/Mac/MainWindow/Detail/styleSheet.css b/Mac/MainWindow/Detail/styleSheet.css index 2caaa30eb..1dba70762 100644 --- a/Mac/MainWindow/Detail/styleSheet.css +++ b/Mac/MainWindow/Detail/styleSheet.css @@ -1,10 +1,10 @@ body { margin-top: 20px; margin-bottom: 64px; - padding-left: 64px; - padding-right: 64px; + padding-left: 48px; + padding-right: 48px; font-family: -apple-system; - font-size: 18px; + font-size: [[font-size]]px; } :root { diff --git a/Mac/MainWindow/MainWindowController.swift b/Mac/MainWindow/MainWindowController.swift index 1bb206d19..0b1be8851 100644 --- a/Mac/MainWindow/MainWindowController.swift +++ b/Mac/MainWindow/MainWindowController.swift @@ -323,13 +323,16 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { NSCursor.setHiddenUntilMouseMoves(true) // TODO: handle search mode - if timelineViewController.canGoToNextUnread() { - goToNextUnreadInTimeline() + if timelineViewController.canGoToNextUnread(wrappingToTop: false) { + goToNextUnreadInTimeline(wrappingToTop: false) } - else if sidebarViewController.canGoToNextUnread() { - sidebarViewController.goToNextUnread() - if timelineViewController.canGoToNextUnread() { - goToNextUnreadInTimeline() + else if sidebarViewController.canGoToNextUnread(wrappingToTop: true) { + sidebarViewController.goToNextUnread(wrappingToTop: true) + + // If we ended up on the same timelineViewController, we may need to wrap + // around to the top of its contents. + if timelineViewController.canGoToNextUnread(wrappingToTop: true) { + goToNextUnreadInTimeline(wrappingToTop: true) } } } @@ -995,13 +998,13 @@ private extension MainWindowController { // MARK: - Command Validation - func canGoToNextUnread() -> Bool { + func canGoToNextUnread(wrappingToTop wrapping: Bool = false) -> Bool { guard let timelineViewController = currentTimelineViewController, let sidebarViewController = sidebarViewController else { return false } // TODO: handle search mode - return timelineViewController.canGoToNextUnread() || sidebarViewController.canGoToNextUnread() + return timelineViewController.canGoToNextUnread(wrappingToTop: wrapping) || sidebarViewController.canGoToNextUnread(wrappingToTop: wrapping) } func canMarkAllAsRead() -> Bool { @@ -1188,14 +1191,14 @@ private extension MainWindowController { // MARK: - Misc. - func goToNextUnreadInTimeline() { + func goToNextUnreadInTimeline(wrappingToTop wrapping: Bool) { guard let timelineViewController = currentTimelineViewController else { return } - if timelineViewController.canGoToNextUnread() { - timelineViewController.goToNextUnread() + if timelineViewController.canGoToNextUnread(wrappingToTop: wrapping) { + timelineViewController.goToNextUnread(wrappingToTop: wrapping) makeTimelineViewFirstResponder() } } diff --git a/Mac/MainWindow/SharingServicePickerDelegate.swift b/Mac/MainWindow/SharingServicePickerDelegate.swift index 1a5349a42..13c989460 100644 --- a/Mac/MainWindow/SharingServicePickerDelegate.swift +++ b/Mac/MainWindow/SharingServicePickerDelegate.swift @@ -18,7 +18,8 @@ import RSCore } func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, sharingServicesForItems items: [Any], proposedSharingServices proposedServices: [NSSharingService]) -> [NSSharingService] { - return proposedServices + SharingServicePickerDelegate.customSharingServices(for: items) + let filteredServices = proposedServices.filter { $0.menuItemTitle != "NetNewsWire" } + return filteredServices + SharingServicePickerDelegate.customSharingServices(for: items) } func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, delegateFor sharingService: NSSharingService) -> NSSharingServiceDelegate? { diff --git a/Mac/MainWindow/Sidebar/SidebarViewController.swift b/Mac/MainWindow/Sidebar/SidebarViewController.swift index 22f097268..e7dcf8cd3 100644 --- a/Mac/MainWindow/Sidebar/SidebarViewController.swift +++ b/Mac/MainWindow/Sidebar/SidebarViewController.swift @@ -294,15 +294,15 @@ protocol SidebarDelegate: class { // MARK: - Navigation - func canGoToNextUnread() -> Bool { - if let _ = nextSelectableRowWithUnreadArticle() { + func canGoToNextUnread(wrappingToTop wrapping: Bool = false) -> Bool { + if let _ = nextSelectableRowWithUnreadArticle(wrappingToTop: wrapping) { return true } return false } - func goToNextUnread() { - guard let row = nextSelectableRowWithUnreadArticle() else { + func goToNextUnread(wrappingToTop wrapping: Bool = false) { + guard let row = nextSelectableRowWithUnreadArticle(wrappingToTop: wrapping) else { assertionFailure("goToNextUnread called before checking if there is a next unread.") return } @@ -685,26 +685,42 @@ private extension SidebarViewController { return false } - func nextSelectableRowWithUnreadArticle() -> Int? { - // Skip group items, because they should never be selected. - - let selectedRow = outlineView.selectedRow - let numberOfRows = outlineView.numberOfRows - var row = selectedRow + 1 - - while (row < numberOfRows) { - if rowHasAtLeastOneUnreadArticle(row) && !rowIsGroupItem(row) { - return row - } - row += 1 + func rowIsExpandedFolder(_ row: Int) -> Bool { + if let node = nodeForRow(row), outlineView.isItemExpanded(node) { + return true } - - row = 0 - while (row <= selectedRow) { - if rowHasAtLeastOneUnreadArticle(row) && !rowIsGroupItem(row) { + return false + } + + func shouldSkipRow(_ row: Int) -> Bool { + let skipExpandedFolders = UserDefaults.standard.bool(forKey: "JalkutRespectFolderExpansionOnNextUnread") + + // Skip group items, because they should never be selected. + // Skip expanded folders only if Jalkut's pref is enabled. + if rowIsGroupItem(row) || (skipExpandedFolders && rowIsExpandedFolder(row)) { + return true + } + return false + } + + func nextSelectableRowWithUnreadArticle(wrappingToTop wrapping: Bool = false) -> Int? { + let numberOfRows = outlineView.numberOfRows + let startRow = outlineView.selectedRow + 1 + + let orderedRows: [Int] + if startRow == numberOfRows { + // Last item is selected, so start at the beginning if we allow wrapping + orderedRows = wrapping ? Array(0.. Bool { - guard let _ = indexOfNextUnreadArticle() else { + func canGoToNextUnread(wrappingToTop wrapping: Bool = false) -> Bool { + guard let _ = indexOfNextUnreadArticle(wrappingToTop: wrapping) else { return false } return true } - func indexOfNextUnreadArticle() -> Int? { - return articles.rowOfNextUnreadArticle(tableView.selectedRow) + func indexOfNextUnreadArticle(wrappingToTop wrapping: Bool = false) -> Int? { + return articles.rowOfNextUnreadArticle(tableView.selectedRow, wrappingToTop: wrapping) } func focus() { diff --git a/Mac/Preferences/Accounts/AccountsAddCloudKit.xib b/Mac/Preferences/Accounts/AccountsAddCloudKit.xib index b6dc5afd1..2e660df44 100644 --- a/Mac/Preferences/Accounts/AccountsAddCloudKit.xib +++ b/Mac/Preferences/Accounts/AccountsAddCloudKit.xib @@ -17,40 +17,14 @@ - + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - + + - + + - - + + + + + + + + + + + + + + + + + - + + + - - - + + + + + + + - + diff --git a/Mac/Preferences/Accounts/AccountsAddLocal.xib b/Mac/Preferences/Accounts/AccountsAddLocal.xib index 315c287f2..36e5232cf 100644 --- a/Mac/Preferences/Accounts/AccountsAddLocal.xib +++ b/Mac/Preferences/Accounts/AccountsAddLocal.xib @@ -20,70 +20,15 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + + + + + + + + + + - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + - - - + + + + + + + + + + + + diff --git a/Mac/Preferences/Accounts/AccountsAddLocalWindowController.swift b/Mac/Preferences/Accounts/AccountsAddLocalWindowController.swift index 753fc4e75..8aedbccd7 100644 --- a/Mac/Preferences/Accounts/AccountsAddLocalWindowController.swift +++ b/Mac/Preferences/Accounts/AccountsAddLocalWindowController.swift @@ -23,7 +23,7 @@ class AccountsAddLocalWindowController: NSWindowController { override func windowDidLoad() { super.windowDidLoad() - localAccountNameTextField.stringValue = Account.defaultLocalAccountName + localAccountNameTextField.stringValue = NSLocalizedString("Create a local account on your Mac.", comment: "Account Local") } // MARK: API diff --git a/Mac/Preferences/Accounts/AccountsFeedWrangler.xib b/Mac/Preferences/Accounts/AccountsFeedWrangler.xib index d28eadf16..4c7c0b4ff 100644 --- a/Mac/Preferences/Accounts/AccountsFeedWrangler.xib +++ b/Mac/Preferences/Accounts/AccountsFeedWrangler.xib @@ -3,15 +3,19 @@ + - + + + + @@ -28,40 +32,13 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + + - @@ -70,7 +47,7 @@ - + @@ -80,11 +57,11 @@ - + - + @@ -93,7 +70,7 @@ - + @@ -103,11 +80,11 @@ - + - + @@ -117,17 +94,6 @@ - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - + + + + @@ -182,5 +226,8 @@ Gw + + + diff --git a/Mac/Preferences/Accounts/AccountsFeedWranglerWindowController.swift b/Mac/Preferences/Accounts/AccountsFeedWranglerWindowController.swift index 5b84ce7af..bab79f8de 100644 --- a/Mac/Preferences/Accounts/AccountsFeedWranglerWindowController.swift +++ b/Mac/Preferences/Accounts/AccountsFeedWranglerWindowController.swift @@ -12,6 +12,10 @@ import RSWeb import Secrets class AccountsFeedWranglerWindowController: NSWindowController { + + @IBOutlet weak var signInTextField: NSTextField! + @IBOutlet weak var noAccountTextField: NSTextField! + @IBOutlet weak var createNewAccountButton: NSButton! @IBOutlet weak var progressIndicator: NSProgressIndicator! @IBOutlet weak var usernameTextField: NSTextField! @IBOutlet weak var passwordTextField: NSSecureTextField! @@ -30,8 +34,12 @@ class AccountsFeedWranglerWindowController: NSWindowController { if let account = account, let credentials = try? account.retrieveCredentials(type: .basic) { usernameTextField.stringValue = credentials.username actionButton.title = NSLocalizedString("Update", comment: "Update") + signInTextField.stringValue = NSLocalizedString("Update your Feed Wrangler account credentials.", comment: "SignIn") + noAccountTextField.isHidden = true + createNewAccountButton.isHidden = true } else { actionButton.title = NSLocalizedString("Create", comment: "Create") + signInTextField.stringValue = NSLocalizedString("Sign in to your Feed Wrangler account.", comment: "SignIn") } } @@ -113,4 +121,9 @@ class AccountsFeedWranglerWindowController: NSWindowController { } } } + + @IBAction func createAccountWithProvider(_ sender: Any) { + NSWorkspace.shared.open(URL(string: "https://feedwrangler.net/users/new")!) + } + } diff --git a/Mac/Preferences/Accounts/AccountsFeedbin.xib b/Mac/Preferences/Accounts/AccountsFeedbin.xib index 8edd2ce12..f5f01ae92 100644 --- a/Mac/Preferences/Accounts/AccountsFeedbin.xib +++ b/Mac/Preferences/Accounts/AccountsFeedbin.xib @@ -3,15 +3,19 @@ + - + + + + @@ -28,43 +32,13 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - + + - @@ -73,7 +47,7 @@ - + @@ -83,11 +57,11 @@ - + - + @@ -96,7 +70,7 @@ - + @@ -106,11 +80,11 @@ - + - + @@ -120,17 +94,6 @@ - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - - + + + + + + + + @@ -185,5 +226,8 @@ Gw + + + diff --git a/Mac/Preferences/Accounts/AccountsFeedbinWindowController.swift b/Mac/Preferences/Accounts/AccountsFeedbinWindowController.swift index b022e75b5..56f0e8d42 100644 --- a/Mac/Preferences/Accounts/AccountsFeedbinWindowController.swift +++ b/Mac/Preferences/Accounts/AccountsFeedbinWindowController.swift @@ -13,6 +13,9 @@ import Secrets class AccountsFeedbinWindowController: NSWindowController { + @IBOutlet weak var signInTextField: NSTextField! + @IBOutlet weak var noAccountTextField: NSTextField! + @IBOutlet weak var createNewAccountButton: NSButton! @IBOutlet weak var progressIndicator: NSProgressIndicator! @IBOutlet weak var usernameTextField: NSTextField! @IBOutlet weak var passwordTextField: NSSecureTextField! @@ -31,8 +34,12 @@ class AccountsFeedbinWindowController: NSWindowController { if let account = account, let credentials = try? account.retrieveCredentials(type: .basic) { usernameTextField.stringValue = credentials.username actionButton.title = NSLocalizedString("Update", comment: "Update") + signInTextField.stringValue = NSLocalizedString("Update your Feedbin account credentials.", comment: "SignIn") + noAccountTextField.isHidden = true + createNewAccountButton.isHidden = true } else { - actionButton.title = NSLocalizedString("Add Account", comment: "Add Account") + actionButton.title = NSLocalizedString("Create", comment: "Add Account") + signInTextField.stringValue = NSLocalizedString("Sign in to your Feedbin account.", comment: "SignIn") } } @@ -117,5 +124,10 @@ class AccountsFeedbinWindowController: NSWindowController { } } + + @IBAction func createAccountWithProvider(_ sender: Any) { + NSWorkspace.shared.open(URL(string: "https://feedbin.com/signup")!) + } + } diff --git a/Mac/Preferences/Accounts/AccountsNewsBlur.xib b/Mac/Preferences/Accounts/AccountsNewsBlur.xib index 798f154a1..b6133daa2 100644 --- a/Mac/Preferences/Accounts/AccountsNewsBlur.xib +++ b/Mac/Preferences/Accounts/AccountsNewsBlur.xib @@ -3,15 +3,19 @@ + - + + + + @@ -28,40 +32,13 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + + - @@ -70,7 +47,7 @@ - + @@ -80,11 +57,11 @@ - + - + @@ -93,7 +70,7 @@ - + @@ -103,11 +80,11 @@ - + - + @@ -117,17 +94,6 @@ - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + - - + + + + + + @@ -182,5 +225,8 @@ Gw + + + diff --git a/Mac/Preferences/Accounts/AccountsNewsBlurWindowController.swift b/Mac/Preferences/Accounts/AccountsNewsBlurWindowController.swift index e3fc527b2..4ae73333d 100644 --- a/Mac/Preferences/Accounts/AccountsNewsBlurWindowController.swift +++ b/Mac/Preferences/Accounts/AccountsNewsBlurWindowController.swift @@ -12,6 +12,10 @@ import RSWeb import Secrets class AccountsNewsBlurWindowController: NSWindowController { + + @IBOutlet weak var signInTextField: NSTextField! + @IBOutlet weak var noAccountTextField: NSTextField! + @IBOutlet weak var createNewAccountButton: NSButton! @IBOutlet weak var progressIndicator: NSProgressIndicator! @IBOutlet weak var usernameTextField: NSTextField! @IBOutlet weak var passwordTextField: NSSecureTextField! @@ -30,8 +34,12 @@ class AccountsNewsBlurWindowController: NSWindowController { if let account = account, let credentials = try? account.retrieveCredentials(type: .newsBlurBasic) { usernameTextField.stringValue = credentials.username actionButton.title = NSLocalizedString("Update", comment: "Update") + signInTextField.stringValue = NSLocalizedString("Update your NewsBlur account credentials.", comment: "SignIn") + noAccountTextField.isHidden = true + createNewAccountButton.isHidden = true } else { actionButton.title = NSLocalizedString("Create", comment: "Create") + signInTextField.stringValue = NSLocalizedString("Sign in to your NewsBlur account.", comment: "SignIn") } } @@ -113,4 +121,9 @@ class AccountsNewsBlurWindowController: NSWindowController { } } } + + @IBAction func createAccountWithProvider(_ sender: Any) { + NSWorkspace.shared.open(URL(string: "https://newsblur.com")!) + } + } diff --git a/Mac/Preferences/Accounts/AccountsPreferencesViewController.swift b/Mac/Preferences/Accounts/AccountsPreferencesViewController.swift index 2153f13d3..5d026882a 100644 --- a/Mac/Preferences/Accounts/AccountsPreferencesViewController.swift +++ b/Mac/Preferences/Accounts/AccountsPreferencesViewController.swift @@ -45,10 +45,7 @@ final class AccountsPreferencesViewController: NSViewController { rTable.size.width = tableView.superview!.frame.size.width tableView.frame = rTable - // Set initial row selection - if sortedAccounts.count > 0 { - tableView.selectRow(0) - } + hideController() } @IBAction func addAccount(_ sender: Any) { @@ -226,12 +223,11 @@ private extension AccountsPreferencesViewController { func showController(_ controller: NSViewController) { hideController() - + addChild(controller) controller.view.translatesAutoresizingMaskIntoConstraints = false detailView.addSubview(controller.view) detailView.addFullSizeConstraints(forSubview: controller.view) - } func hideController() { @@ -239,6 +235,27 @@ private extension AccountsPreferencesViewController { children.removeAll() controller.view.removeFromSuperview() } + + if tableView.selectedRow == -1 { + var helpText = "" + if sortedAccounts.count == 0 { + helpText = NSLocalizedString("Add an account by clicking the + button.", comment: "Add Account Explainer") + } else { + helpText = NSLocalizedString("Select an account or add a new account by clicking the + button.", comment: "Add Account Explainer") + } + + let textHostingController = NSHostingController(rootView: + AddAccountHelpView(delegate: addAccountDelegate, helpText: helpText)) + addChild(textHostingController) + textHostingController.view.translatesAutoresizingMaskIntoConstraints = false + detailView.addSubview(textHostingController.view) + detailView.addConstraints([ + NSLayoutConstraint(item: textHostingController.view, attribute: .top, relatedBy: .equal, toItem: detailView, attribute: .top, multiplier: 1, constant: 1), + NSLayoutConstraint(item: textHostingController.view, attribute: .bottom, relatedBy: .equal, toItem: detailView, attribute: .bottom, multiplier: 1, constant: -deleteButton.frame.height), + NSLayoutConstraint(item: textHostingController.view, attribute: .width, relatedBy: .equal, toItem: detailView, attribute: .width, multiplier: 1, constant: 1) + ]) + + } } } diff --git a/Mac/Preferences/Accounts/AccountsReaderAPI.xib b/Mac/Preferences/Accounts/AccountsReaderAPI.xib index 749dbccdf..3da28d180 100644 --- a/Mac/Preferences/Accounts/AccountsReaderAPI.xib +++ b/Mac/Preferences/Accounts/AccountsReaderAPI.xib @@ -3,6 +3,7 @@ + @@ -11,8 +12,9 @@ - + + @@ -28,49 +30,19 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - + + - @@ -79,7 +51,7 @@ - + @@ -89,11 +61,11 @@ - + - + @@ -102,7 +74,7 @@ - + @@ -112,11 +84,11 @@ - + - + @@ -128,7 +100,7 @@ - + @@ -138,7 +110,7 @@ - + @@ -149,17 +121,6 @@ - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + - - + + + + @@ -212,4 +252,9 @@ Gw + + + + + diff --git a/Mac/Preferences/Accounts/AccountsReaderAPIWindowController.swift b/Mac/Preferences/Accounts/AccountsReaderAPIWindowController.swift index ccec1dedd..15ab289fb 100644 --- a/Mac/Preferences/Accounts/AccountsReaderAPIWindowController.swift +++ b/Mac/Preferences/Accounts/AccountsReaderAPIWindowController.swift @@ -23,6 +23,7 @@ class AccountsReaderAPIWindowController: NSWindowController { @IBOutlet weak var passwordTextField: NSSecureTextField! @IBOutlet weak var errorMessageLabel: NSTextField! @IBOutlet weak var actionButton: NSButton! + @IBOutlet weak var noAccountTextField: NSTextField! var account: Account? var accountType: AccountType? @@ -38,19 +39,23 @@ class AccountsReaderAPIWindowController: NSWindowController { switch accountType { case .freshRSS: titleImageView.image = AppAssets.accountFreshRSS - titleLabel.stringValue = NSLocalizedString("FreshRSS", comment: "FreshRSS") + titleLabel.stringValue = NSLocalizedString("Sign in to your FreshRSS account.", comment: "FreshRSS") + noAccountTextField.stringValue = NSLocalizedString("Don't have a FreshRSS account?", comment: "No FreshRSS") case .inoreader: titleImageView.image = AppAssets.accountInoreader - titleLabel.stringValue = NSLocalizedString("InoReader", comment: "InoReader") + titleLabel.stringValue = NSLocalizedString("Sign in to your InoReader account.", comment: "InoReader") gridView.row(at: 2).isHidden = true + noAccountTextField.stringValue = NSLocalizedString("Don't have an InoReader account?", comment: "No InoReader") case .bazQux: titleImageView.image = AppAssets.accountBazQux - titleLabel.stringValue = NSLocalizedString("BazQux", comment: "BazQux") + titleLabel.stringValue = NSLocalizedString("Sign in to your BazQux account.", comment: "BazQux") gridView.row(at: 2).isHidden = true + noAccountTextField.stringValue = NSLocalizedString("Don't have a BazQux account?", comment: "No BazQux") case .theOldReader: titleImageView.image = AppAssets.accountTheOldReader - titleLabel.stringValue = NSLocalizedString("The Old Reader", comment: "The Old Reader") + titleLabel.stringValue = NSLocalizedString("Sign in to your The Old Reader account.", comment: "The Old Reader") gridView.row(at: 2).isHidden = true + noAccountTextField.stringValue = NSLocalizedString("Don't have a The Old Reader account?", comment: "No OldReader") default: break } @@ -172,5 +177,21 @@ class AccountsReaderAPIWindowController: NSWindowController { } } + + @IBAction func createAccountWithProvider(_ sender: Any) { + switch accountType { + case .freshRSS: + NSWorkspace.shared.open(URL(string: "https://freshrss.org")!) + case .inoreader: + NSWorkspace.shared.open(URL(string: "https://www.inoreader.com")!) + case .bazQux: + NSWorkspace.shared.open(URL(string: "https://bazqux.com")!) + case .theOldReader: + NSWorkspace.shared.open(URL(string: "https://theoldreader.com")!) + default: + return + } + } + } diff --git a/Mac/Preferences/Accounts/AddAccountHelpView.swift b/Mac/Preferences/Accounts/AddAccountHelpView.swift new file mode 100644 index 000000000..373fe6edb --- /dev/null +++ b/Mac/Preferences/Accounts/AddAccountHelpView.swift @@ -0,0 +1,60 @@ +// +// AddAccountHelpView.swift +// NetNewsWire +// +// Created by Stuart Breckenridge on 4/11/20. +// Copyright © 2020 Ranchero Software. All rights reserved. +// + +import SwiftUI +import Account + +struct AddAccountHelpView: View { + + let accountTypes: [AccountType] = AddAccountSections.allOrdered.sectionContent + var delegate: AccountsPreferencesAddAccountDelegate? + var helpText: String + @State private var hoveringId: String? = nil + @State private var iCloudUnavailableError: Bool = false + + var body: some View { + VStack { + HStack { + ForEach(accountTypes, id: \.self) { account in + account.image() + .resizable() + .frame(width: 20, height: 20, alignment: .center) + .onTapGesture { + if account == .cloudKit && AccountManager.shared.accounts.contains(where: { $0.type == .cloudKit }) { + iCloudUnavailableError = true + } else { + delegate?.presentSheetForAccount(account) + } + hoveringId = nil + } + .onHover(perform: { hovering in + if hovering { + hoveringId = account.localizedAccountName() + } else { + hoveringId = nil + } + }) + .scaleEffect(hoveringId == account.localizedAccountName() ? 1.2 : 1) + .shadow(radius: hoveringId == account.localizedAccountName() ? 0.8 : 0) + } + } + + Text(helpText) + .multilineTextAlignment(.center) + .padding(.top, 8) + + } + .alert(isPresented: $iCloudUnavailableError, content: { + Alert(title: Text(NSLocalizedString("Error", comment: "Error")), + message: Text(NSLocalizedString("You've already set up an iCloud account.", comment: "Error")), + dismissButton: Alert.Button.cancel({ + iCloudUnavailableError = false + })) + }) + } +} diff --git a/Mac/Preferences/Accounts/AddAccountsView.swift b/Mac/Preferences/Accounts/AddAccountsView.swift index 61ccaf3f1..664f2c476 100644 --- a/Mac/Preferences/Accounts/AddAccountsView.swift +++ b/Mac/Preferences/Accounts/AddAccountsView.swift @@ -9,11 +9,12 @@ import SwiftUI import Account -private enum AddAccountSections: Int, CaseIterable { +enum AddAccountSections: Int, CaseIterable { case local = 0 case icloud case web case selfhosted + case allOrdered var sectionHeader: String { switch self { @@ -25,6 +26,8 @@ private enum AddAccountSections: Int, CaseIterable { return NSLocalizedString("Web", comment: "Web Account") case .selfhosted: return NSLocalizedString("Self-hosted", comment: "Self hosted Account") + case .allOrdered: + return "" } } @@ -38,6 +41,8 @@ private enum AddAccountSections: Int, CaseIterable { return NSLocalizedString("Web accounts sync your subscriptions across all your devices.", comment: "Web Account") case .selfhosted: return NSLocalizedString("Self-hosted accounts sync your subscriptions across all your devices.", comment: "Self hosted Account") + case .allOrdered: + return "" } } @@ -51,6 +56,11 @@ private enum AddAccountSections: Int, CaseIterable { return [.bazQux, .feedbin, .feedly, .feedWrangler, .inoreader, .newsBlur, .theOldReader] case .selfhosted: return [.freshRSS] + case .allOrdered: + return AddAccountSections.local.sectionContent + + AddAccountSections.icloud.sectionContent + + AddAccountSections.web.sectionContent + + AddAccountSections.selfhosted.sectionContent } } } @@ -141,8 +151,6 @@ struct AddAccountsView: View { .aspectRatio(contentMode: .fit) .frame(width: 25, height: 25, alignment: .center) .padding(.leading, 4) - - Text(account.localizedAccountName()) } .tag(account) diff --git a/Mac/Preferences/ExtensionPoints/EnableExtensionPointHelpView.swift b/Mac/Preferences/ExtensionPoints/EnableExtensionPointHelpView.swift new file mode 100644 index 000000000..a9f9f24de --- /dev/null +++ b/Mac/Preferences/ExtensionPoints/EnableExtensionPointHelpView.swift @@ -0,0 +1,61 @@ +// +// EnableExtensionPointHelpView.swift +// NetNewsWire +// +// Created by Stuart Breckenridge on 4/11/20. +// Copyright © 2020 Ranchero Software. All rights reserved. +// + +import AppKit +import SwiftUI +import RSCore + +struct EnableExtensionPointHelpView: View { + + var extensionPoints: [ExtensionPoint.Type] { + let types = ExtensionPointManager.shared.availableExtensionPointTypes.filter({ $0 is SendToCommand.Type }) + + ExtensionPointManager.shared.availableExtensionPointTypes.filter({ !($0 is SendToCommand.Type) }) + return types + } + var helpText: String + weak var preferencesController: ExtensionPointPreferencesViewController? + + @State private var hoveringId: String? + + var body: some View { + VStack { + HStack { + ForEach(0..? // required because presentationMode.dismiss() doesn't work weak var enabler: ExtensionPointPreferencesEnabler? - @State private var extensionPointTypeName = String(describing: Self.feedProviderExtensionPointTypes.first!) + @State private var extensionPointTypeName = String(describing: Self.sendToCommandExtensionPointTypes.first) + private var selectedType: ExtensionPoint.Type? - init(enabler: ExtensionPointPreferencesEnabler?) { + init(enabler: ExtensionPointPreferencesEnabler?, selectedType: ExtensionPoint.Type? ) { self.enabler = enabler + self.selectedType = selectedType } var body: some View { @@ -48,7 +51,7 @@ struct EnableExtensionPointView: View { Text("Cancel") .frame(width: 80) }) - .accessibility(label: Text("Add Account")) + .accessibility(label: Text("Add Extension")) } if #available(OSX 11.0, *) { Button(action: { @@ -58,9 +61,9 @@ struct EnableExtensionPointView: View { Text("Continue") .frame(width: 80) }) - .help("Add Account") + .help("Add Extension") .keyboardShortcut(.defaultAction) - + .disabled(disableContinue()) } else { Button(action: { enabler?.enable(typeFromName(extensionPointTypeName)) @@ -69,6 +72,7 @@ struct EnableExtensionPointView: View { Text("Continue") .frame(width: 80) }) + .disabled(disableContinue()) } } .padding(.top, 12) @@ -78,6 +82,11 @@ struct EnableExtensionPointView: View { .fixedSize(horizontal: false, vertical: true) .frame(width: 420) .padding() + .onAppear { + if selectedType != nil { + extensionPointTypeName = String(describing: selectedType!) + } + } } var feedProviderExtensionPoints: some View { @@ -101,13 +110,13 @@ struct EnableExtensionPointView: View { Text(extensionPointType.title) } - .tag(extensionPointTypeNames) + .tag(extensionPointTypeName) }) }) .pickerStyle(RadioGroupPickerStyle()) .offset(x: 7.5, y: 0) - Text("An extension point that makes websites appear to provide RSS feeds for their content.") + Text("An extension that makes websites appear to provide RSS feeds for their content.") .foregroundColor(.gray) .font(.caption) .padding(.horizontal) @@ -138,13 +147,13 @@ struct EnableExtensionPointView: View { Text(extensionPointType.title) } - .tag(extensionPointTypeNames) + .tag(extensionPointTypeName) }) }) .pickerStyle(RadioGroupPickerStyle()) .offset(x: 7.5, y: 0) - Text("An extension point that enables a share menu item that passes article data to a third-party application.") + Text("An extension that enables a share menu item that passes article data to a third-party application.") .foregroundColor(.gray) .font(.caption) .padding(.horizontal) @@ -169,4 +178,11 @@ struct EnableExtensionPointView: View { } fatalError() } + + func disableContinue() -> Bool { + ExtensionPointManager.shared.availableExtensionPointTypes.count == 0 + } } + + + diff --git a/Mac/Preferences/ExtensionPoints/ExtensionPointPreferencesViewController.swift b/Mac/Preferences/ExtensionPoints/ExtensionPointPreferencesViewController.swift index d07249fba..2a596f66b 100644 --- a/Mac/Preferences/ExtensionPoints/ExtensionPointPreferencesViewController.swift +++ b/Mac/Preferences/ExtensionPoints/ExtensionPointPreferencesViewController.swift @@ -41,14 +41,17 @@ final class ExtensionPointPreferencesViewController: NSViewController { showDefaultView() - // Set initial row selection - if activeExtensionPoints.count > 0 { - tableView.selectRow(0) - } + } @IBAction func enableExtensionPoints(_ sender: Any) { - let controller = NSHostingController(rootView: EnableExtensionPointView(enabler: self)) + let controller = NSHostingController(rootView: EnableExtensionPointView(enabler: self, selectedType: nil)) + controller.rootView.parent = controller + presentAsSheet(controller) + } + + func enableExtensionPointFromSelection(_ selection: ExtensionPoint.Type) { + let controller = NSHostingController(rootView: EnableExtensionPointView(enabler: self, selectedType: selection)) controller.rootView.parent = controller presentAsSheet(controller) } @@ -179,6 +182,33 @@ private extension ExtensionPointPreferencesViewController { func showDefaultView() { activeExtensionPoints = Array(ExtensionPointManager.shared.activeExtensionPoints.values).sorted(by: { $0.title < $1.title }) tableView.reloadData() + + if tableView.selectedRow == -1 { + var helpText = "" + if ExtensionPointManager.shared.availableExtensionPointTypes.count == 0 { + helpText = NSLocalizedString("You've added all available extension points.", comment: "Extension Explainer") + } + else if activeExtensionPoints.count == 0 { + helpText = NSLocalizedString("Add an extension by clicking the + button.", comment: "Extension Explainer") + } else { + helpText = NSLocalizedString("Select an extension or add a new extension point by clicking the + button.", comment: "Extension Explainer") + } + + if let controller = children.first { + children.removeAll() + controller.view.removeFromSuperview() + } + + let textHostingController = NSHostingController(rootView: EnableExtensionPointHelpView(helpText: helpText, preferencesController: self)) + addChild(textHostingController) + textHostingController.view.translatesAutoresizingMaskIntoConstraints = false + detailView.addSubview(textHostingController.view) + detailView.addConstraints([ + NSLayoutConstraint(item: textHostingController.view, attribute: .top, relatedBy: .equal, toItem: detailView, attribute: .top, multiplier: 1, constant: 1), + NSLayoutConstraint(item: textHostingController.view, attribute: .bottom, relatedBy: .equal, toItem: detailView, attribute: .bottom, multiplier: 1, constant: -deleteButton.frame.height), + NSLayoutConstraint(item: textHostingController.view, attribute: .width, relatedBy: .equal, toItem: detailView, attribute: .width, multiplier: 1, constant: 1) + ]) + } } func showController(_ controller: NSViewController) { @@ -195,6 +225,28 @@ private extension ExtensionPointPreferencesViewController { children.removeAll() controller.view.removeFromSuperview() } + + if tableView.selectedRow == -1 { + var helpText = "" + if ExtensionPointManager.shared.availableExtensionPointTypes.count == 0 { + helpText = NSLocalizedString("You've added all available extension points.", comment: "Extension Explainer") + } + else if activeExtensionPoints.count == 0 { + helpText = NSLocalizedString("Add an extension by clicking the + button.", comment: "Extension Explainer") + } else { + helpText = NSLocalizedString("Select an extension or add a new extension point by clicking the + button.", comment: "Extension Explainer") + } + + let textHostingController = NSHostingController(rootView: EnableExtensionPointHelpView(helpText: helpText, preferencesController: self)) + addChild(textHostingController) + textHostingController.view.translatesAutoresizingMaskIntoConstraints = false + detailView.addSubview(textHostingController.view) + detailView.addConstraints([ + NSLayoutConstraint(item: textHostingController.view, attribute: .top, relatedBy: .equal, toItem: detailView, attribute: .top, multiplier: 1, constant: 1), + NSLayoutConstraint(item: textHostingController.view, attribute: .bottom, relatedBy: .equal, toItem: detailView, attribute: .bottom, multiplier: 1, constant: -deleteButton.frame.height), + NSLayoutConstraint(item: textHostingController.view, attribute: .width, relatedBy: .equal, toItem: detailView, attribute: .width, multiplier: 1, constant: 1) + ]) + } } func enableOauth1(_ provider: OAuth1SwiftProvider.Type, extensionPointType: ExtensionPoint.Type) { @@ -254,3 +306,5 @@ private extension ExtensionPointPreferencesViewController { } } + + diff --git a/Mac/Preferences/General/GeneralPrefencesViewController.swift b/Mac/Preferences/General/GeneralPrefencesViewController.swift index 9d4b0c20b..623052044 100644 --- a/Mac/Preferences/General/GeneralPrefencesViewController.swift +++ b/Mac/Preferences/General/GeneralPrefencesViewController.swift @@ -15,10 +15,8 @@ final class GeneralPreferencesViewController: NSViewController { private var userNotificationSettings: UNNotificationSettings? - @IBOutlet var defaultRSSReaderPopup: NSPopUpButton! @IBOutlet var defaultBrowserPopup: NSPopUpButton! @IBOutlet weak var showUnreadCountCheckbox: NSButton! - private var rssReaderInfo = RSSReaderInfo() public override init(nibName nibNameOrNil: NSNib.Name?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) @@ -44,17 +42,6 @@ final class GeneralPreferencesViewController: NSViewController { // MARK: - Actions - @IBAction func rssReaderPopupDidChangeValue(_ sender: Any?) { - guard let menuItem = defaultRSSReaderPopup.selectedItem else { - return - } - guard let bundleID = menuItem.representedObject as? String else { - return - } - registerAppWithBundleID(bundleID) - updateUI() - } - @IBAction func browserPopUpDidChangeValue(_ sender: Any?) { guard let menuItem = defaultBrowserPopup.selectedItem else { return @@ -115,69 +102,10 @@ private extension GeneralPreferencesViewController { } func updateUI() { - rssReaderInfo = RSSReaderInfo() updateBrowserPopup() - updateRSSReaderPopup() updateHideUnreadCountCheckbox() } - func updateRSSReaderPopup() { - // Top item should always be: NetNewsWire (this app) - // Additional items should be sorted alphabetically. - // Any older versions of NetNewsWire should be listed as: NetNewsWire (old version) - - let menu = NSMenu(title: "RSS Readers") - - let netNewsWireBundleID = Bundle.main.bundleIdentifier! - let thisAppParentheticalComment = NSLocalizedString("(this app)", comment: "Preferences default RSS Reader popup") - let thisAppName = "NetNewsWire \(thisAppParentheticalComment)" - let netNewsWireMenuItem = NSMenuItem(title: thisAppName, action: nil, keyEquivalent: "") - netNewsWireMenuItem.representedObject = netNewsWireBundleID - menu.addItem(netNewsWireMenuItem) - - let readersToList = rssReaderInfo.rssReaders.filter { $0.bundleID != netNewsWireBundleID } - let sortedReaders = readersToList.sorted { (reader1, reader2) -> Bool in - return reader1.nameMinusAppSuffix.localizedStandardCompare(reader2.nameMinusAppSuffix) == .orderedAscending - } - - let oldVersionParentheticalComment = NSLocalizedString("(old version)", comment: "Preferences default RSS Reader popup") - for rssReader in sortedReaders { - var appName = rssReader.nameMinusAppSuffix - if appName.contains("NetNewsWire") { - appName = "\(appName) \(oldVersionParentheticalComment)" - } - let menuItem = NSMenuItem(title: appName, action: nil, keyEquivalent: "") - menuItem.representedObject = rssReader.bundleID - menu.addItem(menuItem) - } - - defaultRSSReaderPopup.menu = menu - - func insertAndSelectNoneMenuItem() { - let noneTitle = NSLocalizedString("None", comment: "Preferences default RSS Reader popup") - let menuItem = NSMenuItem(title: noneTitle, action: nil, keyEquivalent: "") - defaultRSSReaderPopup.menu!.insertItem(menuItem, at: 0) - defaultRSSReaderPopup.selectItem(at: 0) - } - - guard let defaultRSSReaderBundleID = rssReaderInfo.defaultRSSReaderBundleID else { - insertAndSelectNoneMenuItem() - return - } - - for menuItem in defaultRSSReaderPopup.menu!.items { - guard let bundleID = menuItem.representedObject as? String else { - continue - } - if bundleID == defaultRSSReaderBundleID { - defaultRSSReaderPopup.select(menuItem) - return - } - } - - insertAndSelectNoneMenuItem() - } - func registerAppWithBundleID(_ bundleID: String) { NSWorkspace.shared.setDefaultAppBundleID(forURLScheme: "feed", to: bundleID) NSWorkspace.shared.setDefaultAppBundleID(forURLScheme: "feeds", to: bundleID) @@ -245,74 +173,3 @@ private extension GeneralPreferencesViewController { } } - -// MARK: - RSSReaderInfo - -private struct RSSReaderInfo { - - let defaultRSSReaderBundleID: String? - let rssReaders: Set - static let feedURLScheme = "feed:" - - init() { - let defaultRSSReaderBundleID = NSWorkspace.shared.defaultAppBundleID(forURLScheme: RSSReaderInfo.feedURLScheme) - self.defaultRSSReaderBundleID = defaultRSSReaderBundleID - self.rssReaders = RSSReaderInfo.fetchRSSReaders(defaultRSSReaderBundleID) - } - - static func fetchRSSReaders(_ defaultRSSReaderBundleID: String?) -> Set { - let rssReaderBundleIDs = NSWorkspace.shared.bundleIDsForApps(forURLScheme: feedURLScheme) - - var rssReaders = Set() - if let defaultRSSReaderBundleID = defaultRSSReaderBundleID, let defaultReader = RSSReader(bundleID: defaultRSSReaderBundleID) { - rssReaders.insert(defaultReader) - } - rssReaderBundleIDs.forEach { (bundleID) in - if let reader = RSSReader(bundleID: bundleID) { - rssReaders.insert(reader) - } - } - return rssReaders - } -} - - -// MARK: - RSSReader - -private struct RSSReader: Hashable { - - let bundleID: String - let name: String - let nameMinusAppSuffix: String - let path: String - - init?(bundleID: String) { - guard let path = NSWorkspace.shared.absolutePathForApplication(withBundleIdentifier: bundleID) else { - return nil - } - - self.path = path - self.bundleID = bundleID - - let name = (path as NSString).lastPathComponent - self.name = name - if name.hasSuffix(".app") { - self.nameMinusAppSuffix = name.stripping(suffix: ".app") - } - else { - self.nameMinusAppSuffix = name - } - } - - // MARK: - Hashable - - func hash(into hasher: inout Hasher) { - hasher.combine(bundleID) - } - - // MARK: - Equatable - - static func ==(lhs: RSSReader, rhs: RSSReader) -> Bool { - return lhs.bundleID == rhs.bundleID - } -} diff --git a/Mac/Resources/Assets.xcassets/shareExtension.imageset/Contents.json b/Mac/Resources/Assets.xcassets/shareExtension.imageset/Contents.json index c61a427dc..c3628e3ae 100644 --- a/Mac/Resources/Assets.xcassets/shareExtension.imageset/Contents.json +++ b/Mac/Resources/Assets.xcassets/shareExtension.imageset/Contents.json @@ -1,12 +1,12 @@ { "images" : [ { - "filename" : "shareExtensionx1.png", + "filename" : "Icon-MacOS-32x32@1x.png.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "shareExtensionx2.png", + "filename" : "Icon-MacOS-32x32@2x.png.png", "idiom" : "universal", "scale" : "2x" }, diff --git a/Mac/Resources/Assets.xcassets/shareExtension.imageset/Icon-MacOS-32x32@1x.png.png b/Mac/Resources/Assets.xcassets/shareExtension.imageset/Icon-MacOS-32x32@1x.png.png new file mode 100644 index 000000000..084985da6 Binary files /dev/null and b/Mac/Resources/Assets.xcassets/shareExtension.imageset/Icon-MacOS-32x32@1x.png.png differ diff --git a/Mac/Resources/Assets.xcassets/shareExtension.imageset/Icon-MacOS-32x32@2x.png.png b/Mac/Resources/Assets.xcassets/shareExtension.imageset/Icon-MacOS-32x32@2x.png.png new file mode 100644 index 000000000..0c1d6e3b4 Binary files /dev/null and b/Mac/Resources/Assets.xcassets/shareExtension.imageset/Icon-MacOS-32x32@2x.png.png differ diff --git a/Mac/Resources/Assets.xcassets/shareExtension.imageset/shareExtensionx1.png b/Mac/Resources/Assets.xcassets/shareExtension.imageset/shareExtensionx1.png deleted file mode 100644 index d93b02e85..000000000 Binary files a/Mac/Resources/Assets.xcassets/shareExtension.imageset/shareExtensionx1.png and /dev/null differ diff --git a/Mac/Resources/Assets.xcassets/shareExtension.imageset/shareExtensionx2.png b/Mac/Resources/Assets.xcassets/shareExtension.imageset/shareExtensionx2.png deleted file mode 100644 index c5debdda8..000000000 Binary files a/Mac/Resources/Assets.xcassets/shareExtension.imageset/shareExtensionx2.png and /dev/null differ diff --git a/Multiplatform/Shared/AppDefaults.swift b/Multiplatform/Shared/AppDefaults.swift index 76c682176..7ad949455 100644 --- a/Multiplatform/Shared/AppDefaults.swift +++ b/Multiplatform/Shared/AppDefaults.swift @@ -72,6 +72,7 @@ final class AppDefaults: ObservableObject { static let confirmMarkAllAsRead = "confirmMarkAllAsRead" // macOS Defaults + static let articleTextSize = "articleTextSize" static let openInBrowserInBackground = "openInBrowserInBackground" static let defaultBrowserID = "defaultBrowserID" static let checkForUpdatesAutomatically = "checkForUpdatesAutomatically" @@ -231,6 +232,16 @@ final class AppDefaults: ObservableObject { @AppStorage(wrappedValue: false, Key.articleFullscreenEnabled, store: store) var articleFullscreenEnabled: Bool + @AppStorage(wrappedValue: 3, Key.articleTextSize, store: store) var articleTextSizeTag: Int { + didSet { + objectWillChange.send() + } + } + + var articleTextSize: ArticleTextSize { + ArticleTextSize(rawValue: articleTextSizeTag) ?? ArticleTextSize.large + } + // MARK: Refresh var lastRefresh: Date? { set { diff --git a/Multiplatform/Shared/Sidebar/SidebarModel.swift b/Multiplatform/Shared/Sidebar/SidebarModel.swift index 2ffd3d8ea..a7f87b52e 100644 --- a/Multiplatform/Shared/Sidebar/SidebarModel.swift +++ b/Multiplatform/Shared/Sidebar/SidebarModel.swift @@ -242,7 +242,7 @@ private extension SidebarModel { /// - Parameter articles: An array of `Article`s. /// - Warning: An `UndoManager` is created here as the `Environment`'s undo manager appears to be `nil`. func markAllAsRead(_ articles: [Article]) { - guard let undoManager = undoManager ?? UndoManager(), + guard let undoManager = undoManager, let markAsReadCommand = MarkStatusCommand(initialArticles: articles, markingRead: true, undoManager: undoManager) else { return } diff --git a/Multiplatform/macOS/Article/SharingServicePickerDelegate.swift b/Multiplatform/macOS/Article/SharingServicePickerDelegate.swift index 19ea8782c..bc6659530 100644 --- a/Multiplatform/macOS/Article/SharingServicePickerDelegate.swift +++ b/Multiplatform/macOS/Article/SharingServicePickerDelegate.swift @@ -20,7 +20,8 @@ import RSCore } func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, sharingServicesForItems items: [Any], proposedSharingServices proposedServices: [NSSharingService]) -> [NSSharingService] { - return proposedServices + SharingServicePickerDelegate.customSharingServices(for: items) + let filteredServices = proposedServices.filter { $0.menuItemTitle != "NetNewsWire" } + return filteredServices + SharingServicePickerDelegate.customSharingServices(for: items) } func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, delegateFor sharingService: NSSharingService) -> NSSharingServiceDelegate? { diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 6ce989f16..b9da8c139 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -9,6 +9,10 @@ /* Begin PBXBuildFile section */ 1704053424E5985A00A00787 /* SceneNavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1704053324E5985A00A00787 /* SceneNavigationModel.swift */; }; 1704053524E5985A00A00787 /* SceneNavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1704053324E5985A00A00787 /* SceneNavigationModel.swift */; }; + 1710B9132552354E00679C0D /* AddAccountHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1710B9122552354E00679C0D /* AddAccountHelpView.swift */; }; + 1710B9142552354E00679C0D /* AddAccountHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1710B9122552354E00679C0D /* AddAccountHelpView.swift */; }; + 1710B929255246F900679C0D /* EnableExtensionPointHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1710B928255246F900679C0D /* EnableExtensionPointHelpView.swift */; }; + 1710B92A255246F900679C0D /* EnableExtensionPointHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1710B928255246F900679C0D /* EnableExtensionPointHelpView.swift */; }; 1717535624BADF33004498C6 /* GeneralPreferencesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1717535524BADF33004498C6 /* GeneralPreferencesModel.swift */; }; 171BCB8C24CB08A3006E22D9 /* FixAccountCredentialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171BCB8B24CB08A3006E22D9 /* FixAccountCredentialView.swift */; }; 171BCB8D24CB08A3006E22D9 /* FixAccountCredentialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171BCB8B24CB08A3006E22D9 /* FixAccountCredentialView.swift */; }; @@ -495,6 +499,12 @@ 51D5948722668EFA00DFC836 /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */; }; 51D6A5BC23199C85001C27D8 /* MasterTimelineDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */; }; 51D87EE12311D34700E63F03 /* ActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D87EE02311D34700E63F03 /* ActivityType.swift */; }; + 51DC07982552083500A3F79F /* ArticleTextSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC07972552083500A3F79F /* ArticleTextSize.swift */; }; + 51DC07992552083500A3F79F /* ArticleTextSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC07972552083500A3F79F /* ArticleTextSize.swift */; }; + 51DC079A2552083500A3F79F /* ArticleTextSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC07972552083500A3F79F /* ArticleTextSize.swift */; }; + 51DC079B2552083500A3F79F /* ArticleTextSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC07972552083500A3F79F /* ArticleTextSize.swift */; }; + 51DC079C2552083500A3F79F /* ArticleTextSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC07972552083500A3F79F /* ArticleTextSize.swift */; }; + 51DC07AC255209E200A3F79F /* ArticleTextSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC07972552083500A3F79F /* ArticleTextSize.swift */; }; 51DC37072402153E0095D371 /* UpdateSelectionOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC37062402153E0095D371 /* UpdateSelectionOperation.swift */; }; 51DC37092402F1470095D371 /* MasterFeedDataSourceOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC37082402F1470095D371 /* MasterFeedDataSourceOperation.swift */; }; 51DC370B2405BC9A0095D371 /* PreloadedWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC370A2405BC9A0095D371 /* PreloadedWebView.swift */; }; @@ -1434,6 +1444,8 @@ /* Begin PBXFileReference section */ 1704053324E5985A00A00787 /* SceneNavigationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneNavigationModel.swift; sourceTree = ""; }; + 1710B9122552354E00679C0D /* AddAccountHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccountHelpView.swift; sourceTree = ""; }; + 1710B928255246F900679C0D /* EnableExtensionPointHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnableExtensionPointHelpView.swift; sourceTree = ""; }; 1717535524BADF33004498C6 /* GeneralPreferencesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralPreferencesModel.swift; sourceTree = ""; }; 171BCB8B24CB08A3006E22D9 /* FixAccountCredentialView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FixAccountCredentialView.swift; sourceTree = ""; }; 172199C824AB228900A31D04 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; @@ -1730,6 +1742,7 @@ 51CE1C0A23622006005548FC /* RefreshProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshProgressView.swift; sourceTree = ""; }; 51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineDataSource.swift; sourceTree = ""; }; 51D87EE02311D34700E63F03 /* ActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityType.swift; sourceTree = ""; }; + 51DC07972552083500A3F79F /* ArticleTextSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleTextSize.swift; sourceTree = ""; }; 51DC37062402153E0095D371 /* UpdateSelectionOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateSelectionOperation.swift; sourceTree = ""; }; 51DC37082402F1470095D371 /* MasterFeedDataSourceOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedDataSourceOperation.swift; sourceTree = ""; }; 51DC370A2405BC9A0095D371 /* PreloadedWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreloadedWebView.swift; sourceTree = ""; }; @@ -2304,6 +2317,7 @@ isa = PBXGroup; children = ( 5183CFAE254C78C8006B83A5 /* EnableExtensionPointView.swift */, + 1710B928255246F900679C0D /* EnableExtensionPointHelpView.swift */, 515A516D243E7F950089E588 /* ExtensionPointDetail.xib */, 515A5170243E802B0089E588 /* ExtensionPointDetailViewController.swift */, 515A5147243E64BA0089E588 /* ExtensionPointEnableWindowController.swift */, @@ -2804,11 +2818,12 @@ 51C452A822650DA100C03939 /* Article Rendering */ = { isa = PBXGroup; children = ( - 849A977D1ED9EC42007D329B /* ArticleRenderer.swift */, + B27EEBDF244D15F2000932E6 /* shared.css */, + 848362FE2262A30E00DA1D35 /* template.html */, 517630032336215100E15FFF /* main.js */, 49F40DEF2335B71000552BF4 /* newsfoot.js */, - 848362FE2262A30E00DA1D35 /* template.html */, - B27EEBDF244D15F2000932E6 /* shared.css */, + 849A977D1ED9EC42007D329B /* ArticleRenderer.swift */, + 51DC07972552083500A3F79F /* ArticleTextSize.swift */, ); path = "Article Rendering"; sourceTree = ""; @@ -3328,6 +3343,7 @@ isa = PBXGroup; children = ( 178A9F9C2549449F00AB7E9D /* AddAccountsView.swift */, + 1710B9122552354E00679C0D /* AddAccountHelpView.swift */, 84C9FC7222629E1200D921D6 /* AccountsPreferencesViewController.swift */, 84C9FC7422629E1200D921D6 /* AccountsDetail.xib */, 5144EA2E2279FAB600D19003 /* AccountsDetailViewController.swift */, @@ -4424,6 +4440,7 @@ 511B149A24E5DC5400C919BD /* RefreshInterval.swift in Sources */, 510C418324E5D1B4008226FD /* ExtensionContainersFile.swift in Sources */, 510C418524E5D1B4008226FD /* ExtensionContainers.swift in Sources */, + 51DC07AC255209E200A3F79F /* ArticleTextSize.swift in Sources */, 511B149824E5DC2300C919BD /* ShareDefaultContainer.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4485,6 +4502,7 @@ 51E4996A24A8762D00B667CB /* ExtractedArticle.swift in Sources */, 51919FF124AB864A00541E64 /* TimelineModel.swift in Sources */, 5194736E24BBB937001A2939 /* HiddenModifier.swift in Sources */, + 51DC079B2552083500A3F79F /* ArticleTextSize.swift in Sources */, 51E498F124A8085D00B667CB /* StarredFeedDelegate.swift in Sources */, 51E498FF24A808BB00B667CB /* SingleFaviconDownloader.swift in Sources */, 51E4997224A8784300B667CB /* DefaultFeedsImporter.swift in Sources */, @@ -4668,6 +4686,7 @@ 51E4992224A8095600B667CB /* URL-Extensions.swift in Sources */, 51E4990424A808C300B667CB /* WebFeedIconDownloader.swift in Sources */, 51E498CB24A8085D00B667CB /* TodayFeedDelegate.swift in Sources */, + 51DC079C2552083500A3F79F /* ArticleTextSize.swift in Sources */, 51B80F1F24BE531200C6C32D /* SharingServiceView.swift in Sources */, 17D232A924AFF10A0005F075 /* AddWebFeedModel.swift in Sources */, 51A8005224CC453C00F41F1D /* ReplaySubject.swift in Sources */, @@ -4794,11 +4813,13 @@ 65ED3FBD235DEF6C0081F399 /* AppDefaults.swift in Sources */, 65ED3FBE235DEF6C0081F399 /* Account+Scriptability.swift in Sources */, 65ED3FBF235DEF6C0081F399 /* NothingInspectorViewController.swift in Sources */, + 1710B92A255246F900679C0D /* EnableExtensionPointHelpView.swift in Sources */, 65ED3FC0235DEF6C0081F399 /* AppNotifications.swift in Sources */, 65ED3FC1235DEF6C0081F399 /* TimelineKeyboardDelegate.swift in Sources */, 65ED3FC2235DEF6C0081F399 /* Browser.swift in Sources */, 65ED3FC3235DEF6C0081F399 /* DetailWebViewController.swift in Sources */, 65ED3FC4235DEF6C0081F399 /* OPMLExporter.swift in Sources */, + 51DC07992552083500A3F79F /* ArticleTextSize.swift in Sources */, 65ED3FC5235DEF6C0081F399 /* MainWindowController.swift in Sources */, 65ED3FC6235DEF6C0081F399 /* UnreadFeed.swift in Sources */, 65ED3FC8235DEF6C0081F399 /* SidebarCellLayout.swift in Sources */, @@ -4863,6 +4884,7 @@ 65ED3FF7235DEF6C0081F399 /* SearchFeedDelegate.swift in Sources */, 65ED3FF8235DEF6C0081F399 /* ErrorHandler.swift in Sources */, 65ED3FF9235DEF6C0081F399 /* ActivityManager.swift in Sources */, + 1710B9142552354E00679C0D /* AddAccountHelpView.swift in Sources */, 65ED3FFA235DEF6C0081F399 /* WebFeedInspectorViewController.swift in Sources */, 65ED3FFB235DEF6C0081F399 /* AccountsReaderAPIWindowController.swift in Sources */, 65ED3FFC235DEF6C0081F399 /* AccountsAddLocalWindowController.swift in Sources */, @@ -5049,6 +5071,7 @@ 51C452AE2265104D00C03939 /* ArticleStringFormatter.swift in Sources */, 512E08E62268800D00BDCFDD /* FolderTreeControllerDelegate.swift in Sources */, 51A9A60A2382FD240033AADF /* PoppableGestureRecognizerDelegate.swift in Sources */, + 51DC079A2552083500A3F79F /* ArticleTextSize.swift in Sources */, 51C4529922650A0000C03939 /* ArticleStylesManager.swift in Sources */, 51EF0F802277A8330050506E /* MasterTimelineCellLayout.swift in Sources */, 51F85BF722749FA100C787DC /* UIFont-Extensions.swift in Sources */, @@ -5179,6 +5202,7 @@ D5907D7F2004AC00005947E5 /* NSApplication+Scriptability.swift in Sources */, 8405DD9C22153BD7008CE1BF /* NSView-Extensions.swift in Sources */, 849A979F1ED9F130007D329B /* SidebarCell.swift in Sources */, + 1710B929255246F900679C0D /* EnableExtensionPointHelpView.swift in Sources */, 515A50E6243D07A90089E588 /* ExtensionPointManager.swift in Sources */, 51E595A5228CC36500FCC42B /* ArticleStatusSyncTimer.swift in Sources */, 515A5177243E90200089E588 /* ExtensionPointIdentifer.swift in Sources */, @@ -5223,6 +5247,7 @@ 5144EA382279FC6200D19003 /* AccountsAddLocalWindowController.swift in Sources */, 84AD1EAA2031617300BC20B7 /* PasteboardFolder.swift in Sources */, 515A5148243E64BA0089E588 /* ExtensionPointEnableWindowController.swift in Sources */, + 51DC07982552083500A3F79F /* ArticleTextSize.swift in Sources */, 5117715524E1EA0F00A2A836 /* ArticleExtractorButton.swift in Sources */, 5103A9F724225E4C00410853 /* AccountsAddCloudKitWindowController.swift in Sources */, 5144EA51227B8E4500D19003 /* AccountsFeedbinWindowController.swift in Sources */, @@ -5234,6 +5259,7 @@ 511B9806237DCAC90028BCAA /* UserInfoKey.swift in Sources */, 84C9FC7722629E1200D921D6 /* AdvancedPreferencesViewController.swift in Sources */, 849EE72120391F560082A1EA /* SharingServicePickerDelegate.swift in Sources */, + 1710B9132552354E00679C0D /* AddAccountHelpView.swift in Sources */, 5108F6B62375E612001ABC45 /* CacheCleaner.swift in Sources */, 849A97981ED9EFAA007D329B /* Node-Extensions.swift in Sources */, 849EE70F203919360082A1EA /* AppAssets.swift in Sources */, diff --git a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index eee47f843..8e5c04af6 100644 --- a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -69,8 +69,8 @@ "repositoryURL": "https://github.com/Ranchero-Software/RSParser.git", "state": { "branch": null, - "revision": "21d57ffb7ae744cf70bf6ddfb7ad8b7c102e05cf", - "version": "2.0.0-beta2" + "revision": "a4467cb6ab32d67fa8b09fcef8b234c7f96b7f9c", + "version": "2.0.0-beta3" } }, { diff --git a/Shared/AccountType+Helpers.swift b/Shared/AccountType+Helpers.swift index ac40239bc..fe61b9043 100644 --- a/Shared/AccountType+Helpers.swift +++ b/Shared/AccountType+Helpers.swift @@ -52,8 +52,6 @@ extension AccountType { return NSLocalizedString("NewsBlur", comment: "Account name") case .theOldReader: return NSLocalizedString("The Old Reader", comment: "Account name") - default: - return "" } } diff --git a/Shared/Article Rendering/ArticleRenderer.swift b/Shared/Article Rendering/ArticleRenderer.swift index 7d15c6482..16e601462 100644 --- a/Shared/Article Rendering/ArticleRenderer.swift +++ b/Shared/Article Rendering/ArticleRenderer.swift @@ -276,12 +276,7 @@ private extension ArticleRenderer { #else func styleSubstitutions() -> [String: String] { var d = [String: String]() - - if #available(macOS 11.0, *) { - let bodyFont = NSFont.preferredFont(forTextStyle: .body) - d["font-size"] = String(describing: Int(round(bodyFont.pointSize * 1.33))) - } - + d["font-size"] = String(describing: AppDefaults.shared.articleTextSize.fontSize) return d } #endif diff --git a/Shared/Article Rendering/ArticleTextSize.swift b/Shared/Article Rendering/ArticleTextSize.swift new file mode 100644 index 000000000..6911e8ba1 --- /dev/null +++ b/Shared/Article Rendering/ArticleTextSize.swift @@ -0,0 +1,52 @@ +// +// ArticleTextSize.swift +// NetNewsWire +// +// Created by Maurice Parker on 11/3/20. +// Copyright © 2020 Ranchero Software. All rights reserved. +// + +import Foundation + +enum ArticleTextSize: Int, CaseIterable, Identifiable { + case small = 1 + case medium = 2 + case large = 3 + case xlarge = 4 + case xxlarge = 5 + + #if os(macOS) + var fontSize: Int { + switch self { + case .small: + return 14 + case .medium: + return 16 + case .large: + return 18 + case .xlarge: + return 20 + case .xxlarge: + return 22 + } + } + #endif + + var id: String { description() } + + func description() -> String { + switch self { + case .small: + return NSLocalizedString("Small", comment: "Small") + case .medium: + return NSLocalizedString("Medium", comment: "Medium") + case .large: + return NSLocalizedString("Large", comment: "Large") + case .xlarge: + return NSLocalizedString("X-Large", comment: "X-Large") + case .xxlarge: + return NSLocalizedString("XX-Large", comment: "XX-Large") + } + } + +} diff --git a/Shared/ExtensionPoints/RedditFeedProvider-Extensions.swift b/Shared/ExtensionPoints/RedditFeedProvider-Extensions.swift index 86981f547..0f4731e01 100644 --- a/Shared/ExtensionPoints/RedditFeedProvider-Extensions.swift +++ b/Shared/ExtensionPoints/RedditFeedProvider-Extensions.swift @@ -16,7 +16,7 @@ extension RedditFeedProvider: ExtensionPoint { static var title = NSLocalizedString("Reddit", comment: "Reddit") static var image = AppAssets.extensionPointReddit static var description: NSAttributedString = { - return RedditFeedProvider.makeAttrString("This extension enables you to subscribe to Reddit URL's as if they were RSS feeds. It only works with \(Account.defaultLocalAccountName) or iCloud accounts.") + return RedditFeedProvider.makeAttrString("This extension enables you to subscribe to Reddit URLs as if they were RSS feeds. It only works with \(Account.defaultLocalAccountName) or iCloud accounts.") }() var extensionPointID: ExtensionPointIdentifer { diff --git a/Shared/ExtensionPoints/SendToMarsEditCommand.swift b/Shared/ExtensionPoints/SendToMarsEditCommand.swift index 298ff5746..12f1eda2c 100644 --- a/Shared/ExtensionPoints/SendToMarsEditCommand.swift +++ b/Shared/ExtensionPoints/SendToMarsEditCommand.swift @@ -17,7 +17,7 @@ final class SendToMarsEditCommand: ExtensionPoint, SendToCommand { static var title = NSLocalizedString("MarsEdit", comment: "MarsEdit") static var image = AppAssets.extensionPointMarsEdit static var description: NSAttributedString = { - let attrString = SendToMarsEditCommand.makeAttrString("This extension enables share menu functionality to send selected article text to MarsEdit. You need the MarsEdit application for this to work.") + let attrString = SendToMarsEditCommand.makeAttrString("This extension enables share menu functionality to send selected article text to MarsEdit. You need the MarsEdit application for this to work.") let range = NSRange(location: 81, length: 8) attrString.beginEditing() attrString.addAttribute(NSAttributedString.Key.link, value: "https://red-sweater.com/marsedit/", range: range) diff --git a/Shared/ExtensionPoints/SendToMicroBlogCommand.swift b/Shared/ExtensionPoints/SendToMicroBlogCommand.swift index c4ebeddfe..1c8bc031b 100644 --- a/Shared/ExtensionPoints/SendToMicroBlogCommand.swift +++ b/Shared/ExtensionPoints/SendToMicroBlogCommand.swift @@ -19,7 +19,7 @@ final class SendToMicroBlogCommand: ExtensionPoint, SendToCommand { static var title: String = NSLocalizedString("Micro.blog", comment: "Micro.blog") static var image = AppAssets.extensionPointMicroblog static var description: NSAttributedString = { - let attrString = SendToMicroBlogCommand.makeAttrString("This extension enables share menu functionality to send selected article text to Micro.blog. You need the Micro.blog application for this to work.") + let attrString = SendToMicroBlogCommand.makeAttrString("This extension enables share menu functionality to send selected article text to Micro.blog. You need the Micro.blog application for this to work.") let range = NSRange(location: 81, length: 10) attrString.beginEditing() attrString.addAttribute(NSAttributedString.Key.link, value: "https://micro.blog", range: range) diff --git a/Shared/ExtensionPoints/TwitterFeedProvider-Extensions.swift b/Shared/ExtensionPoints/TwitterFeedProvider-Extensions.swift index d38917b66..0e538066d 100644 --- a/Shared/ExtensionPoints/TwitterFeedProvider-Extensions.swift +++ b/Shared/ExtensionPoints/TwitterFeedProvider-Extensions.swift @@ -16,7 +16,7 @@ extension TwitterFeedProvider: ExtensionPoint { static var title = NSLocalizedString("Twitter", comment: "Twitter") static var image = AppAssets.extensionPointTwitter static var description: NSAttributedString = { - return TwitterFeedProvider.makeAttrString("This extension enables you to subscribe to Twitter URL's as if they were RSS feeds. It only works with \(Account.defaultLocalAccountName) or iCloud accounts.") + return TwitterFeedProvider.makeAttrString("This extension enables you to subscribe to Twitter URLs as if they were RSS feeds. It only works with \(Account.defaultLocalAccountName) or iCloud accounts.") }() var extensionPointID: ExtensionPointIdentifer { diff --git a/Shared/Timeline/ArticleArray.swift b/Shared/Timeline/ArticleArray.swift index 391963424..104f06f8c 100644 --- a/Shared/Timeline/ArticleArray.swift +++ b/Shared/Timeline/ArticleArray.swift @@ -20,18 +20,21 @@ extension Array where Element == Article { return self[row] } - func rowOfNextUnreadArticle(_ selectedRow: Int) -> Int? { + func orderedRowIndexes(fromIndex startIndex: Int, wrappingToTop wrapping: Bool) -> [Int] { + if startIndex >= self.count { + // Wrap around to the top if specified + return wrapping ? Array(0..(startIndex..(0.. Int? { if isEmpty { return nil } - var rowIndex = selectedRow - while(true) { - - rowIndex = rowIndex + 1 - if rowIndex >= count { - break - } + for rowIndex in orderedRowIndexes(fromIndex: selectedRow + 1, wrappingToTop: wrapping) { let article = articleAtRow(rowIndex)! if !article.status.read { return rowIndex diff --git a/iOS/Account/Account.storyboard b/iOS/Account/Account.storyboard index 316eabcfc..5d42f4d88 100644 --- a/iOS/Account/Account.storyboard +++ b/iOS/Account/Account.storyboard @@ -627,7 +627,7 @@ - + diff --git a/iOS/Account/FeedbinAccountViewController.swift b/iOS/Account/FeedbinAccountViewController.swift index ae769aba3..270fb969f 100644 --- a/iOS/Account/FeedbinAccountViewController.swift +++ b/iOS/Account/FeedbinAccountViewController.swift @@ -82,7 +82,7 @@ class FeedbinAccountViewController: UITableViewController { // When you fill in the email address via auto-complete it adds extra whitespace let trimmedEmail = email.trimmingCharacters(in: .whitespaces) - guard !AccountManager.shared.duplicateServiceAccount(type: .feedbin, username: trimmedEmail) else { + guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: .feedbin, username: trimmedEmail) else { showError(NSLocalizedString("There is already a Feedbin account with that username created.", comment: "Duplicate Error")) return } diff --git a/iOS/AppAssets.swift b/iOS/AppAssets.swift index a9af4dfea..b260617c1 100644 --- a/iOS/AppAssets.swift +++ b/iOS/AppAssets.swift @@ -154,7 +154,7 @@ struct AppAssets { }() static var masterFolderImage: IconImage = { - return IconImage(UIImage(systemName: "folder.fill")!) + return IconImage(UIImage(systemName: "folder.fill")!, isSymbol: true) }() static var moreImage: UIImage = { @@ -186,7 +186,7 @@ struct AppAssets { }() static var searchFeedImage: IconImage = { - return IconImage(UIImage(systemName: "magnifyingglass")!) + return IconImage(UIImage(systemName: "magnifyingglass")!, isSymbol: true) }() static var secondaryAccentColor: UIColor = { diff --git a/iOS/IconView.swift b/iOS/IconView.swift index 7ce9faf3f..a7d9f3e28 100644 --- a/iOS/IconView.swift +++ b/iOS/IconView.swift @@ -52,7 +52,7 @@ final class IconView: UIView { } private var isSymbolImage: Bool { - return imageView.image?.isSymbolImage ?? false + return iconImage?.isSymbol ?? false } override init(frame: CGRect) { @@ -75,7 +75,7 @@ final class IconView: UIView { override func layoutSubviews() { imageView.setFrameIfNotEqual(rectForImageView()) - if (iconImage != nil && isVerticalBackgroundExposed && !isSymbolImage) || !isDisconcernable { + if !isSymbolImage && ((iconImage != nil && isVerticalBackgroundExposed) || !isDisconcernable) { backgroundColor = AppAssets.iconBackgroundColor } else { backgroundColor = nil diff --git a/iOS/MasterFeed/MasterFeedViewController+Drop.swift b/iOS/MasterFeed/MasterFeedViewController+Drop.swift index 32fab4f7d..95e47c01b 100644 --- a/iOS/MasterFeed/MasterFeedViewController+Drop.swift +++ b/iOS/MasterFeed/MasterFeedViewController+Drop.swift @@ -18,13 +18,16 @@ extension MasterFeedViewController: UITableViewDropDelegate { } func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal { - guard let destIndexPath = destinationIndexPath, - destIndexPath.section > 0, - tableView.hasActiveDrag, - let destIdentifier = dataSource.itemIdentifier(for: destIndexPath), - let destAccount = destIdentifier.account, - let destCell = tableView.cellForRow(at: destIndexPath) else { - return UITableViewDropProposal(operation: .forbidden) + guard let destIndexPath = destinationIndexPath, destIndexPath.section > 0, tableView.hasActiveDrag else { + return UITableViewDropProposal(operation: .forbidden) + } + + guard let destIdentifier = dataSource.itemIdentifier(for: destIndexPath) else { + return UITableViewDropProposal(operation: .move, intent: .insertIntoDestinationIndexPath) + } + + guard let destAccount = destIdentifier.account, let destCell = tableView.cellForRow(at: destIndexPath) else { + return UITableViewDropProposal(operation: .forbidden) } // Validate account specific behaviors... @@ -90,7 +93,8 @@ extension MasterFeedViewController: UITableViewDropDelegate { if let containerID = destIdentifier?.containerID ?? destIdentifier?.parentContainerID { return AccountManager.shared.existingContainer(with: containerID) } else { - return nil + // If we got here, we are trying to drop on an empty section header. Go and find the Account for this section + return coordinator.rootNode.childAtIndex(destIndexPath.section)?.representedObject as? Account } }() diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index 775968271..6630bea81 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -337,8 +337,8 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { return proposedDestinationIndexPath } - // If this is a folder and isn't expanded or doesn't have any entries, let the users drop on it - if destNode.representedObject is Folder && (destNode.numberOfChildNodes == 0 || !coordinator.isExpanded(destNode)) { + // If this is a folder, let the users drop on it + if destNode.representedObject is Folder { return proposedDestinationIndexPath } diff --git a/iOS/SceneDelegate.swift b/iOS/SceneDelegate.swift index 70c95a717..eb6a6438d 100644 --- a/iOS/SceneDelegate.swift +++ b/iOS/SceneDelegate.swift @@ -114,11 +114,11 @@ private extension SceneDelegate { DispatchQueue.main.async { switch AppDefaults.userInterfaceColorPalette { case .automatic: - self.window!.overrideUserInterfaceStyle = .unspecified + self.window?.overrideUserInterfaceStyle = .unspecified case .light: - self.window!.overrideUserInterfaceStyle = .light + self.window?.overrideUserInterfaceStyle = .light case .dark: - self.window!.overrideUserInterfaceStyle = .dark + self.window?.overrideUserInterfaceStyle = .dark } } } diff --git a/xcconfig/NetNewsWire_multiplatform_macOSapp_target.xcconfig b/xcconfig/NetNewsWire_multiplatform_macOSapp_target.xcconfig index 8f1fc3a62..33cdf6669 100644 --- a/xcconfig/NetNewsWire_multiplatform_macOSapp_target.xcconfig +++ b/xcconfig/NetNewsWire_multiplatform_macOSapp_target.xcconfig @@ -41,6 +41,6 @@ PRODUCT_BUNDLE_IDENTIFIER = $(ORGANIZATION_IDENTIFIER).NetNewsWire-Evergreen PRODUCT_NAME = NetNewsWire // Override NetNewsWire_project.xcconfig until we are ready to only target 10.16 -MACOSX_DEPLOYMENT_TARGET = 10.16 +MACOSX_DEPLOYMENT_TARGET = 11.0 SWIFT_SWIFT3_OBJC_INFERENCE = Off SWIFT_VERSION = 5.3 diff --git a/xcconfig/common/NetNewsWire_ios_target_common.xcconfig b/xcconfig/common/NetNewsWire_ios_target_common.xcconfig index 5069c364b..7a5663a23 100644 --- a/xcconfig/common/NetNewsWire_ios_target_common.xcconfig +++ b/xcconfig/common/NetNewsWire_ios_target_common.xcconfig @@ -1,7 +1,7 @@ // High Level Settings common to both the iOS application and any extensions we bundle with it -MARKETING_VERSION = 5.0.4 -CURRENT_PROJECT_VERSION = 53 +MARKETING_VERSION = 5.0.5 +CURRENT_PROJECT_VERSION = 56 ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon diff --git a/xcconfig/common/NetNewsWire_mac_target_common.xcconfig b/xcconfig/common/NetNewsWire_mac_target_common.xcconfig index 2e9918ef0..da8798f37 100644 --- a/xcconfig/common/NetNewsWire_mac_target_common.xcconfig +++ b/xcconfig/common/NetNewsWire_mac_target_common.xcconfig @@ -3,6 +3,7 @@ MARKETING_VERSION = 5.1.2 CURRENT_PROJECT_VERSION = 3016 ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO +ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon COMBINE_HIDPI_IMAGES = YES MACOSX_DEPLOYMENT_TARGET = 10.15