diff --git a/Account/Package.swift b/Account/Package.swift
index e80cec9d3..cafc3933c 100644
--- a/Account/Package.swift
+++ b/Account/Package.swift
@@ -2,7 +2,7 @@
import PackageDescription
var dependencies: [Package.Dependency] = [
- .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "1.0.0")),
+ .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "1.1.0")),
.package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "1.0.0")),
.package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")),
.package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")),
diff --git a/Account/Sources/Account/Account.swift b/Account/Sources/Account/Account.swift
index 24f2a29f2..2f56cac0c 100644
--- a/Account/Sources/Account/Account.swift
+++ b/Account/Sources/Account/Account.swift
@@ -153,7 +153,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
public var sortedFolders: [Folder]? {
if let folders = folders {
- return Array(folders).sorted(by: { $0.nameForDisplay.caseInsensitiveCompare($1.nameForDisplay) == .orderedAscending })
+ return folders.sorted()
}
return nil
}
@@ -517,10 +517,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
addOPMLItems(OPMLNormalizer.normalize(items))
}
- public func markArticles(_ articles: Set, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping (Result) -> Void) {
- delegate.markArticles(for: self, articles: articles, statusKey: statusKey, flag: flag, completion: completion)
- }
-
func existingContainer(withExternalID externalID: String) -> Container? {
guard self.externalID != externalID else {
return self
@@ -639,6 +635,10 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
delegate.restoreFolder(for: self, folder: folder, completion: completion)
}
+ public func mark(articles: Set, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping (Result) -> Void) {
+ delegate.markArticles(for: self, articles: articles, statusKey: statusKey, flag: flag, completion: completion)
+ }
+
func clearWebFeedMetadata(_ feed: WebFeed) {
webFeedMetadata[feed.url] = nil
}
@@ -832,48 +832,46 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
completion?(nil)
}
}
-
+
/// Mark articleIDs statuses based on statusKey and flag.
/// Will create statuses in the database and in memory as needed. Sends a .StatusesDidChange notification.
- /// Returns a set of new article statuses.
- func markAndFetchNew(articleIDs: Set, statusKey: ArticleStatus.Key, flag: Bool, completion: ArticleIDsCompletionBlock? = nil) {
+ func mark(articleIDs: Set, statusKey: ArticleStatus.Key, flag: Bool, completion: DatabaseCompletionBlock? = nil) {
guard !articleIDs.isEmpty else {
- completion?(.success(Set()))
+ completion?(nil)
return
}
- database.markAndFetchNew(articleIDs: articleIDs, statusKey: statusKey, flag: flag) { result in
- switch result {
- case .success(let newArticleStatusIDs):
+ database.mark(articleIDs: articleIDs, statusKey: statusKey, flag: flag) { databaseError in
+ if let databaseError = databaseError {
+ completion?(databaseError)
+ } else {
self.noteStatusesForArticleIDsDidChange(articleIDs: articleIDs, statusKey: statusKey, flag: flag)
- completion?(.success(newArticleStatusIDs))
- case .failure(let databaseError):
- completion?(.failure(databaseError))
+ completion?(nil)
}
}
}
/// Mark articleIDs as read. Will create statuses in the database and in memory as needed. Sends a .StatusesDidChange notification.
/// Returns a set of new article statuses.
- func markAsRead(_ articleIDs: Set, completion: ArticleIDsCompletionBlock? = nil) {
- markAndFetchNew(articleIDs: articleIDs, statusKey: .read, flag: true, completion: completion)
+ func markAsRead(_ articleIDs: Set, completion: DatabaseCompletionBlock? = nil) {
+ mark(articleIDs: articleIDs, statusKey: .read, flag: true, completion: completion)
}
/// Mark articleIDs as unread. Will create statuses in the database and in memory as needed. Sends a .StatusesDidChange notification.
/// Returns a set of new article statuses.
- func markAsUnread(_ articleIDs: Set, completion: ArticleIDsCompletionBlock? = nil) {
- markAndFetchNew(articleIDs: articleIDs, statusKey: .read, flag: false, completion: completion)
+ func markAsUnread(_ articleIDs: Set, completion: DatabaseCompletionBlock? = nil) {
+ mark(articleIDs: articleIDs, statusKey: .read, flag: false, completion: completion)
}
/// Mark articleIDs as starred. Will create statuses in the database and in memory as needed. Sends a .StatusesDidChange notification.
/// Returns a set of new article statuses.
- func markAsStarred(_ articleIDs: Set, completion: ArticleIDsCompletionBlock? = nil) {
- markAndFetchNew(articleIDs: articleIDs, statusKey: .starred, flag: true, completion: completion)
+ func markAsStarred(_ articleIDs: Set, completion: DatabaseCompletionBlock? = nil) {
+ mark(articleIDs: articleIDs, statusKey: .starred, flag: true, completion: completion)
}
/// Mark articleIDs as unstarred. Will create statuses in the database and in memory as needed. Sends a .StatusesDidChange notification.
/// Returns a set of new article statuses.
- func markAsUnstarred(_ articleIDs: Set, completion: ArticleIDsCompletionBlock? = nil) {
- markAndFetchNew(articleIDs: articleIDs, statusKey: .starred, flag: false, completion: completion)
+ func markAsUnstarred(_ articleIDs: Set, completion: DatabaseCompletionBlock? = nil) {
+ mark(articleIDs: articleIDs, statusKey: .starred, flag: false, completion: completion)
}
// Delete the articles associated with the given set of articleIDs
diff --git a/Account/Sources/Account/AccountManager.swift b/Account/Sources/Account/AccountManager.swift
index 07bd893d9..836cf0603 100644
--- a/Account/Sources/Account/AccountManager.swift
+++ b/Account/Sources/Account/AccountManager.swift
@@ -70,6 +70,9 @@ public final class AccountManager: UnreadCountProvider {
if lastArticleFetchEndTime == nil || lastArticleFetchEndTime! < accountLastArticleFetchEndTime {
lastArticleFetchEndTime = accountLastArticleFetchEndTime
}
+ } else {
+ lastArticleFetchEndTime = nil
+ break
}
}
return lastArticleFetchEndTime
diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift
index bcd4689bf..07079b5fe 100644
--- a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift
+++ b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift
@@ -426,6 +426,7 @@ final class CloudKitAccountDelegate: AccountDelegate, Logging {
func accountDidInitialize(_ account: Account) {
self.account = account
+ refreshProgress.name = account.nameForDisplay
accountZone.delegate = CloudKitAcountZoneDelegate(account: account, refreshProgress: refreshProgress, articlesZone: articlesZone)
articlesZone.delegate = CloudKitArticlesZoneDelegate(account: account, database: database, articlesZone: articlesZone)
@@ -484,6 +485,7 @@ private extension CloudKitAccountDelegate {
completion(.failure(error))
}
+ refreshProgress.isIndeterminate = true
refreshProgress.addToNumberOfTasksAndRemaining(3)
accountZone.fetchChangesInZone() { result in
self.refreshProgress.completeTask()
@@ -495,6 +497,7 @@ private extension CloudKitAccountDelegate {
case .success:
self.refreshArticleStatus(for: account) { result in
self.refreshProgress.completeTask()
+ self.refreshProgress.isIndeterminate = false
switch result {
case .success:
@@ -522,6 +525,7 @@ private extension CloudKitAccountDelegate {
func standardRefreshAll(for account: Account, completion: @escaping (Result) -> Void) {
let intialWebFeedsCount = account.flattenedWebFeeds().count
+ refreshProgress.isIndeterminate = true
refreshProgress.addToNumberOfTasksAndRemaining(3 + intialWebFeedsCount)
func fail(_ error: Error) {
@@ -542,6 +546,7 @@ private extension CloudKitAccountDelegate {
switch result {
case .success:
self.refreshProgress.completeTask()
+ self.refreshProgress.isIndeterminate = false
self.combinedRefresh(account, webFeeds) { result in
self.sendArticleStatus(for: account, showProgress: true) { _ in
self.refreshProgress.clear()
@@ -799,7 +804,7 @@ private extension CloudKitAccountDelegate {
self.sendArticleStatus(for: account, showProgress: true) { result in
switch result {
case .success:
- self.articlesZone.fetchChangesInZone() { _ in }
+ self.refreshArticleStatus(for: account) { _ in }
case .failure(let error):
self.logger.error("CloudKit Feed send articles error: \(error.localizedDescription, privacy: .public)")
}
diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift b/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift
index e5f971c55..e25f23d8c 100644
--- a/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift
+++ b/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift
@@ -23,8 +23,6 @@ final class CloudKitAccountZone: CloudKitZone {
var zoneID: CKRecordZone.ID
- var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit")
-
weak var container: CKContainer?
weak var database: CKDatabase?
var delegate: CloudKitZoneDelegate?
diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift
index bc1224836..91dbc5169 100644
--- a/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift
+++ b/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift
@@ -31,7 +31,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
self.articlesZone = articlesZone
}
- func cloudKitDidModify(changed: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result) -> Void) {
+ func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result) -> Void) {
for deletedRecordKey in deleted {
switch deletedRecordKey.recordType {
case CloudKitAccountZone.CloudKitWebFeed.recordType:
@@ -43,7 +43,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
}
}
- for changedRecord in changed {
+ for changedRecord in updated {
switch changedRecord.recordType {
case CloudKitAccountZone.CloudKitWebFeed.recordType:
addOrUpdateWebFeed(changedRecord)
diff --git a/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift b/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift
index 0553513e1..91cd788b9 100644
--- a/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift
+++ b/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift
@@ -19,8 +19,6 @@ final class CloudKitArticlesZone: CloudKitZone {
var zoneID: CKRecordZone.ID
- var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit")
-
weak var container: CKContainer?
weak var database: CKDatabase?
var delegate: CloudKitZoneDelegate? = nil
@@ -64,28 +62,6 @@ final class CloudKitArticlesZone: CloudKitZone {
migrateChangeToken()
}
- func refreshArticles(completion: @escaping ((Result) -> Void)) {
- fetchChangesInZone() { result in
- switch result {
- case .success:
- completion(.success(()))
- case .failure(let error):
- if case CloudKitZoneError.userDeletedZone = error {
- self.createZoneRecord() { result in
- switch result {
- case .success:
- self.refreshArticles(completion: completion)
- case .failure(let error):
- completion(.failure(error))
- }
- }
- } else {
- completion(.failure(error))
- }
- }
- }
- }
-
func saveNewArticles(_ articles: Set, completion: @escaping ((Result) -> Void)) {
guard !articles.isEmpty else {
completion(.success(()))
diff --git a/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift
index 95f607f07..613640046 100644
--- a/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift
+++ b/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift
@@ -28,7 +28,7 @@ class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate, Logging {
self.articlesZone = articlesZone
}
- func cloudKitDidModify(changed: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result) -> Void) {
+ func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result) -> Void) {
database.selectPendingReadStatusArticleIDs() { result in
switch result {
@@ -37,14 +37,16 @@ class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate, Logging {
self.database.selectPendingStarredStatusArticleIDs() { result in
switch result {
case .success(let pendingStarredStatusArticleIDs):
-
- self.delete(recordKeys: deleted, pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs) {
- self.update(records: changed,
- pendingReadStatusArticleIDs: pendingReadStatusArticleIDs,
- pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs,
- completion: completion)
+ self.delete(recordKeys: deleted, pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs) { error in
+ if let error = error {
+ completion(.failure(error))
+ } else {
+ self.update(records: updated,
+ pendingReadStatusArticleIDs: pendingReadStatusArticleIDs,
+ pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs,
+ completion: completion)
+ }
}
-
case .failure(let error):
self.logger.error("Error occurred getting pending starred records: \(error.localizedDescription, privacy: .public)")
completion(.failure(CloudKitZoneError.unknown))
@@ -63,19 +65,27 @@ class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate, Logging {
private extension CloudKitArticlesZoneDelegate {
- func delete(recordKeys: [CloudKitRecordKey], pendingStarredStatusArticleIDs: Set, completion: @escaping () -> Void) {
+ func delete(recordKeys: [CloudKitRecordKey], pendingStarredStatusArticleIDs: Set, completion: @escaping (Error?) -> Void) {
let receivedRecordIDs = recordKeys.filter({ $0.recordType == CloudKitArticlesZone.CloudKitArticleStatus.recordType }).map({ $0.recordID })
let receivedArticleIDs = Set(receivedRecordIDs.map({ stripPrefix($0.externalID) }))
let deletableArticleIDs = receivedArticleIDs.subtracting(pendingStarredStatusArticleIDs)
guard !deletableArticleIDs.isEmpty else {
- completion()
+ completion(nil)
return
}
- database.deleteSelectedForProcessing(Array(deletableArticleIDs)) { _ in
- self.account?.delete(articleIDs: deletableArticleIDs) { _ in
- completion()
+ database.deleteSelectedForProcessing(Array(deletableArticleIDs)) { databaseError in
+ if let databaseError = databaseError {
+ completion(databaseError)
+ } else {
+ self.account?.delete(articleIDs: deletableArticleIDs) { databaseError in
+ if let databaseError = databaseError {
+ completion(databaseError)
+ } else {
+ completion(nil)
+ }
+ }
}
}
}
@@ -96,8 +106,8 @@ private extension CloudKitArticlesZoneDelegate {
let group = DispatchGroup()
group.enter()
- account?.markAsUnread(updateableUnreadArticleIDs) { result in
- if case .failure(let databaseError) = result {
+ account?.markAsUnread(updateableUnreadArticleIDs) { databaseError in
+ if let databaseError = databaseError {
errorOccurred = true
self.logger.error("Error occurred while storing unread statuses: \(databaseError.localizedDescription, privacy: .public)")
}
@@ -105,8 +115,8 @@ private extension CloudKitArticlesZoneDelegate {
}
group.enter()
- account?.markAsRead(updateableReadArticleIDs) { result in
- if case .failure(let databaseError) = result {
+ account?.markAsRead(updateableReadArticleIDs) { databaseError in
+ if let databaseError = databaseError {
errorOccurred = true
self.logger.error("Error occurred while storing read statuses: \(databaseError.localizedDescription, privacy: .public)")
}
@@ -114,8 +124,8 @@ private extension CloudKitArticlesZoneDelegate {
}
group.enter()
- account?.markAsUnstarred(updateableUnstarredArticleIDs) { result in
- if case .failure(let databaseError) = result {
+ account?.markAsUnstarred(updateableUnstarredArticleIDs) { databaseError in
+ if let databaseError = databaseError {
errorOccurred = true
self.logger.error("Error occurred while storing unstarred statuses: \(databaseError.localizedDescription, privacy: .public)")
}
@@ -123,8 +133,8 @@ private extension CloudKitArticlesZoneDelegate {
}
group.enter()
- account?.markAsStarred(updateableStarredArticleIDs) { result in
- if case .failure(let databaseError) = result {
+ account?.markAsStarred(updateableStarredArticleIDs) { databaseError in
+ if let databaseError = databaseError {
errorOccurred = true
self.logger.error("Error occurred while stroing starred records: \(databaseError.localizedDescription, privacy: .public)")
}
diff --git a/Account/Sources/Account/CloudKit/CloudKitReceiveStatusOperation.swift b/Account/Sources/Account/CloudKit/CloudKitReceiveStatusOperation.swift
index 700bcef17..91d0f0789 100644
--- a/Account/Sources/Account/CloudKit/CloudKitReceiveStatusOperation.swift
+++ b/Account/Sources/Account/CloudKit/CloudKitReceiveStatusOperation.swift
@@ -32,7 +32,7 @@ class CloudKitReceiveStatusOperation: MainThreadOperation, Logging {
logger.debug("Refreshing article statuses...")
- articlesZone.refreshArticles() { result in
+ articlesZone.fetchChangesInZone() { result in
self.logger.debug("Done refreshing article statuses.")
switch result {
case .success:
diff --git a/Account/Sources/Account/CombinedRefreshProgress.swift b/Account/Sources/Account/CombinedRefreshProgress.swift
index 803fb9e1c..0020b94a6 100644
--- a/Account/Sources/Account/CombinedRefreshProgress.swift
+++ b/Account/Sources/Account/CombinedRefreshProgress.swift
@@ -18,25 +18,63 @@ public struct CombinedRefreshProgress {
public let numberRemaining: Int
public let numberCompleted: Int
public let isComplete: Bool
+ public let isIndeterminate: Bool
+ public let label: String
- init(numberOfTasks: Int, numberRemaining: Int, numberCompleted: Int) {
+ init(numberOfTasks: Int, numberRemaining: Int, numberCompleted: Int, isIndeterminate: Bool, label: String) {
self.numberOfTasks = max(numberOfTasks, 0)
self.numberRemaining = max(numberRemaining, 0)
self.numberCompleted = max(numberCompleted, 0)
self.isComplete = numberRemaining < 1
+ self.isIndeterminate = isIndeterminate
+ self.label = label
}
public init(downloadProgressArray: [DownloadProgress]) {
+ var numberOfDownloadsPossible = 0
+ var numberOfDownloadsActive = 0
var numberOfTasks = 0
var numberRemaining = 0
var numberCompleted = 0
-
+ var isIndeterminate = false
+ var isInprecise = false
+
for downloadProgress in downloadProgressArray {
+ numberOfDownloadsPossible += 1
+ numberOfDownloadsActive += downloadProgress.isComplete ? 0 : 1
numberOfTasks += downloadProgress.numberOfTasks
numberRemaining += downloadProgress.numberRemaining
numberCompleted += downloadProgress.numberCompleted
+
+ if downloadProgress.isIndeterminate {
+ isIndeterminate = true
+ }
+
+ if !downloadProgress.isPrecise {
+ isInprecise = true
+ }
}
- self.init(numberOfTasks: numberOfTasks, numberRemaining: numberRemaining, numberCompleted: numberCompleted)
+ var label = ""
+
+ if numberOfDownloadsActive > 0 {
+ if isInprecise {
+ if numberOfDownloadsActive == 1 {
+ if let activeName = downloadProgressArray.first(where: { $0.isComplete == false })?.name {
+ let formatString = NSLocalizedString("Syncing %@", comment: "Status bar progress")
+ label = NSString(format: formatString as NSString, activeName) as String
+ }
+ } else {
+ let formatString = NSLocalizedString("Syncing %@ accounts", comment: "Status bar progress")
+ label = NSString(format: formatString as NSString, NSNumber(value: numberOfDownloadsActive)) as String
+ }
+ } else {
+ let formatString = NSLocalizedString("%@ of %@", comment: "Status bar progress")
+ label = NSString(format: formatString as NSString, NSNumber(value: numberCompleted), NSNumber(value: numberOfTasks)) as String
+ }
+ }
+
+ self.init(numberOfTasks: numberOfTasks, numberRemaining: numberRemaining, numberCompleted: numberCompleted, isIndeterminate: isIndeterminate, label: label)
}
+
}
diff --git a/Account/Sources/Account/Feedbin/FeedbinAccountDelegate.swift b/Account/Sources/Account/Feedbin/FeedbinAccountDelegate.swift
index 334f02e4b..c73b5ef05 100644
--- a/Account/Sources/Account/Feedbin/FeedbinAccountDelegate.swift
+++ b/Account/Sources/Account/Feedbin/FeedbinAccountDelegate.swift
@@ -576,6 +576,7 @@ final class FeedbinAccountDelegate: AccountDelegate, Logging {
func accountDidInitialize(_ account: Account) {
credentials = try? account.retrieveCredentials(type: .basic)
+ refreshProgress.name = account.nameForDisplay
}
func accountWillBeDeleted(_ account: Account) {
@@ -1406,27 +1407,29 @@ private extension FeedbinAccountDelegate {
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
return
}
+
+ func complete() {
+ DispatchQueue.main.async {
+ account.clearWebFeedMetadata(feed)
+ account.removeWebFeed(feed)
+ if let folders = account.folders {
+ for folder in folders {
+ folder.removeWebFeed(feed)
+ }
+ }
+ completion(.success(()))
+ }
+ }
refreshProgress.addToNumberOfTasksAndRemaining(1)
caller.deleteSubscription(subscriptionID: subscriptionID) { result in
self.refreshProgress.completeTask()
switch result {
case .success:
- DispatchQueue.main.async {
- account.clearWebFeedMetadata(feed)
- account.removeWebFeed(feed)
- if let folders = account.folders {
- for folder in folders {
- folder.removeWebFeed(feed)
- }
- }
- completion(.success(()))
- }
+ complete()
case .failure(let error):
- DispatchQueue.main.async {
- let wrappedError = AccountError.wrappedError(error: error, account: account)
- completion(.failure(wrappedError))
- }
+ self.logger.error("Unable to remove feed from Feedbin. Removing locally and continue processing: \(error.localizedDescription, privacy: .public)")
+ complete()
}
}
diff --git a/Account/Sources/Account/Feedly/FeedlyAPICaller.swift b/Account/Sources/Account/Feedly/FeedlyAPICaller.swift
index 40942b2f8..e84b801bd 100644
--- a/Account/Sources/Account/Feedly/FeedlyAPICaller.swift
+++ b/Account/Sources/Account/Feedly/FeedlyAPICaller.swift
@@ -7,6 +7,7 @@
//
import Foundation
+import RSCore
import RSWeb
import Secrets
@@ -820,33 +821,48 @@ extension FeedlyAPICaller: FeedlyMarkArticlesService {
fatalError("\(components) does not produce a valid URL.")
}
- var request = URLRequest(url: url)
- request.httpMethod = "POST"
- request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType)
- request.addValue("application/json", forHTTPHeaderField: "Accept-Type")
- request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization)
-
- do {
- let body = MarkerEntriesBody(action: action.actionValue, entryIds: Array(articleIds))
- let encoder = JSONEncoder()
- let data = try encoder.encode(body)
- request.httpBody = data
- } catch {
- return DispatchQueue.main.async {
- completion(.failure(error))
+ let articleIdChunks = Array(articleIds).chunked(into: 300)
+ let dispatchGroup = DispatchGroup()
+ var groupError: Error? = nil
+
+ for articleIdChunk in articleIdChunks {
+
+ var request = URLRequest(url: url)
+ request.httpMethod = "POST"
+ request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType)
+ request.addValue("application/json", forHTTPHeaderField: "Accept-Type")
+ request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization)
+
+ do {
+ let body = MarkerEntriesBody(action: action.actionValue, entryIds: Array(articleIdChunk))
+ let encoder = JSONEncoder()
+ let data = try encoder.encode(body)
+ request.httpBody = data
+ } catch {
+ return DispatchQueue.main.async {
+ completion(.failure(error))
+ }
+ }
+
+ dispatchGroup.enter()
+ send(request: request, resultType: String.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in
+ switch result {
+ case .success(let (httpResponse, _)):
+ if httpResponse.statusCode != 200 {
+ groupError = URLError(.cannotDecodeContentData)
+ }
+ case .failure(let error):
+ groupError = error
+ }
+ dispatchGroup.leave()
}
}
- send(request: request, resultType: String.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in
- switch result {
- case .success(let (httpResponse, _)):
- if httpResponse.statusCode == 200 {
- completion(.success(()))
- } else {
- completion(.failure(URLError(.cannotDecodeContentData)))
- }
- case .failure(let error):
- completion(.failure(error))
+ dispatchGroup.notify(queue: .main) {
+ if let groupError = groupError {
+ completion(.failure(groupError))
+ } else {
+ completion(.success(()))
}
}
}
diff --git a/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift b/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift
index ba4f133d3..4294c4726 100644
--- a/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift
+++ b/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift
@@ -527,6 +527,7 @@ final class FeedlyAccountDelegate: AccountDelegate, Logging {
func accountDidInitialize(_ account: Account) {
initializedAccount = account
credentials = try? account.retrieveCredentials(type: .oauthAccessToken)
+ refreshProgress.name = account.nameForDisplay
}
func accountWillBeDeleted(_ account: Account) {
diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyIngestStarredArticleIdsOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyIngestStarredArticleIdsOperation.swift
index 29022c3fa..a38ee238f 100644
--- a/Account/Sources/Account/Feedly/Operations/FeedlyIngestStarredArticleIdsOperation.swift
+++ b/Account/Sources/Account/Feedly/Operations/FeedlyIngestStarredArticleIdsOperation.swift
@@ -123,18 +123,18 @@ final class FeedlyIngestStarredArticleIdsOperation: FeedlyOperation, Logging {
let results = StarredStatusResults()
group.enter()
- account.markAsStarred(remoteStarredArticleIDs) { result in
- if case .failure(let error) = result {
- results.markAsStarredError = error
+ account.markAsStarred(remoteStarredArticleIDs) { databaseError in
+ if let databaseError = databaseError {
+ results.markAsStarredError = databaseError
}
group.leave()
}
let deltaUnstarredArticleIDs = localStarredArticleIDs.subtracting(remoteStarredArticleIDs)
group.enter()
- account.markAsUnstarred(deltaUnstarredArticleIDs) { result in
- if case .failure(let error) = result {
- results.markAsUnstarredError = error
+ account.markAsUnstarred(deltaUnstarredArticleIDs) { databaseError in
+ if let databaseError = databaseError {
+ results.markAsUnstarredError = databaseError
}
group.leave()
}
diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyIngestUnreadArticleIdsOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyIngestUnreadArticleIdsOperation.swift
index 72ec9f075..fa618fe70 100644
--- a/Account/Sources/Account/Feedly/Operations/FeedlyIngestUnreadArticleIdsOperation.swift
+++ b/Account/Sources/Account/Feedly/Operations/FeedlyIngestUnreadArticleIdsOperation.swift
@@ -123,18 +123,18 @@ final class FeedlyIngestUnreadArticleIdsOperation: FeedlyOperation, Logging {
let results = ReadStatusResults()
group.enter()
- account.markAsUnread(remoteUnreadArticleIDs) { result in
- if case .failure(let error) = result {
- results.markAsUnreadError = error
+ account.markAsUnread(remoteUnreadArticleIDs) { databaseError in
+ if let databaseError = databaseError {
+ results.markAsUnreadError = databaseError
}
group.leave()
}
let articleIDsToMarkRead = localUnreadArticleIDs.subtracting(remoteUnreadArticleIDs)
group.enter()
- account.markAsRead(articleIDsToMarkRead) { result in
- if case .failure(let error) = result {
- results.markAsReadError = error
+ account.markAsRead(articleIDsToMarkRead) { databaseError in
+ if let databaseError = databaseError {
+ results.markAsReadError = databaseError
}
group.leave()
}
diff --git a/Account/Sources/Account/LocalAccount/LocalAccountDelegate.swift b/Account/Sources/Account/LocalAccount/LocalAccountDelegate.swift
index d19608c36..cfe18af77 100644
--- a/Account/Sources/Account/LocalAccount/LocalAccountDelegate.swift
+++ b/Account/Sources/Account/LocalAccount/LocalAccountDelegate.swift
@@ -222,6 +222,8 @@ final class LocalAccountDelegate: AccountDelegate, Logging {
func accountDidInitialize(_ account: Account) {
self.account = account
+ refreshProgress.name = account.nameForDisplay
+ refreshProgress.isPrecise = true
}
func accountWillBeDeleted(_ account: Account) {
diff --git a/Account/Sources/Account/NewsBlur/NewsBlurAccountDelegate.swift b/Account/Sources/Account/NewsBlur/NewsBlurAccountDelegate.swift
index 37966a2af..443ffb535 100644
--- a/Account/Sources/Account/NewsBlur/NewsBlurAccountDelegate.swift
+++ b/Account/Sources/Account/NewsBlur/NewsBlurAccountDelegate.swift
@@ -603,6 +603,7 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging {
func accountDidInitialize(_ account: Account) {
credentials = try? account.retrieveCredentials(type: .newsBlurSessionId)
+ refreshProgress.name = account.nameForDisplay
}
func accountWillBeDeleted(_ account: Account) {
diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift b/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift
index ae2c4ae49..813317083 100644
--- a/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift
+++ b/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift
@@ -628,6 +628,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate, Logging {
func accountDidInitialize(_ account: Account) {
credentials = try? account.retrieveCredentials(type: .readerAPIKey)
+ refreshProgress.name = account.nameForDisplay
}
func accountWillBeDeleted(_ account: Account) {
@@ -1058,7 +1059,7 @@ private extension ReaderAPIAccountDelegate {
uniqueID: entry.uniqueID(variant: variant),
feedURL: streamID,
url: nil,
- externalURL: entry.alternates.first?.url,
+ externalURL: entry.alternates?.first?.url,
title: entry.title,
language: nil,
contentHTML: entry.summary.content,
diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPIEntry.swift b/Account/Sources/Account/ReaderAPI/ReaderAPIEntry.swift
index 5d8575945..93de146f8 100644
--- a/Account/Sources/Account/ReaderAPI/ReaderAPIEntry.swift
+++ b/Account/Sources/Account/ReaderAPI/ReaderAPIEntry.swift
@@ -58,7 +58,7 @@ struct ReaderAPIEntry: Codable {
let timestampUsec: String?
let summary: ReaderAPIArticleSummary
- let alternates: [ReaderAPIAlternateLocation]
+ let alternates: [ReaderAPIAlternateLocation]?
let categories: [String]
let origin: ReaderAPIEntryOrigin
diff --git a/Appcasts/netnewswire-beta.xml b/Appcasts/netnewswire-beta.xml
index 0fcfa3416..c4699ebef 100755
--- a/Appcasts/netnewswire-beta.xml
+++ b/Appcasts/netnewswire-beta.xml
@@ -5,6 +5,39 @@
https://ranchero.com/downloads/netnewswire-beta.xml
Most recent NetNewsWire changes with links to updates.
en
+
+ -
+
NetNewsWire 6.1.1b1
+ Fixed a bug that could prevent users from accessing BazQux if an article was missing a field
+ Fixed an issue that could prevent Feedly users from syncing if they tried to mark too many articles as read at the same time
+ ]]>
+ Wed, 02 Nov 2022 21:20:00 -0700
+
+ 10.15.0
+
+
+ -
+
NetNewsWire 6.1
+ Article themes. Several themes ship with the app, and you can create your own. You can change the theme in Preferences or by adding the theme switcher to the toolbar
+ Copy URLs using repaired, rather than raw, feed links
+ Restore article scroll position on relaunching app
+ Added Copy Article URL and Copy External URL commands to the Edit menu
+ Fixed a bug where using cmd-Q wouldn’t always quit the app as quickly as one might prefer
+ Disallow creation of iCloud account in the app if iCloud and iCloud Drive aren’t both enabled
+ Fixed bug showing quote tweets that only included an image
+ Added a hidden pref to suppress downloading/syncing on start: `defaults write com.ranchero.NetNewsWire-Evergreen DevroeSuppressSyncOnLaunch -bool true`
+ Video autoplay is now disallowed
+ Article view now supports RTL layout
+ Fixed a few font and sizing issues
+ Updated built-in feeds
+ Better alignment for items in General Preferences pane
+ ]]>
+ Thu, 07 Apr 2022 10:05:00 -0700
+
+ 10.15.0
+
-
NetNewsWire 6.1b5
diff --git a/Appcasts/netnewswire-release.xml b/Appcasts/netnewswire-release.xml
index 1781af04d..7c9cc240e 100755
--- a/Appcasts/netnewswire-release.xml
+++ b/Appcasts/netnewswire-release.xml
@@ -6,6 +6,28 @@
Most recent NetNewsWire releases (not test builds).
en
+ -
+
NetNewsWire 6.1
+ Article themes. Several themes ship with the app, and you can create your own. You can change the theme in Preferences or by adding the theme switcher to the toolbar
+ Copy URLs using repaired, rather than raw, feed links
+ Restore article scroll position on relaunching app
+ Added Copy Article URL and Copy External URL commands to the Edit menu
+ Fixed a bug where using cmd-Q wouldn’t always quit the app as quickly as one might prefer
+ Disallow creation of iCloud account in the app if iCloud and iCloud Drive aren’t both enabled
+ Fixed bug showing quote tweets that only included an image
+ Added a hidden pref to suppress downloading/syncing on start: `defaults write com.ranchero.NetNewsWire-Evergreen DevroeSuppressSyncOnLaunch -bool true`
+ Video autoplay is now disallowed
+ Article view now supports RTL layout
+ Fixed a few font and sizing issues
+ Updated built-in feeds
+ Better alignment for items in General Preferences pane
+ ]]>
+ Thu, 07 Apr 2022 10:05:00 -0700
+
+ 10.15.0
+
+
-
NetNewsWire 6.0.3
, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping ArticleIDsCompletionBlock) {
- articlesTable.markAndFetchNew(articleIDs, statusKey, flag, completion)
+ public func mark(articleIDs: Set, statusKey: ArticleStatus.Key, flag: Bool, completion: DatabaseCompletionBlock?) {
+ articlesTable.mark(articleIDs, statusKey, flag, completion)
}
/// Create statuses for specified articleIDs. For existing statuses, don’t do anything.
diff --git a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift
index e7768af8d..e41a57a25 100644
--- a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift
+++ b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift
@@ -221,7 +221,7 @@ final class ArticlesTable: DatabaseTable {
func makeDatabaseCalls(_ database: FMDatabase) {
let articleIDs = parsedItems.articleIDs()
- let (statusesDictionary, _) = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, false, database) //1
+ let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, false, database) //1
assert(statusesDictionary.count == articleIDs.count)
let incomingArticles = Article.articlesWithParsedItems(parsedItems, webFeedID, self.accountID, statusesDictionary) //2
@@ -303,7 +303,7 @@ final class ArticlesTable: DatabaseTable {
articleIDs.formUnion(parsedItems.articleIDs())
}
- let (statusesDictionary, _) = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, read, database) //1
+ let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, read, database) //1
assert(statusesDictionary.count == articleIDs.count)
let allIncomingArticles = Article.articlesWithWebFeedIDsAndItems(webFeedIDsAndItems, self.accountID, statusesDictionary) //2
@@ -476,17 +476,17 @@ final class ArticlesTable: DatabaseTable {
}
}
- func markAndFetchNew(_ articleIDs: Set, _ statusKey: ArticleStatus.Key, _ flag: Bool, _ completion: @escaping ArticleIDsCompletionBlock) {
+ func mark(_ articleIDs: Set, _ statusKey: ArticleStatus.Key, _ flag: Bool, _ completion: DatabaseCompletionBlock?) {
queue.runInTransaction { databaseResult in
switch databaseResult {
case .success(let database):
- let newStatusIDs = self.statusesTable.markAndFetchNew(articleIDs, statusKey, flag, database)
+ self.statusesTable.mark(articleIDs, statusKey, flag, database)
DispatchQueue.main.async {
- completion(.success(newStatusIDs))
+ completion?(nil)
}
case .failure(let databaseError):
DispatchQueue.main.async {
- completion(.failure(databaseError))
+ completion?(databaseError)
}
}
}
diff --git a/ArticlesDatabase/Sources/ArticlesDatabase/StatusesTable.swift b/ArticlesDatabase/Sources/ArticlesDatabase/StatusesTable.swift
index 95d06d788..d170718e4 100644
--- a/ArticlesDatabase/Sources/ArticlesDatabase/StatusesTable.swift
+++ b/ArticlesDatabase/Sources/ArticlesDatabase/StatusesTable.swift
@@ -28,7 +28,7 @@ final class StatusesTable: DatabaseTable {
// MARK: - Creating/Updating
- func ensureStatusesForArticleIDs(_ articleIDs: Set, _ read: Bool, _ database: FMDatabase) -> ([String: ArticleStatus], Set) {
+ func ensureStatusesForArticleIDs(_ articleIDs: Set, _ read: Bool, _ database: FMDatabase) -> [String: ArticleStatus] {
#if DEBUG
// Check for missing statuses — this asserts that all the passed-in articleIDs exist in the statuses table.
@@ -44,7 +44,7 @@ final class StatusesTable: DatabaseTable {
// Check cache.
let articleIDsMissingCachedStatus = articleIDsWithNoCachedStatus(articleIDs)
if articleIDsMissingCachedStatus.isEmpty {
- return (statusesDictionary(articleIDs), Set())
+ return statusesDictionary(articleIDs)
}
// Check database.
@@ -56,7 +56,7 @@ final class StatusesTable: DatabaseTable {
self.createAndSaveStatusesForArticleIDs(articleIDsNeedingStatus, read, database)
}
- return (statusesDictionary(articleIDs), articleIDsNeedingStatus)
+ return statusesDictionary(articleIDs)
}
// MARK: - Marking
@@ -85,11 +85,10 @@ final class StatusesTable: DatabaseTable {
return updatedStatuses
}
- func markAndFetchNew(_ articleIDs: Set, _ statusKey: ArticleStatus.Key, _ flag: Bool, _ database: FMDatabase) -> Set {
- let (statusesDictionary, newStatusIDs) = ensureStatusesForArticleIDs(articleIDs, flag, database)
+ func mark(_ articleIDs: Set, _ statusKey: ArticleStatus.Key, _ flag: Bool, _ database: FMDatabase) {
+ let statusesDictionary = ensureStatusesForArticleIDs(articleIDs, flag, database)
let statuses = Set(statusesDictionary.values)
mark(statuses, statusKey, flag, database)
- return newStatusIDs
}
// MARK: - Fetching
diff --git a/Mac/AppAssets.swift b/Mac/AppAssets.swift
index 2e6103762..21c22c35a 100644
--- a/Mac/AppAssets.swift
+++ b/Mac/AppAssets.swift
@@ -295,4 +295,10 @@ struct AppAssets {
}
}
+ static var notificationSoundBlipFileName: String = {
+ // https://freesound.org/people/cabled_mess/sounds/350862/
+ return "notificationSoundBlip.mp3"
+ }()
+
+
}
diff --git a/Mac/AppDefaults.swift b/Mac/AppDefaults.swift
index df9b7c618..685d6fd44 100644
--- a/Mac/AppDefaults.swift
+++ b/Mac/AppDefaults.swift
@@ -42,6 +42,7 @@ final class AppDefaults {
static let exportOPMLAccountID = "exportOPMLAccountID"
static let defaultBrowserID = "defaultBrowserID"
static let currentThemeName = "currentThemeName"
+ static let hasSeenNotAllArticlesHaveURLsAlert = "hasSeenNotAllArticlesHaveURLsAlert"
// Hidden prefs
static let showDebugMenu = "ShowDebugMenu"
@@ -220,6 +221,15 @@ final class AppDefaults {
AppDefaults.setString(for: Key.currentThemeName, newValue)
}
}
+
+ var hasSeenNotAllArticlesHaveURLsAlert: Bool {
+ get {
+ return UserDefaults.standard.bool(forKey: Key.hasSeenNotAllArticlesHaveURLsAlert)
+ }
+ set {
+ UserDefaults.standard.set(newValue, forKey: Key.hasSeenNotAllArticlesHaveURLsAlert)
+ }
+ }
var showTitleOnMainWindow: Bool {
return AppDefaults.bool(for: Key.showTitleOnMainWindow)
diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift
index 959c2c213..d155b34a4 100644
--- a/Mac/AppDelegate.swift
+++ b/Mac/AppDelegate.swift
@@ -16,6 +16,7 @@ import RSCore
import RSCoreResources
import Secrets
import CrashReporter
+import SwiftUI
// If we're not going to import Sparkle, provide dummy protocols to make it easy
// for AppDelegate to comply
@@ -31,7 +32,7 @@ var appDelegate: AppDelegate!
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, UNUserNotificationCenterDelegate, UnreadCountProvider, SPUStandardUserDriverDelegate, SPUUpdaterDelegate, Logging
{
-
+
private struct WindowRestorationIdentifiers {
static let mainWindow = "mainWindow"
}
@@ -43,7 +44,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
var webFeedIconDownloader: WebFeedIconDownloader!
var extensionContainersFile: ExtensionContainersFile!
var extensionFeedAddRequestFile: ExtensionFeedAddRequestFile!
-
+
var appName: String!
var refreshTimer: AccountRefreshTimer?
@@ -60,7 +61,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
}
}
}
-
+
var isShutDownSyncDone = false
@IBOutlet var shareMenuItem: NSMenuItem!
@@ -70,7 +71,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
@IBOutlet var sortByNewestArticleOnTopMenuItem: NSMenuItem!
@IBOutlet var groupArticlesByFeedMenuItem: NSMenuItem!
@IBOutlet var checkForUpdatesMenuItem: NSMenuItem!
-
+
var unreadCount = 0 {
didSet {
if unreadCount != oldValue {
@@ -79,7 +80,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
}
}
}
-
+
private var mainWindowController: MainWindowController? {
var bestController: MainWindowController?
for candidateController in mainWindowControllers {
@@ -104,43 +105,40 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
private var inspectorWindowController: InspectorWindowController?
private var crashReportWindowController: CrashReportWindowController? // For testing only
private let appMovementMonitor = RSAppMovementMonitor()
- #if !MAC_APP_STORE && !TEST
+#if !MAC_APP_STORE && !TEST
private var softwareUpdater: SPUUpdater!
private var crashReporter: PLCrashReporter!
- #endif
+#endif
- private var themeImportPath: String?
-
override init() {
NSWindow.allowsAutomaticWindowTabbing = false
super.init()
-
- #if !MAC_APP_STORE
+
+#if !MAC_APP_STORE
let crashReporterConfig = PLCrashReporterConfig.defaultConfiguration()
crashReporter = PLCrashReporter(configuration: crashReporterConfig)
crashReporter.enable()
- #endif
-
+#endif
+
SecretsManager.provider = Secrets()
AccountManager.shared = AccountManager(accountsFolder: Platform.dataSubfolder(forApplication: nil, folderName: "Accounts")!)
ArticleThemesManager.shared = ArticleThemesManager(folderPath: Platform.dataSubfolder(forApplication: nil, folderName: "Themes")!)
FeedProviderManager.shared.delegate = ExtensionPointManager.shared
-
+
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(inspectableObjectsDidChange(_:)), name: .InspectableObjectsDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(importDownloadedTheme(_:)), name: .didEndDownloadingTheme, object: nil)
- NotificationCenter.default.addObserver(self, selector: #selector(themeImportError(_:)), name: .didFailToImportThemeWithError, object: nil)
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(didWakeNotification(_:)), name: NSWorkspace.didWakeNotification, object: nil)
-
+
appDelegate = self
}
-
+
// MARK: - API
func showAddFolderSheetOnWindow(_ window: NSWindow) {
addFolderWindowController = AddFolderWindowController()
addFolderWindowController!.runSheetOnWindow(window)
}
-
+
func showAddWebFeedSheetOnWindow(_ window: NSWindow, urlString: String?, name: String?, account: Account?, folder: Folder?) {
addFeedController = AddFeedController(hostWindow: window)
addFeedController?.showAddFeedSheet(.webFeed, urlString, name, account, folder)
@@ -152,7 +150,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
installAppleEventHandlers()
CacheCleaner.purgeIfNecessary()
-
+
// Try to establish a cache in the Caches folder, but if it fails for some reason fall back to a temporary dir
let cacheFolder: String
if let userCacheFolder = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false).path {
@@ -162,40 +160,40 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
let bundleIdentifier = (Bundle.main.infoDictionary!["CFBundleIdentifier"]! as! String)
cacheFolder = (NSTemporaryDirectory() as NSString).appendingPathComponent(bundleIdentifier)
}
-
+
let faviconsFolder = (cacheFolder as NSString).appendingPathComponent("Favicons")
let faviconsFolderURL = URL(fileURLWithPath: faviconsFolder)
try! FileManager.default.createDirectory(at: faviconsFolderURL, withIntermediateDirectories: true, attributes: nil)
faviconDownloader = FaviconDownloader(folder: faviconsFolder)
-
+
let imagesFolder = (cacheFolder as NSString).appendingPathComponent("Images")
let imagesFolderURL = URL(fileURLWithPath: imagesFolder)
try! FileManager.default.createDirectory(at: imagesFolderURL, withIntermediateDirectories: true, attributes: nil)
imageDownloader = ImageDownloader(folder: imagesFolder)
-
+
authorAvatarDownloader = AuthorAvatarDownloader(imageDownloader: imageDownloader)
webFeedIconDownloader = WebFeedIconDownloader(imageDownloader: imageDownloader, folder: cacheFolder)
-
+
appName = (Bundle.main.infoDictionary!["CFBundleExecutable"]! as! String)
}
func applicationDidFinishLaunching(_ note: Notification) {
-
- #if MAC_APP_STORE || TEST
- checkForUpdatesMenuItem.isHidden = true
- #else
- // Initialize Sparkle...
- let hostBundle = Bundle.main
- let updateDriver = SPUStandardUserDriver(hostBundle: hostBundle, delegate: self)
- self.softwareUpdater = SPUUpdater(hostBundle: hostBundle, applicationBundle: hostBundle, userDriver: updateDriver, delegate: self)
-
- do {
- try self.softwareUpdater.start()
- }
- catch {
- logger.error("Failed to start software updater with error: \(error.localizedDescription, privacy: .public)")
- }
- #endif
+
+#if MAC_APP_STORE || TEST
+ checkForUpdatesMenuItem.isHidden = true
+#else
+ // Initialize Sparkle...
+ let hostBundle = Bundle.main
+ let updateDriver = SPUStandardUserDriver(hostBundle: hostBundle, delegate: self)
+ self.softwareUpdater = SPUUpdater(hostBundle: hostBundle, applicationBundle: hostBundle, userDriver: updateDriver, delegate: self)
+
+ do {
+ try self.softwareUpdater.start()
+ }
+ catch {
+ logger.error("Failed to start software updater with error: \(error.localizedDescription, privacy: .public)")
+ }
+#endif
AppDefaults.shared.registerDefaults()
let isFirstRun = AppDefaults.shared.isFirstRun
@@ -203,14 +201,14 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
logger.debug("Is first run")
}
let localAccount = AccountManager.shared.defaultAccount
-
+
if isFirstRun && !AccountManager.shared.anyAccountHasAtLeastOneFeed() {
// Import feeds. Either old NNW 3 feeds or the default feeds.
if !NNW3ImportController.importSubscriptionsIfFileExists(account: localAccount) {
DefaultFeedsImporter.importDefaultFeeds(account: localAccount)
}
}
-
+
updateSortMenuItems()
updateGroupByFeedMenuItem()
@@ -225,26 +223,26 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
if isFirstRun {
mainWindowController?.window?.center()
}
-
+
NotificationCenter.default.addObserver(self, selector: #selector(webFeedSettingDidChange(_:)), name: .WebFeedSettingDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
-
+
DispatchQueue.main.async {
self.unreadCount = AccountManager.shared.unreadCount
}
-
+
if InspectorWindowController.shouldOpenAtStartup {
self.toggleInspectorWindow(self)
}
-
+
extensionContainersFile = ExtensionContainersFile()
extensionFeedAddRequestFile = ExtensionFeedAddRequestFile()
-
+
refreshTimer = AccountRefreshTimer()
syncTimer = ArticleStatusSyncTimer()
- UNUserNotificationCenter.current().requestAuthorization(options:[.badge]) { (granted, error) in }
-
+ UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .alert, .sound]) { (granted, error) in }
+
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
if settings.authorizationStatus == .authorized {
DispatchQueue.main.async {
@@ -252,14 +250,14 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
}
}
}
-
+
UNUserNotificationCenter.current().delegate = self
userNotificationManager = UserNotificationManager()
-
- #if DEBUG
+
+#if DEBUG
refreshTimer!.update()
syncTimer!.update()
- #else
+#else
if AppDefaults.shared.suppressSyncOnLaunch {
refreshTimer!.update()
syncTimer!.update()
@@ -269,26 +267,26 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
self.syncTimer!.timedRefresh(nil)
}
}
- #endif
+#endif
if AppDefaults.shared.showDebugMenu {
- // The Web Inspector uses SPI and can never appear in a MAC_APP_STORE build.
- #if MAC_APP_STORE
- let debugMenu = debugMenuItem.submenu!
- let toggleWebInspectorItemIndex = debugMenu.indexOfItem(withTarget: self, andAction: #selector(toggleWebInspectorEnabled(_:)))
- if toggleWebInspectorItemIndex != -1 {
- debugMenu.removeItem(at: toggleWebInspectorItemIndex)
- }
- #endif
- } else {
+ // The Web Inspector uses SPI and can never appear in a MAC_APP_STORE build.
+#if MAC_APP_STORE
+ let debugMenu = debugMenuItem.submenu!
+ let toggleWebInspectorItemIndex = debugMenu.indexOfItem(withTarget: self, andAction: #selector(toggleWebInspectorEnabled(_:)))
+ if toggleWebInspectorItemIndex != -1 {
+ debugMenu.removeItem(at: toggleWebInspectorItemIndex)
+ }
+#endif
+ } else {
debugMenuItem.menu?.removeItem(debugMenuItem)
}
-
- #if !MAC_APP_STORE
+
+#if !MAC_APP_STORE
DispatchQueue.main.async {
CrashReporter.check(crashReporter: self.crashReporter)
}
- #endif
+#endif
}
@@ -299,7 +297,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
mainWindowController.handle(userActivity)
return true
}
-
+
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
// https://github.com/brentsimmons/NetNewsWire/issues/522
// I couldn’t reproduce the crashing bug, but it appears to happen on creating a main window
@@ -313,7 +311,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
mainWindowController.showWindow(self)
return false
}
-
+
func applicationDidBecomeActive(_ notification: Notification) {
fireOldTimers()
}
@@ -322,7 +320,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
ArticleStringFormatter.emptyCaches()
saveState()
}
-
+
func application(_ application: NSApplication, didReceiveRemoteNotification userInfo: [String : Any]) {
AccountManager.shared.receiveRemoteNotification(userInfo: userInfo)
}
@@ -346,14 +344,58 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
let timeout = Date().addingTimeInterval(2)
while !isShutDownSyncDone && RunLoop.current.run(mode: .default, before: timeout) && timeout > Date() { }
}
-
+
+ func presentThemeImportError(_ error: Error) {
+ var informativeText: String = ""
+
+ if let decodingError = error as? DecodingError {
+ switch decodingError {
+ case .typeMismatch(let type, _):
+ let localizedError = NSLocalizedString("This theme cannot be used because the the type—“%@”—is mismatched in the Info.plist", comment: "Type mismatch")
+ informativeText = NSString.localizedStringWithFormat(localizedError as NSString, type as! CVarArg) as String
+ case .valueNotFound(let value, _):
+ let localizedError = NSLocalizedString("This theme cannot be used because the the value—“%@”—is not found in the Info.plist.", comment: "Decoding value missing")
+ informativeText = NSString.localizedStringWithFormat(localizedError as NSString, value as! CVarArg) as String
+ case .keyNotFound(let codingKey, _):
+ let localizedError = NSLocalizedString("This theme cannot be used because the the key—“%@”—is not found in the Info.plist.", comment: "Decoding key missing")
+ informativeText = NSString.localizedStringWithFormat(localizedError as NSString, codingKey.stringValue) as String
+ case .dataCorrupted(let context):
+ guard let underlyingError = context.underlyingError as NSError?,
+ let debugDescription = underlyingError.userInfo["NSDebugDescription"] as? String else {
+ informativeText = error.localizedDescription
+ break
+ }
+ let localizedError = NSLocalizedString("This theme cannot be used because of data corruption in the Info.plist: %@.", comment: "Decoding key missing")
+ informativeText = NSString.localizedStringWithFormat(localizedError as NSString, debugDescription) as String
+
+ default:
+ informativeText = error.localizedDescription
+ }
+ } else {
+ informativeText = error.localizedDescription
+ }
+
+ DispatchQueue.main.async {
+ let alert = NSAlert()
+ alert.alertStyle = .warning
+ alert.messageText = NSLocalizedString("Theme Error", comment: "Theme error")
+ alert.informativeText = informativeText
+ alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK"))
+
+ alert.buttons[0].keyEquivalent = "\r"
+
+ let response = alert.runModal()
+ }
+ }
+
// MARK: Notifications
+
@objc func unreadCountDidChange(_ note: Notification) {
if note.object is AccountManager {
unreadCount = AccountManager.shared.unreadCount
}
}
-
+
@objc func webFeedSettingDidChange(_ note: Notification) {
guard let feed = note.object as? WebFeed, let key = note.userInfo?[WebFeed.WebFeedSettingUserInfoKey] as? String else {
return
@@ -362,14 +404,14 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
let _ = faviconDownloader.favicon(for: feed)
}
}
-
+
@objc func inspectableObjectsDidChange(_ note: Notification) {
guard let inspectorWindowController = inspectorWindowController, inspectorWindowController.isOpen else {
return
}
inspectorWindowController.objects = objectsForInspector()
}
-
+
@objc func userDefaultsDidChange(_ note: Notification) {
updateSortMenuItems()
updateGroupByFeedMenuItem()
@@ -388,14 +430,14 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
@objc func importDownloadedTheme(_ note: Notification) {
guard let userInfo = note.userInfo,
- let url = userInfo["url"] as? URL else {
+ let url = userInfo["url"] as? URL else {
return
}
DispatchQueue.main.async {
self.importTheme(filename: url.path)
}
}
-
+
// MARK: Main Window
func createMainWindowController() -> MainWindowController {
@@ -408,12 +450,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
mainWindowControllers.append(controller)
return controller
}
-
+
func windowControllerWithName(_ storyboardName: String) -> NSWindowController {
let storyboard = NSStoryboard(name: NSStoryboard.Name(storyboardName), bundle: nil)
return storyboard.instantiateInitialController()! as! NSWindowController
}
-
+
@discardableResult
func createAndShowMainWindow() -> MainWindowController {
let controller = createMainWindowController()
@@ -426,7 +468,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
return controller
}
-
+
func createAndShowMainWindowIfNecessary() {
if mainWindowController == nil {
createAndShowMainWindow()
@@ -434,7 +476,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
mainWindowController?.showWindow(self)
}
}
-
+
func removeMainWindow(_ windowController: MainWindowController) {
guard mainWindowControllers.count > 1 else { return }
if let index = mainWindowControllers.firstIndex(of: windowController) {
@@ -447,10 +489,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
if shuttingDown {
return false
}
-
+
let isDisplayingSheet = mainWindowController?.isDisplayingSheet ?? false
let isSpecialAccountAvailable = AccountManager.shared.activeAccounts.contains(where: { $0.type == .onMyMac || $0.type == .cloudKit })
-
+
if item.action == #selector(refreshAll(_:)) {
return !AccountManager.shared.refreshInProgress && !AccountManager.shared.activeAccounts.isEmpty
}
@@ -484,7 +526,20 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
}
return ExtensionPointManager.shared.isTwitterEnabled
}
+
+ #if !DEBUG
+ if item.action == #selector(debugDropConditionalGetInfo(_:)) {
+ return false
+ }
+ #endif
+ if item.action == #selector(debugTestCrashReporterWindow(_:)) ||
+ item.action == #selector(debugTestCrashReportSending(_:)) ||
+ item.action == #selector(forceCrash(_:)) {
+ let appIDPrefix = Bundle.main.infoDictionary?["AppIdentifierPrefix"] as! String
+ return appIDPrefix == "M8L2WTLA8W."
+ }
+
#if !MAC_APP_STORE
if item.action == #selector(toggleWebInspectorEnabled(_:)) {
(item as! NSMenuItem).state = AppDefaults.shared.webInspectorEnabled ? .on : .off
@@ -719,6 +774,21 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
self.softwareUpdater.checkForUpdates()
#endif
}
+
+ @IBAction func showAbout(_ sender: Any?) {
+ if #available(macOS 12, *) {
+ for window in NSApplication.shared.windows {
+ if window.identifier == .aboutNetNewsWire {
+ window.makeKeyAndOrderFront(nil)
+ return
+ }
+ }
+ let controller = AboutWindowController()
+ controller.window?.makeKeyAndOrderFront(nil)
+ } else {
+ NSApplication.shared.orderFrontStandardAboutPanel(self)
+ }
+ }
}
@@ -751,31 +821,53 @@ extension AppDelegate {
@IBAction func debugDropConditionalGetInfo(_ sender: Any?) {
#if DEBUG
- AccountManager.shared.activeAccounts.forEach{ $0.debugDropConditionalGetInfo() }
+ AccountManager.shared.activeAccounts.forEach{ $0.debugDropConditionalGetInfo() }
#endif
}
+ @IBAction func debugClearImageCaches(_ sender: Any?) {
+ let alert = NSAlert()
+ alert.alertStyle = .warning
+ alert.messageText = NSLocalizedString("Are you sure you want to clear the image caches? This will restart NetNewsWire to begin reloading the remote images.",
+ comment: "Clear and restart confirmation message.")
+ alert.addButton(withTitle: NSLocalizedString("Clear & Restart", comment: "Clear & Restart"))
+ alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel"))
+
+ let userChoice = alert.runModal()
+ if userChoice == .alertFirstButtonReturn {
+ CacheCleaner.purge()
+
+ let configuration = NSWorkspace.OpenConfiguration()
+ configuration.createsNewApplicationInstance = true
+ NSWorkspace.shared.openApplication(at: Bundle.main.bundleURL, configuration: configuration)
+
+ NSApp.terminate(self)
+ }
+ }
+
@IBAction func debugTestCrashReporterWindow(_ sender: Any?) {
#if DEBUG
- crashReportWindowController = CrashReportWindowController(crashLogText: "This is a test crash log.")
- crashReportWindowController!.testing = true
- crashReportWindowController!.showWindow(self)
+ crashReportWindowController = CrashReportWindowController(crashLogText: "This is a test crash log.")
+ crashReportWindowController!.testing = true
+ crashReportWindowController!.showWindow(self)
#endif
}
@IBAction func debugTestCrashReportSending(_ sender: Any?) {
+ #if DEBUG
CrashReporter.sendCrashLogText("This is a test. Hi, Brent.")
+ #endif
}
@IBAction func forceCrash(_ sender: Any?) {
+ #if DEBUG
fatalError("This is a deliberate crash.")
+ #endif
}
@IBAction func openApplicationSupportFolder(_ sender: Any?) {
- #if DEBUG
- guard let appSupport = Platform.dataSubfolder(forApplication: nil, folderName: "") else { return }
- NSWorkspace.shared.open(URL(fileURLWithPath: appSupport))
- #endif
+ guard let appSupport = Platform.dataSubfolder(forApplication: nil, folderName: "") else { return }
+ NSWorkspace.shared.open(URL(fileURLWithPath: appSupport))
}
@IBAction func toggleWebInspectorEnabled(_ sender: Any?) {
@@ -898,8 +990,7 @@ internal extension AppDelegate {
}
}
} catch {
- NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error" : error, "path": filename])
- logger.error("Error importing theme: \(error.localizedDescription, privacy: .public)")
+ presentThemeImportError(error)
}
}
@@ -918,67 +1009,6 @@ internal extension AppDelegate {
alert.beginSheetModal(for: window)
}
- @objc func themeImportError(_ note: Notification) {
- guard let userInfo = note.userInfo,
- let error = userInfo["error"] as? Error else {
- return
- }
- themeImportPath = userInfo["path"] as? String
- var informativeText: String = ""
- if let decodingError = error as? DecodingError {
- switch decodingError {
- case .typeMismatch(let type, _):
- let localizedError = NSLocalizedString("This theme cannot be used because the the type—“%@”—is mismatched in the Info.plist", comment: "Type mismatch")
- informativeText = NSString.localizedStringWithFormat(localizedError as NSString, type as! CVarArg) as String
- case .valueNotFound(let value, _):
- let localizedError = NSLocalizedString("This theme cannot be used because the the value—“%@”—is not found in the Info.plist.", comment: "Decoding value missing")
- informativeText = NSString.localizedStringWithFormat(localizedError as NSString, value as! CVarArg) as String
- case .keyNotFound(let codingKey, _):
- let localizedError = NSLocalizedString("This theme cannot be used because the the key—“%@”—is not found in the Info.plist.", comment: "Decoding key missing")
- informativeText = NSString.localizedStringWithFormat(localizedError as NSString, codingKey.stringValue) as String
- case .dataCorrupted(let context):
- guard let underlyingError = context.underlyingError as NSError?,
- let debugDescription = underlyingError.userInfo["NSDebugDescription"] as? String else {
- informativeText = error.localizedDescription
- break
- }
- let localizedError = NSLocalizedString("This theme cannot be used because of data corruption in the Info.plist: %@.", comment: "Decoding key missing")
- informativeText = NSString.localizedStringWithFormat(localizedError as NSString, debugDescription) as String
-
- default:
- informativeText = error.localizedDescription
- }
- } else {
- informativeText = error.localizedDescription
- }
-
- DispatchQueue.main.async {
- let alert = NSAlert()
- alert.alertStyle = .warning
- alert.messageText = NSLocalizedString("Theme Error", comment: "Theme download error")
- alert.informativeText = informativeText
- alert.addButton(withTitle: NSLocalizedString("Open Theme Folder", comment: "Open Theme Folder"))
- alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK"))
-
- let button = alert.buttons.first
- button?.target = self
- button?.action = #selector(self.openThemesFolder(_:))
- alert.buttons[0].keyEquivalent = "\033"
- alert.buttons[1].keyEquivalent = "\r"
- alert.runModal()
- }
- }
-
- @objc func openThemesFolder(_ sender: Any) {
- if themeImportPath == nil {
- let url = URL(fileURLWithPath: ArticleThemesManager.shared.folderPath)
- NSWorkspace.shared.open(url)
- } else {
- let url = URL(fileURLWithPath: themeImportPath!)
- NSWorkspace.shared.open(url.deletingLastPathComponent())
- }
- }
-
}
/*
@@ -1020,41 +1050,31 @@ extension AppDelegate: NSWindowRestoration {
private extension AppDelegate {
func handleMarkAsRead(userInfo: [AnyHashable: Any]) {
+ markArticle(userInfo: userInfo, statusKey: .read)
+ }
+
+ func handleMarkAsStarred(userInfo: [AnyHashable: Any]) {
+ markArticle(userInfo: userInfo, statusKey: .starred)
+ }
+
+ func markArticle(userInfo: [AnyHashable: Any], statusKey: ArticleStatus.Key) {
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any],
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else {
return
}
- let account = AccountManager.shared.existingAccount(with: accountID)
- guard account != nil else {
+ guard let account = AccountManager.shared.existingAccount(with: accountID) else {
logger.debug("No account found from notification.")
return
}
- let article = try? account!.fetchArticles(.articleIDs([articleID]))
- guard article != nil else {
+
+ guard let articles = try? account.fetchArticles(.articleIDs([articleID])), !articles.isEmpty else {
logger.debug("No article found from search using: \(articleID, privacy: .public)")
return
}
- account!.markArticles(article!, statusKey: .read, flag: true) { _ in }
+
+ account.mark(articles: articles, statusKey: statusKey, flag: true) { _ in }
}
- func handleMarkAsStarred(userInfo: [AnyHashable: Any]) {
- guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any],
- let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
- let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else {
- return
- }
- let account = AccountManager.shared.existingAccount(with: accountID)
- guard account != nil else {
- logger.debug("No account found from notification.")
- return
- }
- let article = try? account!.fetchArticles(.articleIDs([articleID]))
- guard article != nil else {
- logger.debug("No article found from search using: \(articleID, privacy: .public)")
- return
- }
- account!.markArticles(article!, statusKey: .starred, flag: true) { _ in }
- }
}
diff --git a/Mac/Base.lproj/Main.storyboard b/Mac/Base.lproj/Main.storyboard
index 90758a355..06967fdb1 100644
--- a/Mac/Base.lproj/Main.storyboard
+++ b/Mac/Base.lproj/Main.storyboard
@@ -1,8 +1,8 @@
-
+
-
+
@@ -18,7 +18,7 @@
-
+
@@ -539,6 +539,12 @@
+
+
+
+
+
+
diff --git a/Mac/Base.lproj/MainWindow.storyboard b/Mac/Base.lproj/MainWindow.storyboard
index 9d501c240..310a1b4c7 100644
--- a/Mac/Base.lproj/MainWindow.storyboard
+++ b/Mac/Base.lproj/MainWindow.storyboard
@@ -1,8 +1,9 @@
-
+
-
+
+
@@ -174,9 +175,9 @@
-
+
-
+
@@ -294,27 +295,23 @@
-
-
-
-
+
-
+
-
+
-
-
-
-
-
+
+
+
+
@@ -332,16 +329,24 @@
-
+
-
+
+
+
+
+
+
+
+
+
@@ -374,19 +379,20 @@
+
+
+
+
-
-
-
-
-
+
+
-
+
diff --git a/Mac/Browser.swift b/Mac/Browser.swift
index d574bc7ad..7b47dad85 100644
--- a/Mac/Browser.swift
+++ b/Mac/Browser.swift
@@ -73,3 +73,48 @@ extension Browser {
NSLocalizedString("Open in Browser in Background", comment: "Open in Browser in Background menu item title")
}
}
+
+extension Browser {
+
+ /// Open multiple pages in the default browser, warning if over a certain number of URLs are passed.
+ /// - Parameters:
+ /// - urlStrings: The URL strings to open.
+ /// - window: The window on which to display the "over limit" alert sheet. If `nil`, will be displayed as a
+ /// modal dialog.
+ /// - invertPreference: Whether to invert the user's "Open web pages in background in browser" preference.
+ static func open(_ urlStrings: [String], fromWindow window: NSWindow?, invertPreference: Bool = false) {
+ if urlStrings.count > 500 {
+ return
+ }
+
+ func doOpenURLs() {
+ for urlString in urlStrings {
+ Browser.open(urlString, invertPreference: invertPreference)
+ }
+ }
+
+ if urlStrings.count > 20 {
+ let alert = NSAlert()
+ let messageFormat = NSLocalizedString("Are you sure you want to open %ld articles in your browser?", comment: "Open in Browser confirmation alert message format")
+ alert.messageText = String.localizedStringWithFormat(messageFormat, urlStrings.count)
+ let confirmButtonTitleFormat = NSLocalizedString("Open %ld Articles", comment: "Open URLs in Browser confirm button format")
+ alert.addButton(withTitle: String.localizedStringWithFormat(confirmButtonTitleFormat, urlStrings.count))
+ alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel button"))
+
+ if let window {
+ alert.beginSheetModal(for: window) { response in
+ if response == .alertFirstButtonReturn {
+ doOpenURLs()
+ }
+ }
+ } else {
+ if alert.runModal() == .alertFirstButtonReturn {
+ doOpenURLs()
+ }
+ }
+ } else {
+ doOpenURLs()
+ }
+ }
+
+}
diff --git a/Mac/MainWindow/About/AboutNetNewsWireView.swift b/Mac/MainWindow/About/AboutNetNewsWireView.swift
new file mode 100644
index 000000000..94937cc85
--- /dev/null
+++ b/Mac/MainWindow/About/AboutNetNewsWireView.swift
@@ -0,0 +1,55 @@
+//
+// AboutNetNewsWireView.swift
+// NetNewsWire
+//
+// Created by Stuart Breckenridge on 03/10/2022.
+// Copyright © 2022 Ranchero Software. All rights reserved.
+//
+
+import SwiftUI
+
+@available(macOS 12, *)
+struct AboutNetNewsWireView: View {
+ var body: some View {
+ HStack {
+ Spacer()
+ VStack(spacing: 8) {
+ Spacer()
+
+ Image("About")
+ .resizable()
+ .frame(width: 75, height: 75)
+
+ Text("NetNewsWire")
+ .font(.headline)
+
+ Text("\(Bundle.main.versionNumber) (\(Bundle.main.buildNumber))")
+ .foregroundColor(.secondary)
+ .font(.callout)
+
+ Text("By Brent Simmons and the NetNewsWire team.")
+ .font(.subheadline)
+
+ Text("[netnewswire.com](https://netnewswire.com)")
+ .font(.callout)
+
+ Spacer()
+
+ Text(verbatim: "Copyright © Brent Simmons 2002 - \(Calendar.current.component(.year, from: .now))")
+ .font(.callout)
+ .foregroundColor(.secondary)
+ .padding(.bottom)
+ }
+ Spacer()
+ }
+ .multilineTextAlignment(.center)
+ .frame(width: 400, height: 400)
+ }
+}
+
+@available(macOS 12, *)
+struct AboutNetNewsWireView_Previews: PreviewProvider {
+ static var previews: some View {
+ AboutNetNewsWireView()
+ }
+}
diff --git a/Mac/MainWindow/About/AboutWindowController.swift b/Mac/MainWindow/About/AboutWindowController.swift
new file mode 100644
index 000000000..9fbe83ac2
--- /dev/null
+++ b/Mac/MainWindow/About/AboutWindowController.swift
@@ -0,0 +1,131 @@
+//
+// AboutWindowController.swift
+// NetNewsWire
+//
+// Created by Stuart Breckenridge on 03/10/2022.
+// Copyright © 2022 Ranchero Software. All rights reserved.
+//
+
+import AppKit
+import SwiftUI
+import RSCore
+
+extension NSToolbarItem.Identifier {
+ static let aboutGroup = NSToolbarItem.Identifier("about.toolbar.group")
+}
+
+extension NSUserInterfaceItemIdentifier {
+ static let aboutNetNewsWire = NSUserInterfaceItemIdentifier("about.netnewswire")
+}
+
+// MARK: - AboutWindowController
+
+@available(macOS 12, *)
+class AboutWindowController: NSWindowController, NSToolbarDelegate {
+
+ var hostingController: AboutHostingController
+
+ override init(window: NSWindow?) {
+ self.hostingController = AboutHostingController(rootView: AnyView(AboutNetNewsWireView()))
+ super.init(window: window)
+ let window = NSWindow(contentViewController: hostingController)
+ window.identifier = .aboutNetNewsWire
+ window.standardWindowButton(.zoomButton)?.isEnabled = false
+ window.titleVisibility = .hidden
+ self.window = window
+ self.hostingController.configureToolbar()
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func windowDidLoad() {
+ super.windowDidLoad()
+ }
+
+}
+
+// MARK: - AboutHostingController
+
+@available(macOS 12, *)
+class AboutHostingController: NSHostingController, NSToolbarDelegate {
+
+ private lazy var segmentedControl: NSSegmentedControl = {
+ let control = NSSegmentedControl(labels: ["About", "Credits"],
+ trackingMode: .selectOne,
+ target: self,
+ action: #selector(segmentedControlSelectionChanged(_:)))
+ control.segmentCount = 2
+ control.setSelected(true, forSegment: 0)
+ return control
+ }()
+
+ override init(rootView: AnyView) {
+ super.init(rootView: rootView)
+ }
+
+ @MainActor required dynamic init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ public func configureToolbar() {
+ let toolbar = NSToolbar(identifier: NSToolbar.Identifier("netnewswire.about.toolbar"))
+ toolbar.delegate = self
+ toolbar.autosavesConfiguration = false
+ toolbar.allowsUserCustomization = false
+ view.window?.toolbar = toolbar
+ view.window?.toolbarStyle = .unified
+ toolbar.insertItem(withItemIdentifier: .flexibleSpace, at: 0)
+ toolbar.insertItem(withItemIdentifier: .flexibleSpace, at: 2)
+ }
+
+ // MARK: NSToolbarDelegate
+
+ func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
+ switch itemIdentifier {
+
+ case .aboutGroup:
+ let toolbarItem = NSToolbarItem(itemIdentifier: .aboutGroup)
+ toolbarItem.view = segmentedControl
+ toolbarItem.autovalidates = true
+ return toolbarItem
+ default:
+ return nil
+ }
+ }
+
+ func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
+ return [.aboutGroup]
+ }
+
+ func toolbarWillAddItem(_ notification: Notification) {
+ //
+ }
+
+ func toolbarDidRemoveItem(_ notification: Notification) {
+ //
+ }
+
+ func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
+ return [.aboutGroup]
+ }
+
+ func toolbarSelectableItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
+ return []
+ }
+
+ // MARK: - Target/Action
+ @objc
+ func segmentedControlSelectionChanged(_ sender: NSSegmentedControl) {
+ if sender.selectedSegment == 0 {
+ rootView = AnyView(AboutNetNewsWireView())
+ } else {
+ rootView = AnyView(CreditsNetNewsWireView())
+ }
+ }
+
+}
+
+
+
diff --git a/Mac/MainWindow/About/CreditsNetNewsWireView.swift b/Mac/MainWindow/About/CreditsNetNewsWireView.swift
new file mode 100644
index 000000000..88a63fe75
--- /dev/null
+++ b/Mac/MainWindow/About/CreditsNetNewsWireView.swift
@@ -0,0 +1,80 @@
+//
+// CreditsNetNewsWireView.swift
+// NetNewsWire
+//
+// Created by Stuart Breckenridge on 03/10/2022.
+// Copyright © 2022 Ranchero Software. All rights reserved.
+//
+
+import SwiftUI
+
+@available(macOS 12, *)
+struct CreditsNetNewsWireView: View, LoadableAboutData {
+ var body: some View {
+ ScrollView(.vertical, showsIndicators: false) {
+ Spacer()
+ .frame(height: 12)
+ Section("Primary Contributors") {
+ GroupBox {
+ ForEach(0.. some View {
+ HStack {
+ Text(appCredit.name)
+ Spacer()
+ if let role = appCredit.role {
+ Text(role)
+ .foregroundColor(.secondary)
+ }
+ Image(systemName: "info.circle")
+ .foregroundColor(.secondary)
+ }
+ .onTapGesture {
+ guard let url = appCredit.url else { return }
+ if let _ = URL(string: url) {
+ Browser.open(url, inBackground: false)
+ }
+ }
+ }
+}
+
+@available(macOS 12, *)
+struct CreditsNetNewsWireView_Previews: PreviewProvider {
+ static var previews: some View {
+ CreditsNetNewsWireView()
+ }
+}
diff --git a/Mac/MainWindow/Detail/DetailContainerView.swift b/Mac/MainWindow/Detail/DetailContainerView.swift
index 01480ad40..b98e18b0d 100644
--- a/Mac/MainWindow/Detail/DetailContainerView.swift
+++ b/Mac/MainWindow/Detail/DetailContainerView.swift
@@ -30,11 +30,10 @@ final class DetailContainerView: NSView, NSTextFinderBarContainer {
contentView.translatesAutoresizingMaskIntoConstraints = false
addSubview(contentView, positioned: .below, relativeTo: detailStatusBarView)
- // Constrain the content view to fill the available space on all sides except the top, which we'll constrain to the find bar
- var constraints = constraintsToMakeSubViewFullSize(contentView).filter { $0.firstAttribute != .top }
+ // Constrain the content view to fill the available space on all sides
+ var constraints = constraintsToMakeSubViewFullSize(contentView)
constraints.append(findBarContainerView.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor))
- constraints.append(findBarContainerView.bottomAnchor.constraint(equalTo: contentView.topAnchor))
NSLayoutConstraint.activate(constraints)
contentViewConstraints = constraints
}
diff --git a/Mac/MainWindow/Detail/DetailWebView.swift b/Mac/MainWindow/Detail/DetailWebView.swift
index f66555500..56f5d7812 100644
--- a/Mac/MainWindow/Detail/DetailWebView.swift
+++ b/Mac/MainWindow/Detail/DetailWebView.swift
@@ -117,7 +117,7 @@ private extension NSUserInterfaceItemIdentifier {
private extension DetailWebView {
- static let menuItemIdentifiersToHide: [NSUserInterfaceItemIdentifier] = [.DetailMenuItemIdentifierReload, .DetailMenuItemIdentifierOpenLink]
+ static let menuItemIdentifiersToHide: [NSUserInterfaceItemIdentifier] = [.DetailMenuItemIdentifierReload]
static let menuItemIdentifierMatchStrings = ["newwindow", "download"]
func shouldHideMenuItem(_ menuItem: NSMenuItem) -> Bool {
diff --git a/Mac/MainWindow/MainWindowController.swift b/Mac/MainWindow/MainWindowController.swift
index 64dd77e59..36b89dddb 100644
--- a/Mac/MainWindow/MainWindowController.swift
+++ b/Mac/MainWindow/MainWindowController.swift
@@ -204,7 +204,15 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
public func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
if item.action == #selector(copyArticleURL(_:)) {
- return canCopyArticleURL()
+ let canCopyArticleURL = canCopyArticleURL()
+
+ if let item = item as? NSMenuItem {
+ let format = NSLocalizedString("Copy Article URL", comment: "Copy Article URL");
+
+ item.title = String.localizedStringWithFormat(format, selectedArticles?.count ?? 0)
+ }
+
+ return canCopyArticleURL
}
if item.action == #selector(copyExternalURL(_:)) {
@@ -321,21 +329,21 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
}
@IBAction func copyArticleURL(_ sender: Any?) {
- if let link = oneSelectedArticle?.preferredURL?.absoluteString {
- URLPasteboardWriter.write(urlString: link, to: .general)
+ if let currentLinks {
+ URLPasteboardWriter.write(urlStrings: currentLinks, alertingIn: window)
}
}
@IBAction func copyExternalURL(_ sender: Any?) {
- if let link = oneSelectedArticle?.externalLink {
- URLPasteboardWriter.write(urlString: link, to: .general)
+ if let links = selectedArticles?.compactMap({ $0.externalLink }) {
+ URLPasteboardWriter.write(urlStrings: links, to: .general)
}
}
@IBAction func openArticleInBrowser(_ sender: Any?) {
- if let link = currentLink {
- Browser.open(link, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false)
- }
+ guard let selectedArticles else { return }
+ let urlStrings = selectedArticles.compactMap { $0.preferredLink }
+ Browser.open(urlStrings, fromWindow: window, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false)
}
@IBAction func openInBrowser(_ sender: Any?) {
@@ -529,16 +537,17 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
assertionFailure("Expected toolbarShowShareMenu to be called only by the Share item in the toolbar.")
return
}
- guard let view = shareToolbarItem.view else {
- // TODO: handle menu form representation
- return
- }
let sortedArticles = selectedArticles.sortedByDate(.orderedAscending)
let items = sortedArticles.map { ArticlePasteboardWriter(article: $0) }
let sharingServicePicker = NSSharingServicePicker(items: items)
sharingServicePicker.delegate = sharingServicePickerDelegate
- sharingServicePicker.show(relativeTo: view.bounds, of: view, preferredEdge: .minY)
+
+ if let view = shareToolbarItem.view, view.window != nil {
+ sharingServicePicker.show(relativeTo: view.bounds, of: view, preferredEdge: .minY)
+ } else if let view = window?.contentView {
+ sharingServicePicker.show(relativeTo: CGRect(x: view.frame.width / 2.0, y: view.frame.height - 4, width: 1, height: 1), of: view, preferredEdge: .minY)
+ }
}
@IBAction func moveFocusToSearchField(_ sender: Any?) {
@@ -628,6 +637,10 @@ extension MainWindowController: NSWindowDelegate {
extension MainWindowController: SidebarDelegate {
+ var directlyMarkedAsUnreadArticles: Set? {
+ return timelineContainerViewController?.currentTimelineViewController?.directlyMarkedAsUnreadArticles
+ }
+
func sidebarSelectionDidChange(_: SidebarViewController, selectedObjects: [AnyObject]?) {
// Don’t update the timeline if it already has those objects.
let representedObjectsAreTheSame = timelineContainerViewController?.regularTimelineViewControllerHasRepresentedObjects(selectedObjects) ?? false
@@ -666,6 +679,9 @@ extension MainWindowController: TimelineContainerViewControllerDelegate {
articleExtractor = nil
isShowingExtractedArticle = false
makeToolbarValidate()
+ if #available(macOS 13.0, *) { } else {
+ updateShareToolbarItemMenu()
+ }
let detailState: DetailState
if let articles = articles {
@@ -894,11 +910,23 @@ extension MainWindowController: NSToolbarDelegate {
button.action = #selector(toggleArticleExtractor(_:))
button.rightClickAction = #selector(showArticleExtractorMenu(_:))
toolbarItem.view = button
+ toolbarItem.menuFormRepresentation = NSMenuItem(title: description, action: #selector(toggleArticleExtractor(_:)), keyEquivalent: "")
return toolbarItem
case .share:
let title = NSLocalizedString("Share", comment: "Share")
- return buildToolbarButton(.share, title, AppAssets.shareImage, "toolbarShowShareMenu:")
+ let image = AppAssets.shareImage
+ if #available(macOS 13.0, *) {
+ // `item.view` is required for properly positioning the sharing picker.
+ return buildToolbarButton(.share, title, image, "toolbarShowShareMenu:", usesCustomButtonView: true)
+ } else {
+ let item = NSMenuToolbarItem(itemIdentifier: .share)
+ item.image = image
+ item.toolTip = title
+ item.label = title
+ item.showsIndicator = false
+ return item
+ }
case .openInBrowser:
let title = NSLocalizedString("Open in Browser", comment: "Open in Browser")
@@ -1043,7 +1071,11 @@ private extension MainWindowController {
}
var currentLink: String? {
- return oneSelectedArticle?.preferredLink
+ return selectedArticles?.first { $0.preferredLink != nil }?.preferredLink
+ }
+
+ var currentLinks: [String?]? {
+ return selectedArticles?.map { $0.preferredLink }
}
// MARK: - State Restoration
@@ -1081,7 +1113,11 @@ private extension MainWindowController {
// MARK: - Command Validation
func canCopyArticleURL() -> Bool {
- return currentLink != nil
+ if let currentLinks, currentLinks.count != 0 {
+ return true
+ }
+
+ return false
}
func canCopyExternalURL() -> Bool {
@@ -1130,16 +1166,13 @@ private extension MainWindowController {
if let toolbarItem = item as? NSToolbarItem {
toolbarItem.toolTip = commandName
+ toolbarItem.image = markingRead ? AppAssets.readClosedImage : AppAssets.readOpenImage
}
if let menuItem = item as? NSMenuItem {
menuItem.title = commandName
}
- if let toolbarItem = item as? NSToolbarItem, let button = toolbarItem.view as? NSButton {
- button.image = markingRead ? AppAssets.readClosedImage : AppAssets.readOpenImage
- }
-
return result
}
@@ -1220,16 +1253,13 @@ private extension MainWindowController {
if let toolbarItem = item as? NSToolbarItem {
toolbarItem.toolTip = commandName
+ toolbarItem.image = starring ? AppAssets.starOpenImage : AppAssets.starClosedImage
}
if let menuItem = item as? NSMenuItem {
menuItem.title = commandName
}
- if let toolbarItem = item as? NSToolbarItem, let button = toolbarItem.view as? NSButton {
- button.image = starring ? AppAssets.starOpenImage : AppAssets.starClosedImage
- }
-
return result
}
@@ -1252,24 +1282,24 @@ private extension MainWindowController {
guard let isReadFiltered = timelineContainerViewController?.isReadFiltered else {
(item as? NSMenuItem)?.title = hideCommand
- if let toolbarItem = item as? NSToolbarItem, let button = toolbarItem.view as? NSButton {
+ if let toolbarItem = item as? NSToolbarItem {
toolbarItem.toolTip = hideCommand
- button.image = AppAssets.filterInactive
+ toolbarItem.image = AppAssets.filterInactive
}
return false
}
if isReadFiltered {
(item as? NSMenuItem)?.title = showCommand
- if let toolbarItem = item as? NSToolbarItem, let button = toolbarItem.view as? NSButton {
+ if let toolbarItem = item as? NSToolbarItem {
toolbarItem.toolTip = showCommand
- button.image = AppAssets.filterActive
+ toolbarItem.image = AppAssets.filterActive
}
} else {
(item as? NSMenuItem)?.title = hideCommand
- if let toolbarItem = item as? NSToolbarItem, let button = toolbarItem.view as? NSButton {
+ if let toolbarItem = item as? NSToolbarItem {
toolbarItem.toolTip = hideCommand
- button.image = AppAssets.filterInactive
+ toolbarItem.image = AppAssets.filterInactive
}
}
@@ -1386,19 +1416,26 @@ private extension MainWindowController {
}
}
- func buildToolbarButton(_ itemIdentifier: NSToolbarItem.Identifier, _ title: String, _ image: NSImage, _ selector: String) -> NSToolbarItem {
+ func buildToolbarButton(_ itemIdentifier: NSToolbarItem.Identifier, _ title: String, _ image: NSImage, _ selector: String, usesCustomButtonView: Bool = false) -> NSToolbarItem {
let toolbarItem = RSToolbarItem(itemIdentifier: itemIdentifier)
toolbarItem.autovalidates = true
- let button = NSButton()
- button.bezelStyle = .texturedRounded
- button.image = image
- button.imageScaling = .scaleProportionallyDown
- button.action = Selector((selector))
-
- toolbarItem.view = button
toolbarItem.toolTip = title
toolbarItem.label = title
+
+ if usesCustomButtonView {
+ let button = NSButton()
+ button.bezelStyle = .texturedRounded
+ button.image = image
+ button.imageScaling = .scaleProportionallyDown
+ button.action = Selector((selector))
+ toolbarItem.view = button
+ toolbarItem.menuFormRepresentation = NSMenuItem(title: title, action: Selector((selector)), keyEquivalent: "")
+ } else {
+ toolbarItem.image = image
+ toolbarItem.isBordered = true
+ toolbarItem.action = Selector((selector))
+ }
return toolbarItem
}
@@ -1434,7 +1471,7 @@ private extension MainWindowController {
let defaultThemeItem = NSMenuItem()
defaultThemeItem.title = ArticleTheme.defaultTheme.name
defaultThemeItem.action = #selector(selectArticleTheme(_:))
- defaultThemeItem.state = defaultThemeItem.title == ArticleThemesManager.shared.currentThemeName ? .on : .off
+ defaultThemeItem.state = defaultThemeItem.title == ArticleThemesManager.shared.currentTheme.name ? .on : .off
articleThemeMenu.addItem(defaultThemeItem)
articleThemeMenu.addItem(NSMenuItem.separator())
@@ -1443,7 +1480,7 @@ private extension MainWindowController {
let themeItem = NSMenuItem()
themeItem.title = themeName
themeItem.action = #selector(selectArticleTheme(_:))
- themeItem.state = themeItem.title == ArticleThemesManager.shared.currentThemeName ? .on : .off
+ themeItem.state = themeItem.title == ArticleThemesManager.shared.currentTheme.name ? .on : .off
articleThemeMenu.addItem(themeItem)
}
@@ -1451,5 +1488,17 @@ private extension MainWindowController {
articleThemePopUpButton?.menu = articleThemeMenu
}
+ func updateShareToolbarItemMenu() {
+ guard let shareToolbarItem = shareToolbarItem as? NSMenuToolbarItem else {
+ return
+ }
+ if let shareMenu = shareMenu {
+ shareToolbarItem.isEnabled = true
+ shareToolbarItem.menu = shareMenu
+ } else {
+ shareToolbarItem.isEnabled = false
+ }
+ }
+
}
diff --git a/Mac/MainWindow/Sidebar/SidebarStatusBarView.swift b/Mac/MainWindow/Sidebar/SidebarStatusBarView.swift
index d65733d51..315ea4ae3 100644
--- a/Mac/MainWindow/Sidebar/SidebarStatusBarView.swift
+++ b/Mac/MainWindow/Sidebar/SidebarStatusBarView.swift
@@ -31,8 +31,8 @@ final class SidebarStatusBarView: NSView {
}
override func awakeFromNib() {
-
progressIndicator.isHidden = true
+ progressIndicator.usesThreadedAnimation = true
progressLabel.isHidden = true
let progressLabelFontSize = progressLabel.font?.pointSize ?? 13.0
@@ -43,20 +43,18 @@ final class SidebarStatusBarView: NSView {
}
@objc func updateUI() {
-
guard let progress = progress else {
stopProgressIfNeeded()
return
}
updateProgressIndicator(progress)
- updateProgressLabel(progress)
+ progressLabel.stringValue = progress.label
}
// MARK: Notifications
@objc dynamic func progressDidChange(_ notification: Notification) {
-
progress = AccountManager.shared.combinedRefreshProgress
}
}
@@ -68,10 +66,10 @@ private extension SidebarStatusBarView {
static let animationDuration = 0.2
func stopProgressIfNeeded() {
-
if !isAnimatingProgress {
return
}
+
isAnimatingProgress = false
self.progressIndicator.stopAnimation(self)
progressIndicator.isHidden = true
@@ -88,10 +86,10 @@ private extension SidebarStatusBarView {
}
func startProgressIfNeeded() {
-
if isAnimatingProgress {
return
}
+
isAnimatingProgress = true
progressIndicator.isHidden = false
progressLabel.isHidden = false
@@ -108,12 +106,16 @@ private extension SidebarStatusBarView {
}
func updateProgressIndicator(_ progress: CombinedRefreshProgress) {
-
if progress.isComplete {
stopProgressIfNeeded()
return
}
+ if progressIndicator.isIndeterminate != progress.isIndeterminate {
+ stopProgressIfNeeded()
+ progressIndicator.isIndeterminate = progress.isIndeterminate
+ }
+
startProgressIfNeeded()
let maxValue = Double(progress.numberOfTasks)
@@ -127,16 +129,4 @@ private extension SidebarStatusBarView {
}
}
- func updateProgressLabel(_ progress: CombinedRefreshProgress) {
-
- if progress.isComplete {
- progressLabel.stringValue = ""
- return
- }
-
- let formatString = NSLocalizedString("%@ of %@", comment: "Status bar progress")
- let s = NSString(format: formatString as NSString, NSNumber(value: progress.numberCompleted), NSNumber(value: progress.numberOfTasks))
-
- progressLabel.stringValue = s as String
- }
}
diff --git a/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift b/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift
index 9ede13706..9163e33c7 100644
--- a/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift
+++ b/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift
@@ -69,8 +69,16 @@ extension SidebarViewController {
return
}
- let articles = unreadArticles(for: objects)
- guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: Array(articles), markingRead: true, undoManager: undoManager) else {
+ var markableArticles = unreadArticles(for: objects)
+ if let directlyMarkedAsUnreadArticles = delegate?.directlyMarkedAsUnreadArticles {
+ markableArticles = markableArticles.subtracting(directlyMarkedAsUnreadArticles)
+ }
+
+ guard let undoManager = undoManager,
+ let markReadCommand = MarkStatusCommand(initialArticles: markableArticles,
+ markingRead: true,
+ directlyMarked: false,
+ undoManager: undoManager) else {
return
}
runCommand(markReadCommand)
diff --git a/Mac/MainWindow/Sidebar/SidebarViewController.swift b/Mac/MainWindow/Sidebar/SidebarViewController.swift
index 99b33cb90..8dbd08563 100644
--- a/Mac/MainWindow/Sidebar/SidebarViewController.swift
+++ b/Mac/MainWindow/Sidebar/SidebarViewController.swift
@@ -17,6 +17,7 @@ extension Notification.Name {
}
protocol SidebarDelegate: AnyObject {
+ var directlyMarkedAsUnreadArticles: Set? { get }
func sidebarSelectionDidChange(_: SidebarViewController, selectedObjects: [AnyObject]?)
func unreadCount(for: AnyObject) -> Int
func sidebarInvalidatedRestorationState(_: SidebarViewController)
@@ -256,7 +257,11 @@ protocol SidebarDelegate: AnyObject {
return
}
if AppDefaults.shared.feedDoubleClickMarkAsRead, let articles = try? singleSelectedWebFeed?.fetchUnreadArticles() {
- if let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: Array(articles), markingRead: true, undoManager: undoManager) {
+ if let undoManager = undoManager,
+ let markReadCommand = MarkStatusCommand(initialArticles: Array(articles),
+ markingRead: true,
+ directlyMarked: false,
+ undoManager: undoManager) {
runCommand(markReadCommand)
}
}
diff --git a/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift b/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift
index 4a50eadf7..88f0ac440 100644
--- a/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift
+++ b/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift
@@ -39,12 +39,12 @@ extension TimelineViewController {
@objc func markArticlesReadFromContextualMenu(_ sender: Any?) {
guard let articles = articles(from: sender) else { return }
- markArticles(articles, read: true)
+ markArticles(articles, read: true, directlyMarked: true)
}
@objc func markArticlesUnreadFromContextualMenu(_ sender: Any?) {
guard let articles = articles(from: sender) else { return }
- markArticles(articles, read: false)
+ markArticles(articles, read: false, directlyMarked: true)
}
@objc func markAboveArticlesReadFromContextualMenu(_ sender: Any?) {
@@ -59,14 +59,14 @@ extension TimelineViewController {
@objc func markArticlesStarredFromContextualMenu(_ sender: Any?) {
guard let articles = articles(from: sender) else { return }
- markArticles(articles, starred: true)
+ markArticles(articles, starred: true, directlyMarked: true)
}
@objc func markArticlesUnstarredFromContextualMenu(_ sender: Any?) {
guard let articles = articles(from: sender) else {
return
}
- markArticles(articles, starred: false)
+ markArticles(articles, starred: false, directlyMarked: true)
}
@objc func selectFeedInSidebarFromContextualMenu(_ sender: Any?) {
@@ -81,7 +81,11 @@ extension TimelineViewController {
return
}
- guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: feedArticles, markingRead: true, undoManager: undoManager) else {
+ guard let undoManager = undoManager,
+ let markReadCommand = MarkStatusCommand(initialArticles: feedArticles,
+ markingRead: true,
+ directlyMarked: false,
+ undoManager: undoManager) else {
return
}
@@ -89,18 +93,19 @@ extension TimelineViewController {
}
@objc func openInBrowserFromContextualMenu(_ sender: Any?) {
-
- guard let menuItem = sender as? NSMenuItem, let urlString = menuItem.representedObject as? String else {
+ guard let menuItem = sender as? NSMenuItem, let urlStrings = menuItem.representedObject as? [String] else {
return
}
- Browser.open(urlString, inBackground: false)
+
+ Browser.open(urlStrings, fromWindow: self.view.window, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false)
}
@objc func copyURLFromContextualMenu(_ sender: Any?) {
- guard let menuItem = sender as? NSMenuItem, let urlString = menuItem.representedObject as? String else {
+ guard let menuItem = sender as? NSMenuItem, let urlStrings = menuItem.representedObject as? [String?] else {
return
}
- URLPasteboardWriter.write(urlString: urlString, to: .general)
+
+ URLPasteboardWriter.write(urlStrings: urlStrings, alertingIn: self.view.window)
}
@objc func performShareServiceFromContextualMenu(_ sender: Any?) {
@@ -114,16 +119,21 @@ extension TimelineViewController {
private extension TimelineViewController {
- func markArticles(_ articles: [Article], read: Bool) {
- markArticles(articles, statusKey: .read, flag: read)
+ func markArticles(_ articles: [Article], read: Bool, directlyMarked: Bool) {
+ markArticles(articles, statusKey: .read, flag: read, directlyMarked: directlyMarked)
}
- func markArticles(_ articles: [Article], starred: Bool) {
- markArticles(articles, statusKey: .starred, flag: starred)
+ func markArticles(_ articles: [Article], starred: Bool, directlyMarked: Bool) {
+ markArticles(articles, statusKey: .starred, flag: starred, directlyMarked: directlyMarked)
}
- func markArticles(_ articles: [Article], statusKey: ArticleStatus.Key, flag: Bool) {
- guard let undoManager = undoManager, let markStatusCommand = MarkStatusCommand(initialArticles: articles, statusKey: statusKey, flag: flag, undoManager: undoManager) else {
+ func markArticles(_ articles: [Article], statusKey: ArticleStatus.Key, flag: Bool, directlyMarked: Bool) {
+ guard let undoManager = undoManager,
+ let markStatusCommand = MarkStatusCommand(initialArticles: articles,
+ statusKey: statusKey,
+ flag: flag,
+ directlyMarked: directlyMarked,
+ undoManager: undoManager) else {
return
}
@@ -176,14 +186,19 @@ private extension TimelineViewController {
menu.addItem(markAllMenuItem)
}
}
-
- if articles.count == 1, let link = articles.first!.preferredLink {
+
+ let links = articles.map { $0.preferredLink }
+ let compactLinks = links.compactMap { $0 }
+
+ if compactLinks.count > 0 {
menu.addSeparatorIfNeeded()
- menu.addItem(openInBrowserMenuItem(link))
+ menu.addItem(openInBrowserMenuItem(compactLinks))
+ menu.addItem(openInBrowserReversedMenuItem(compactLinks))
+
menu.addSeparatorIfNeeded()
- menu.addItem(copyArticleURLMenuItem(link))
-
- if let externalLink = articles.first?.externalLink, externalLink != link {
+ menu.addItem(copyArticleURLsMenuItem(links))
+
+ if let externalLink = articles.first?.externalLink, externalLink != links.first {
menu.addItem(copyExternalURLMenuItem(externalLink))
}
}
@@ -274,13 +289,21 @@ private extension TimelineViewController {
return menuItem(menuText, #selector(markAllInFeedAsRead(_:)), articles)
}
- func openInBrowserMenuItem(_ urlString: String) -> NSMenuItem {
+ func openInBrowserMenuItem(_ urlStrings: [String]) -> NSMenuItem {
+ return menuItem(NSLocalizedString("Open in Browser", comment: "Command"), #selector(openInBrowserFromContextualMenu(_:)), urlStrings)
+ }
- return menuItem(NSLocalizedString("Open in Browser", comment: "Command"), #selector(openInBrowserFromContextualMenu(_:)), urlString)
+ func openInBrowserReversedMenuItem(_ urlStrings: [String]) -> NSMenuItem {
+ let item = menuItem(Browser.titleForOpenInBrowserInverted, #selector(openInBrowserFromContextualMenu(_:)), urlStrings)
+ item.keyEquivalentModifierMask = .shift
+ item.isAlternate = true
+ return item;
}
- func copyArticleURLMenuItem(_ urlString: String) -> NSMenuItem {
- return menuItem(NSLocalizedString("Copy Article URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), urlString)
+ func copyArticleURLsMenuItem(_ urlStrings: [String?]) -> NSMenuItem {
+ let format = NSLocalizedString("Copy Article URL", comment: "Command")
+ let title = String.localizedStringWithFormat(format, urlStrings.count)
+ return menuItem(title, #selector(copyURLFromContextualMenu(_:)), urlStrings)
}
func copyExternalURLMenuItem(_ urlString: String) -> NSMenuItem {
diff --git a/Mac/MainWindow/Timeline/TimelineViewController.swift b/Mac/MainWindow/Timeline/TimelineViewController.swift
index cb9c7e9d3..c15a59a7b 100644
--- a/Mac/MainWindow/Timeline/TimelineViewController.swift
+++ b/Mac/MainWindow/Timeline/TimelineViewController.swift
@@ -65,7 +65,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
if !representedObjectArraysAreEqual(oldValue, representedObjects) {
unreadCount = 0
- selectionDidChange(nil)
if showsSearchResults {
fetchAndReplaceArticlesAsync()
} else {
@@ -75,6 +74,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
}
updateUnreadCount()
}
+ selectionDidChange(nil)
}
}
}
@@ -123,10 +123,13 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
showFeedNames = .feed
}
+ directlyMarkedAsUnreadArticles = Set()
articleRowMap = [String: [Int]]()
tableView.reloadData()
}
}
+
+ var directlyMarkedAsUnreadArticles = Set()
var unreadCount: Int = 0 {
didSet {
@@ -219,6 +222,8 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange(_:)), name: .UserDidDeleteAccount, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(containerChildrenDidChange(_:)), name: .ChildrenDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
+ NotificationCenter.default.addObserver(self, selector: #selector(markStatusCommandDidDirectMarking(_:)), name: .MarkStatusCommandDidDirectMarking, object: nil)
+ NotificationCenter.default.addObserver(self, selector: #selector(markStatusCommandDidUndoDirectMarking(_:)), name: .MarkStatusCommandDidUndoDirectMarking, object: nil)
didRegisterForNotifications = true
}
}
@@ -230,7 +235,13 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
// MARK: - API
func markAllAsRead(completion: (() -> Void)? = nil) {
- guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: articles, markingRead: true, undoManager: undoManager, completion: completion) else {
+ let markableArticles = Set(articles).subtracting(directlyMarkedAsUnreadArticles)
+ guard let undoManager = undoManager,
+ let markReadCommand = MarkStatusCommand(initialArticles: markableArticles,
+ markingRead: true,
+ directlyMarked: false,
+ undoManager: undoManager,
+ completion: completion) else {
return
}
runCommand(markReadCommand)
@@ -315,9 +326,8 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
// MARK: - Actions
@objc func openArticleInBrowser(_ sender: Any?) {
- if let link = oneSelectedArticle?.preferredLink {
- Browser.open(link, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false)
- }
+ let urlStrings = selectedArticles.compactMap { $0.preferredLink }
+ Browser.open(urlStrings, fromWindow: self.view.window, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false)
}
@IBAction func toggleStatusOfSelectedArticles(_ sender: Any?) {
@@ -337,14 +347,22 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
}
@IBAction func markSelectedArticlesAsRead(_ sender: Any?) {
- guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: selectedArticles, markingRead: true, undoManager: undoManager) else {
+ guard let undoManager = undoManager,
+ let markReadCommand = MarkStatusCommand(initialArticles: selectedArticles,
+ markingRead: true,
+ directlyMarked: true,
+ undoManager: undoManager) else {
return
}
runCommand(markReadCommand)
}
@IBAction func markSelectedArticlesAsUnread(_ sender: Any?) {
- guard let undoManager = undoManager, let markUnreadCommand = MarkStatusCommand(initialArticles: selectedArticles, markingRead: false, undoManager: undoManager) else {
+ guard let undoManager = undoManager,
+ let markUnreadCommand = MarkStatusCommand(initialArticles: selectedArticles,
+ markingRead: false,
+ directlyMarked: true,
+ undoManager: undoManager) else {
return
}
runCommand(markUnreadCommand)
@@ -412,7 +430,11 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
return
}
- guard let undoManager = undoManager, let markStarredCommand = MarkStatusCommand(initialArticles: selectedArticles, markingRead: markingRead, undoManager: undoManager) else {
+ guard let undoManager = undoManager,
+ let markStarredCommand = MarkStatusCommand(initialArticles: selectedArticles,
+ markingRead: markingRead,
+ directlyMarked: true,
+ undoManager: undoManager) else {
return
}
@@ -435,7 +457,11 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
return
}
- guard let undoManager = undoManager, let markStarredCommand = MarkStatusCommand(initialArticles: selectedArticles, markingStarred: starring, undoManager: undoManager) else {
+ guard let undoManager = undoManager,
+ let markStarredCommand = MarkStatusCommand(initialArticles: selectedArticles,
+ markingStarred: starring,
+ directlyMarked: true,
+ undoManager: undoManager) else {
return
}
runCommand(markStarredCommand)
@@ -502,7 +528,12 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
return
}
- guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: articlesToMark, markingRead: true, undoManager: undoManager) else {
+ let markableArticles = Set(articlesToMark).subtracting(directlyMarkedAsUnreadArticles)
+ guard let undoManager = undoManager,
+ let markReadCommand = MarkStatusCommand(initialArticles: markableArticles,
+ markingRead: true,
+ directlyMarked: false,
+ undoManager: undoManager) else {
return
}
runCommand(markReadCommand)
@@ -510,9 +541,16 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
func markAboveArticlesRead(_ selectedArticles: [Article]) {
guard let first = selectedArticles.first else { return }
+
let articlesToMark = articles.articlesAbove(article: first)
guard !articlesToMark.isEmpty else { return }
- guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: articlesToMark, markingRead: true, undoManager: undoManager) else {
+
+ let markableArticles = Set(articlesToMark).subtracting(directlyMarkedAsUnreadArticles)
+ guard let undoManager = undoManager,
+ let markReadCommand = MarkStatusCommand(initialArticles: markableArticles,
+ markingRead: true,
+ directlyMarked: false,
+ undoManager: undoManager) else {
return
}
runCommand(markReadCommand)
@@ -520,9 +558,16 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
func markBelowArticlesRead(_ selectedArticles: [Article]) {
guard let last = selectedArticles.last else { return }
+
let articlesToMark = articles.articlesBelow(article: last)
guard !articlesToMark.isEmpty else { return }
- guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: articlesToMark, markingRead: true, undoManager: undoManager) else {
+
+ let markableArticles = Set(articlesToMark).subtracting(directlyMarkedAsUnreadArticles)
+ guard let undoManager = undoManager,
+ let markReadCommand = MarkStatusCommand(initialArticles: markableArticles,
+ markingRead: true,
+ directlyMarked: false,
+ undoManager: undoManager) else {
return
}
runCommand(markReadCommand)
@@ -666,6 +711,28 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
self.groupByFeed = AppDefaults.shared.timelineGroupByFeed
}
+ @objc func markStatusCommandDidDirectMarking(_ note: Notification) {
+ guard let userInfo = note.userInfo,
+ let articles = userInfo[Account.UserInfoKey.articles] as? Set,
+ let statusKey = userInfo[Account.UserInfoKey.statusKey] as? ArticleStatus.Key,
+ let flag = userInfo[Account.UserInfoKey.statusFlag] as? Bool else { return }
+
+ if statusKey == .read && flag == false {
+ directlyMarkedAsUnreadArticles.formUnion(articles)
+ }
+ }
+
+ @objc func markStatusCommandDidUndoDirectMarking(_ note: Notification) {
+ guard let userInfo = note.userInfo,
+ let articles = userInfo[Account.UserInfoKey.articles] as? Set,
+ let statusKey = userInfo[Account.UserInfoKey.statusKey] as? ArticleStatus.Key,
+ let flag = userInfo[Account.UserInfoKey.statusFlag] as? Bool else { return }
+
+ if statusKey == .read && flag == false {
+ directlyMarkedAsUnreadArticles.subtract(articles)
+ }
+ }
+
// MARK: - Reloading Data
private func cellForRowView(_ rowView: NSView) -> NSView? {
@@ -779,8 +846,7 @@ extension TimelineViewController: NSUserInterfaceValidations {
item.title = Browser.titleForOpenInBrowserInverted
}
- let currentLink = oneSelectedArticle?.preferredLink
- return currentLink != nil
+ return selectedArticles.first { $0.preferredLink != nil } != nil
}
if item.action == #selector(copy(_:)) {
@@ -901,14 +967,22 @@ extension TimelineViewController: NSTableViewDelegate {
}
private func toggleArticleRead(_ article: Article) {
- guard let undoManager = undoManager, let markUnreadCommand = MarkStatusCommand(initialArticles: [article], markingRead: !article.status.read, undoManager: undoManager) else {
+ guard let undoManager = undoManager,
+ let markUnreadCommand = MarkStatusCommand(initialArticles: [article],
+ markingRead: !article.status.read,
+ directlyMarked: true,
+ undoManager: undoManager) else {
return
}
self.runCommand(markUnreadCommand)
}
-
+
private func toggleArticleStarred(_ article: Article) {
- guard let undoManager = undoManager, let markUnreadCommand = MarkStatusCommand(initialArticles: [article], markingStarred: !article.status.starred, undoManager: undoManager) else {
+ guard let undoManager = undoManager,
+ let markUnreadCommand = MarkStatusCommand(initialArticles: [article],
+ markingStarred: !article.status.starred,
+ directlyMarked: true,
+ undoManager: undoManager) else {
return
}
self.runCommand(markUnreadCommand)
diff --git a/Mac/MainWindow/URLPasteboardWriter+NetNewsWire.swift b/Mac/MainWindow/URLPasteboardWriter+NetNewsWire.swift
new file mode 100644
index 000000000..81c3e3ce4
--- /dev/null
+++ b/Mac/MainWindow/URLPasteboardWriter+NetNewsWire.swift
@@ -0,0 +1,36 @@
+//
+// URLPasteboardWriter+NetNewsWire.swift
+// NetNewsWire
+//
+// Created by Nate Weaver on 2022-10-10.
+// Copyright © 2022 Ranchero Software. All rights reserved.
+//
+
+import RSCore
+
+extension URLPasteboardWriter {
+
+ /// Copy URL strings, alerting the user the first time the array of URL strings contains `nil`.
+ /// - Parameters:
+ /// - urlStrings: The URL strings to copy.
+ /// - pasteboard: The pastebaord to copy to.
+ /// - window: The window to use as a sheet parent for the alert. If `nil`, will run the alert modally.
+ static func write(urlStrings: [String?], to pasteboard: NSPasteboard = .general, alertingIn window: NSWindow?) {
+ URLPasteboardWriter.write(urlStrings: urlStrings.compactMap { $0 }, to: pasteboard)
+
+ if urlStrings.contains(nil), !AppDefaults.shared.hasSeenNotAllArticlesHaveURLsAlert {
+ let alert = NSAlert()
+ alert.messageText = NSLocalizedString("Some articles don’t have links, so they weren't copied.", comment: "\"Some articles have no links\" copy alert message text")
+ alert.informativeText = NSLocalizedString("You won't see this message again.", comment: "You won't see this message again")
+
+ if let window {
+ alert.beginSheetModal(for: window)
+ } else {
+ alert.runModal() // this should never happen
+ }
+
+ AppDefaults.shared.hasSeenNotAllArticlesHaveURLsAlert = true
+ }
+ }
+
+}
diff --git a/Mac/Preferences/Accounts/AccountsAddCloudKitWindowController.swift b/Mac/Preferences/Accounts/AccountsAddCloudKitWindowController.swift
index ac1dce14c..6c01d9ef9 100644
--- a/Mac/Preferences/Accounts/AccountsAddCloudKitWindowController.swift
+++ b/Mac/Preferences/Accounts/AccountsAddCloudKitWindowController.swift
@@ -13,7 +13,7 @@ enum AccountsAddCloudKitWindowControllerError: LocalizedError {
case iCloudDriveMissing
var errorDescription: String? {
- return NSLocalizedString("Unable to add iCloud Account. Please make sure you have iCloud and iCloud enabled in System Preferences.", comment: "Unable to add iCloud Account.")
+ return NSLocalizedString("Unable to add iCloud Account. Please make sure you have iCloud and iCloud Drive enabled in System Preferences.", comment: "Unable to add iCloud Account.")
}
}
diff --git a/Mac/Preferences/ExtensionPoints/EnableExtensionPointView.swift b/Mac/Preferences/ExtensionPoints/EnableExtensionPointView.swift
index 09b4ec3b3..5f8d0c113 100644
--- a/Mac/Preferences/ExtensionPoints/EnableExtensionPointView.swift
+++ b/Mac/Preferences/ExtensionPoints/EnableExtensionPointView.swift
@@ -15,10 +15,10 @@ struct EnableExtensionPointView: View {
weak var parent: NSHostingController? // required because presentationMode.dismiss() doesn't work
weak var enabler: ExtensionPointPreferencesEnabler?
- @State private var extensionPointTypeName = String(describing: Self.sendToCommandExtensionPointTypes.first)
+ @State private var extensionPointTypeName = ""
private var selectedType: ExtensionPoint.Type?
- init(enabler: ExtensionPointPreferencesEnabler?, selectedType: ExtensionPoint.Type? ) {
+ init(enabler: ExtensionPointPreferencesEnabler?, selectedType: ExtensionPoint.Type?) {
self.enabler = enabler
self.selectedType = selectedType
}
@@ -99,6 +99,11 @@ struct EnableExtensionPointView: View {
.foregroundColor(.gray)
.font(.caption)
.padding(.horizontal)
+ .onAppear {
+ if extensionPointTypeName.count == 0 {
+ self.extensionPointTypeName = extensionPointTypeNames.first!
+ }
+ }
}
}
@@ -159,7 +164,7 @@ struct EnableExtensionPointView: View {
}
func disableContinue() -> Bool {
- ExtensionPointManager.shared.availableExtensionPointTypes.count == 0
+ ExtensionPointManager.shared.availableExtensionPointTypes.count == 0 || extensionPointTypeName.count == 0
}
}
diff --git a/Mac/Resources/Assets.xcassets/About.imageset/Contents.json b/Mac/Resources/Assets.xcassets/About.imageset/Contents.json
new file mode 100644
index 000000000..5d8cba1f4
--- /dev/null
+++ b/Mac/Resources/Assets.xcassets/About.imageset/Contents.json
@@ -0,0 +1,22 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "Icon-MacOS-512x512@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "Icon-MacOS-512x512@2x 1.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Mac/Resources/Assets.xcassets/About.imageset/Icon-MacOS-512x512@2x 1.png b/Mac/Resources/Assets.xcassets/About.imageset/Icon-MacOS-512x512@2x 1.png
new file mode 100644
index 000000000..95a4b1625
Binary files /dev/null and b/Mac/Resources/Assets.xcassets/About.imageset/Icon-MacOS-512x512@2x 1.png differ
diff --git a/Mac/Resources/Assets.xcassets/About.imageset/Icon-MacOS-512x512@2x.png b/Mac/Resources/Assets.xcassets/About.imageset/Icon-MacOS-512x512@2x.png
new file mode 100644
index 000000000..95a4b1625
Binary files /dev/null and b/Mac/Resources/Assets.xcassets/About.imageset/Icon-MacOS-512x512@2x.png differ
diff --git a/Mac/Resources/Assets.xcassets/preferencesToolbarExtensions.symbolset/Contents.json b/Mac/Resources/Assets.xcassets/preferencesToolbarExtensions.symbolset/Contents.json
index 1a4791180..657cf3c03 100644
--- a/Mac/Resources/Assets.xcassets/preferencesToolbarExtensions.symbolset/Contents.json
+++ b/Mac/Resources/Assets.xcassets/preferencesToolbarExtensions.symbolset/Contents.json
@@ -3,6 +3,9 @@
"author" : "xcode",
"version" : 1
},
+ "properties" : {
+ "symbol-rendering-intent" : "template"
+ },
"symbols" : [
{
"filename" : "preferencesToolbarExtensions.svg",
diff --git a/Mac/Resources/Assets.xcassets/preferencesToolbarExtensions.symbolset/preferencesToolbarExtensions.svg b/Mac/Resources/Assets.xcassets/preferencesToolbarExtensions.symbolset/preferencesToolbarExtensions.svg
index 90f8e2c60..58cd4b435 100644
--- a/Mac/Resources/Assets.xcassets/preferencesToolbarExtensions.symbolset/preferencesToolbarExtensions.svg
+++ b/Mac/Resources/Assets.xcassets/preferencesToolbarExtensions.symbolset/preferencesToolbarExtensions.svg
@@ -1,161 +1,93 @@
-
+
-
+
-
- Weight/Scale Variations
- Ultralight
- Thin
- Light
- Regular
- Medium
- Semibold
- Bold
- Heavy
- Black
-
-
-
+
+ Weight/Scale Variations
+ Ultralight
+ Thin
+ Light
+ Regular
+ Medium
+ Semibold
+ Bold
+ Heavy
+ Black
+
+
+
-
-
+
+
-
-
+
+
- Design Variations
- Symbols are supported in up to nine weights and three scales.
- For optimal layout with text and other symbols, vertically align
- symbols with the adjacent text.
-
-
-
+ Design Variations
+ Symbols are supported in up to nine weights and three scales.
+ For optimal layout with text and other symbols, vertically align
+ symbols with the adjacent text.
+
+
+
-
- Margins
- Leading and trailing margins on the left and right side of each symbol
- can be adjusted by modifying the x-location of the margin guidelines.
- Modifications are automatically applied proportionally to all
- scales and weights.
-
-
+
+ Margins
+ Leading and trailing margins on the left and right side of each symbol
+ can be adjusted by modifying the x-location of the margin guidelines.
+ Modifications are automatically applied proportionally to all
+ scales and weights.
+
+
- Exporting
- Symbols should be outlined when exporting to ensure the
- design is preserved when submitting to Xcode.
- Template v.2.0
- Requires Xcode 12 or greater
- Generated from bolt.fill.batteryblock
- Typeset at 100 points
- Small
- Medium
- Large
+ Exporting
+ Symbols should be outlined when exporting to ensure the
+ design is preserved when submitting to Xcode.
+ Template v.3.0
+ Requires Xcode 13 or greater
+ Generated from puzzlepiece.extension
+ Typeset at 100 points
+ Small
+ Medium
+ Large
-
+
-
+
-
+
-
-
+
+
+
+
+
+
-
-
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
diff --git a/Mac/Resources/Localizable.stringsdict b/Mac/Resources/Localizable.stringsdict
new file mode 100644
index 000000000..bbe875a1f
--- /dev/null
+++ b/Mac/Resources/Localizable.stringsdict
@@ -0,0 +1,22 @@
+
+
+
+
+ Copy Article URL
+
+ NSStringLocalizedFormatKey
+ %#@copy_article_url@
+ copy_article_url
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ other
+ Copy Article URLs
+ one
+ Copy Article URL
+
+
+
+
diff --git a/Mac/Scriptability/AppDelegate+Scriptability.swift b/Mac/Scriptability/AppDelegate+Scriptability.swift
index cfd8310f5..07f3057c2 100644
--- a/Mac/Scriptability/AppDelegate+Scriptability.swift
+++ b/Mac/Scriptability/AppDelegate+Scriptability.swift
@@ -56,15 +56,14 @@ extension AppDelegate : AppDelegateAppleEvents {
if let themeURL = URL(string: themeURLString) {
let request = URLRequest(url: themeURL)
let task = URLSession.shared.downloadTask(with: request) { [weak self] location, response, error in
- guard let location = location else {
+ guard let self, let location else {
return
}
do {
try ArticleThemeDownloader.shared.handleFile(at: location)
} catch {
- NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error": error])
- self?.logger.error("Failed to import theme: \(error.localizedDescription, privacy: .public)")
+ self.presentThemeImportError(error)
}
}
task.resume()
diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj
index adef6cf2a..b30af17ee 100644
--- a/NetNewsWire.xcodeproj/project.pbxproj
+++ b/NetNewsWire.xcodeproj/project.pbxproj
@@ -119,7 +119,6 @@
511D43D2231FA62C00FB1562 /* GlobalKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 844B5B641FEA11F200C7C76A /* GlobalKeyboardShortcuts.plist */; };
511D43EF231FBDE900FB1562 /* LaunchScreenPad.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 511D43ED231FBDE800FB1562 /* LaunchScreenPad.storyboard */; };
511D4419231FC02D00FB1562 /* KeyboardManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511D4410231FC02D00FB1562 /* KeyboardManager.swift */; };
- 51236339236915B100951F16 /* RoundedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 512363372369155100951F16 /* RoundedProgressView.swift */; };
512392BE24E33A3C00F11704 /* RedditSelectAccountTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516AE603246AF37B00731738 /* RedditSelectAccountTableViewController.swift */; };
512392BF24E33A3C00F11704 /* RedditSelectSortTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516AE607246AFC9900731738 /* RedditSelectSortTableViewController.swift */; };
512392C024E33A3C00F11704 /* RedditAdd.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 516AE5FF246AF34100731738 /* RedditAdd.storyboard */; };
@@ -193,6 +192,7 @@
513F32882593EF8F0003048F /* RSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 513F32872593EF8F0003048F /* RSCore */; };
513F32892593EF8F0003048F /* RSCore in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 513F32872593EF8F0003048F /* RSCore */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
5141E7392373C18B0013FF27 /* WebFeedInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5141E7382373C18B0013FF27 /* WebFeedInspectorViewController.swift */; };
+ 514217062921C9DD00963F14 /* Bundle-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BF42273625800C787DC /* Bundle-Extensions.swift */; };
5142192A23522B5500E07E2C /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5142192923522B5500E07E2C /* ImageViewController.swift */; };
514219372352510100E07E2C /* ImageScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514219362352510100E07E2C /* ImageScrollView.swift */; };
5142194B2353C1CF00E07E2C /* main_mac.js in Resources */ = {isa = PBXBuildFile; fileRef = 5142194A2353C1CF00E07E2C /* main_mac.js */; };
@@ -299,7 +299,6 @@
51A1699B235E10D700EB091F /* AccountInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16991235E10D600EB091F /* AccountInspectorViewController.swift */; };
51A1699C235E10D700EB091F /* AddAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16992235E10D600EB091F /* AddAccountViewController.swift */; };
51A1699D235E10D700EB091F /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16993235E10D600EB091F /* SettingsViewController.swift */; };
- 51A1699F235E10D700EB091F /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16995235E10D600EB091F /* AboutViewController.swift */; };
51A169A0235E10D700EB091F /* FeedbinAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16996235E10D700EB091F /* FeedbinAccountViewController.swift */; };
51A66685238075AE00CB272D /* AddWebFeedDefaultContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A66684238075AE00CB272D /* AddWebFeedDefaultContainer.swift */; };
51A737AE24DB19730015FA66 /* RSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 51A737AD24DB19730015FA66 /* RSCore */; };
@@ -399,8 +398,6 @@
51C4CFF224D37D1F00AF9874 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C4CFEF24D37D1F00AF9874 /* Secrets.swift */; };
51C4CFF624D37DD500AF9874 /* Secrets in Frameworks */ = {isa = PBXBuildFile; productRef = 51C4CFF524D37DD500AF9874 /* Secrets */; };
51C9DE5823EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */; };
- 51CE1C0923621EDA005548FC /* RefreshProgressView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */; };
- 51CE1C0B23622007005548FC /* RefreshProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CE1C0A23622006005548FC /* RefreshProgressView.swift */; };
51CE1C712367721A005548FC /* testURLsOfCurrentArticle.applescript in Sources */ = {isa = PBXBuildFile; fileRef = 84F9EADB213660A100CF2DE4 /* testURLsOfCurrentArticle.applescript */; };
51D0214626ED617100FF2E0F /* core.css in Resources */ = {isa = PBXBuildFile; fileRef = 51D0214526ED617100FF2E0F /* core.css */; };
51D0214726ED617100FF2E0F /* core.css in Resources */ = {isa = PBXBuildFile; fileRef = 51D0214526ED617100FF2E0F /* core.css */; };
@@ -408,6 +405,7 @@
51D205EF28E3CF8D007C46EF /* LinkTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D205EE28E3CF8D007C46EF /* LinkTextField.swift */; };
51D205F028E3CF8D007C46EF /* LinkTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D205EE28E3CF8D007C46EF /* LinkTextField.swift */; };
51D5948722668EFA00DFC836 /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */; };
+ 51D5D116291EEF5600AA1278 /* RefreshProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D5D114291EEDC600AA1278 /* RefreshProgressView.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 */; };
@@ -439,10 +437,6 @@
51EF0F7E2277A57D0050506E /* MasterTimelineAccessibilityCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F7D2277A57D0050506E /* MasterTimelineAccessibilityCellLayout.swift */; };
51EF0F802277A8330050506E /* MasterTimelineCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F7F2277A8330050506E /* MasterTimelineCellLayout.swift */; };
51EFDA1B24E6D16A0085C3D6 /* SafariExt.js in Resources */ = {isa = PBXBuildFile; fileRef = 515D4FCB2325815A00EE1167 /* SafariExt.js */; };
- 51F85BEB22724CB600C787DC /* About.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 51F85BEA22724CB600C787DC /* About.rtf */; };
- 51F85BEF2272520B00C787DC /* Thanks.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 51F85BEE2272520B00C787DC /* Thanks.rtf */; };
- 51F85BF12272524100C787DC /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 51F85BF02272524100C787DC /* Credits.rtf */; };
- 51F85BF32272531500C787DC /* Dedication.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 51F85BF22272531500C787DC /* Dedication.rtf */; };
51F85BF52273625800C787DC /* Bundle-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BF42273625800C787DC /* Bundle-Extensions.swift */; };
51F85BF722749FA100C787DC /* UIFont-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BF622749FA100C787DC /* UIFont-Extensions.swift */; };
51F85BF92274AA7B00C787DC /* UIBarButtonItem-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BF82274AA7B00C787DC /* UIBarButtonItem-Extensions.swift */; };
@@ -825,6 +819,7 @@
84F9EAF4213660A100CF2DE4 /* testGenericScript.applescript in Sources */ = {isa = PBXBuildFile; fileRef = 84F9EAE1213660A100CF2DE4 /* testGenericScript.applescript */; };
84F9EAF5213660A100CF2DE4 /* establishMainWindowStartingState.applescript in Sources */ = {isa = PBXBuildFile; fileRef = 84F9EAE2213660A100CF2DE4 /* establishMainWindowStartingState.applescript */; };
84FF69B11FC3793300DC198E /* FaviconURLFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */; };
+ B20180AB28E3B76F0059686A /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = B20180AA28E3B76F0059686A /* Localizable.stringsdict */; };
B24E9ADC245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24E9ABA245AB88300DA5718 /* NSAttributedString+NetNewsWire.swift */; };
B24E9ADD245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24E9ABA245AB88300DA5718 /* NSAttributedString+NetNewsWire.swift */; };
B24E9ADE245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24E9ABA245AB88300DA5718 /* NSAttributedString+NetNewsWire.swift */; };
@@ -834,6 +829,8 @@
B2B8075E239C49D300F191E0 /* RSImage-AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */; };
B2B80778239C4C7000F191E0 /* RSImage-AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */; };
B2B80779239C4C7300F191E0 /* RSImage-AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */; };
+ B2C12C6628F4C46800373730 /* URLPasteboardWriter+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2C12C6528F4C46800373730 /* URLPasteboardWriter+NetNewsWire.swift */; };
+ B2C12C6728F4C46800373730 /* URLPasteboardWriter+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2C12C6528F4C46800373730 /* URLPasteboardWriter+NetNewsWire.swift */; };
B528F81E23333C7E00E735DD /* page.html in Resources */ = {isa = PBXBuildFile; fileRef = B528F81D23333C7E00E735DD /* page.html */; };
BDCB516724282C8A00102A80 /* AccountsNewsBlur.xib in Resources */ = {isa = PBXBuildFile; fileRef = BDCB514D24282C8A00102A80 /* AccountsNewsBlur.xib */; };
BDCB516824282C8A00102A80 /* AccountsNewsBlur.xib in Resources */ = {isa = PBXBuildFile; fileRef = BDCB514D24282C8A00102A80 /* AccountsNewsBlur.xib */; };
@@ -853,11 +850,24 @@
D5F4EDB720074D6500B9E363 /* WebFeed+Scriptability.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F4EDB620074D6500B9E363 /* WebFeed+Scriptability.swift */; };
D5F4EDB920074D7C00B9E363 /* Folder+Scriptability.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F4EDB820074D7C00B9E363 /* Folder+Scriptability.swift */; };
DD82AB0A231003F6002269DF /* SharingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD82AB09231003F6002269DF /* SharingTests.swift */; };
+ DDF9E1D728EDF2FC000BC355 /* notificationSoundBlip.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = DDF9E1D628EDF2FC000BC355 /* notificationSoundBlip.mp3 */; };
+ DDF9E1D828EDF2FC000BC355 /* notificationSoundBlip.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = DDF9E1D628EDF2FC000BC355 /* notificationSoundBlip.mp3 */; };
+ DDF9E1D928EDF2FC000BC355 /* notificationSoundBlip.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = DDF9E1D628EDF2FC000BC355 /* notificationSoundBlip.mp3 */; };
DF5AD10128D6922200CA3BF7 /* SmartFeedSummaryWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1768144D2564BCE000D98635 /* SmartFeedSummaryWidget.swift */; };
- DFD6AACF27ADE86E00463FAD /* NewsFax.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = DFD6AACD27ADE86E00463FAD /* NewsFax.nnwtheme */; };
+ DF790D6228E990A900455FC7 /* AboutData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF790D6128E990A900455FC7 /* AboutData.swift */; };
+ DFC14F0F28EA55BD00F6EE86 /* AboutWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFC14F0E28EA55BD00F6EE86 /* AboutWindowController.swift */; };
+ DFC14F1228EA5DC500F6EE86 /* AboutData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF790D6128E990A900455FC7 /* AboutData.swift */; };
+ DFC14F1328EA677C00F6EE86 /* Bundle-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BF42273625800C787DC /* Bundle-Extensions.swift */; };
+ DFC14F1528EB177000F6EE86 /* AboutNetNewsWireView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFC14F1428EB177000F6EE86 /* AboutNetNewsWireView.swift */; };
+ DFC14F1728EB17A800F6EE86 /* CreditsNetNewsWireView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFC14F1628EB17A800F6EE86 /* CreditsNetNewsWireView.swift */; };
+ DFCE4F9128EF26F100405869 /* About.plist in Resources */ = {isa = PBXBuildFile; fileRef = DFCE4F9028EF26F000405869 /* About.plist */; };
+ DFCE4F9228EF26F100405869 /* About.plist in Resources */ = {isa = PBXBuildFile; fileRef = DFCE4F9028EF26F000405869 /* About.plist */; };
+ DFCE4F9428EF278300405869 /* Thanks.md in Resources */ = {isa = PBXBuildFile; fileRef = DFCE4F9328EF278300405869 /* Thanks.md */; };
+ DFCE4F9528EF278300405869 /* Thanks.md in Resources */ = {isa = PBXBuildFile; fileRef = DFCE4F9328EF278300405869 /* Thanks.md */; };
DFFB8FC2279B75E300AC21D7 /* Account in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 51BC2F4A24D343A500E90810 /* Account */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
DFFC199827A0D0D7004B7AEF /* NotificationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFFC199727A0D0D7004B7AEF /* NotificationsViewController.swift */; };
DFFC199A27A0D32A004B7AEF /* NotificationsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFFC199927A0D32A004B7AEF /* NotificationsTableViewCell.swift */; };
+ DFFC4E7428E95C01006B82AF /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFFC4E7328E95C01006B82AF /* AboutView.swift */; };
FF3ABF13232599810074C542 /* ArticleSorterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF09232599450074C542 /* ArticleSorterTests.swift */; };
FF3ABF1523259DDB0074C542 /* ArticleSorter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */; };
FF3ABF162325AF5D0074C542 /* ArticleSorter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */; };
@@ -1190,7 +1200,6 @@
511B9805237DCAC90028BCAA /* UserInfoKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInfoKey.swift; sourceTree = ""; };
511D43EE231FBDE800FB1562 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreenPad.storyboard; sourceTree = ""; };
511D4410231FC02D00FB1562 /* KeyboardManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardManager.swift; sourceTree = ""; };
- 512363372369155100951F16 /* RoundedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedProgressView.swift; sourceTree = ""; };
5126EE96226CB48A00C22AFC /* SceneCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneCoordinator.swift; sourceTree = ""; };
5127B236222B4849006D641D /* DetailKeyboardDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailKeyboardDelegate.swift; sourceTree = ""; };
5127B237222B4849006D641D /* DetailKeyboardShortcuts.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = DetailKeyboardShortcuts.plist; sourceTree = ""; };
@@ -1295,7 +1304,6 @@
51A16991235E10D600EB091F /* AccountInspectorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountInspectorViewController.swift; sourceTree = ""; };
51A16992235E10D600EB091F /* AddAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddAccountViewController.swift; sourceTree = ""; };
51A16993235E10D600EB091F /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; };
- 51A16995235E10D600EB091F /* AboutViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; };
51A16996235E10D700EB091F /* FeedbinAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedbinAccountViewController.swift; sourceTree = ""; };
51A66684238075AE00CB272D /* AddWebFeedDefaultContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddWebFeedDefaultContainer.swift; sourceTree = ""; };
51A9A5E32380C8870033AADF /* ShareFolderPickerAccountCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ShareFolderPickerAccountCell.xib; sourceTree = ""; };
@@ -1341,10 +1349,9 @@
51CD32C424D2CF1D009ABAEF /* Articles */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Articles; sourceTree = ""; };
51CD32C624D2DEF9009ABAEF /* Account */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Account; sourceTree = ""; };
51CD32C724D2E06C009ABAEF /* Secrets */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Secrets; sourceTree = ""; };
- 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RefreshProgressView.xib; sourceTree = ""; };
- 51CE1C0A23622006005548FC /* RefreshProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshProgressView.swift; sourceTree = ""; };
51D0214526ED617100FF2E0F /* core.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = core.css; sourceTree = ""; };
51D205EE28E3CF8D007C46EF /* LinkTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkTextField.swift; sourceTree = ""; };
+ 51D5D114291EEDC600AA1278 /* 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 = ""; };
@@ -1371,10 +1378,6 @@
51EF0F7F2277A8330050506E /* MasterTimelineCellLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineCellLayout.swift; sourceTree = ""; };
51F805D32428499E0022C792 /* NetNewsWire-dev.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "NetNewsWire-dev.entitlements"; sourceTree = ""; };
51F805ED24284C1C0022C792 /* NetNewsWire-dev.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "NetNewsWire-dev.entitlements"; sourceTree = ""; };
- 51F85BEA22724CB600C787DC /* About.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = About.rtf; sourceTree = ""; };
- 51F85BEE2272520B00C787DC /* Thanks.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Thanks.rtf; sourceTree = ""; };
- 51F85BF02272524100C787DC /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = ""; };
- 51F85BF22272531500C787DC /* Dedication.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Dedication.rtf; sourceTree = ""; };
51F85BF42273625800C787DC /* Bundle-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle-Extensions.swift"; sourceTree = ""; };
51F85BF622749FA100C787DC /* UIFont-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont-Extensions.swift"; sourceTree = ""; };
51F85BF82274AA7B00C787DC /* UIBarButtonItem-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem-Extensions.swift"; sourceTree = ""; };
@@ -1566,11 +1569,13 @@
84F9EAE2213660A100CF2DE4 /* establishMainWindowStartingState.applescript */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.applescript; path = establishMainWindowStartingState.applescript; sourceTree = ""; };
84F9EAE4213660A100CF2DE4 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconURLFinder.swift; sourceTree = ""; };
+ B20180AA28E3B76F0059686A /* Localizable.stringsdict */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = ""; };
B24E9ABA245AB88300DA5718 /* NSAttributedString+NetNewsWire.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+NetNewsWire.swift"; sourceTree = ""; };
B24EFD482330FF99006C6242 /* NetNewsWire-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NetNewsWire-Bridging-Header.h"; sourceTree = ""; };
B24EFD5923310109006C6242 /* WKPreferencesPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WKPreferencesPrivate.h; sourceTree = ""; };
B27EEBDF244D15F2000932E6 /* stylesheet.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = stylesheet.css; sourceTree = ""; };
B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RSImage-AppIcons.swift"; sourceTree = ""; };
+ B2C12C6528F4C46800373730 /* URLPasteboardWriter+NetNewsWire.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLPasteboardWriter+NetNewsWire.swift"; sourceTree = ""; };
B528F81D23333C7E00E735DD /* page.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = page.html; sourceTree = ""; };
BDCB514D24282C8A00102A80 /* AccountsNewsBlur.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountsNewsBlur.xib; sourceTree = ""; };
C5A6ED5123C9AF4300AB6BE2 /* TitleActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleActivityItemSource.swift; sourceTree = ""; };
@@ -1595,10 +1600,17 @@
D5F4EDB620074D6500B9E363 /* WebFeed+Scriptability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebFeed+Scriptability.swift"; sourceTree = ""; };
D5F4EDB820074D7C00B9E363 /* Folder+Scriptability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Folder+Scriptability.swift"; sourceTree = ""; };
DD82AB09231003F6002269DF /* SharingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharingTests.swift; sourceTree = ""; };
+ DDF9E1D628EDF2FC000BC355 /* notificationSoundBlip.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = notificationSoundBlip.mp3; sourceTree = ""; };
+ DF790D6128E990A900455FC7 /* AboutData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutData.swift; sourceTree = ""; };
+ DFC14F0E28EA55BD00F6EE86 /* AboutWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutWindowController.swift; sourceTree = ""; };
+ DFC14F1428EB177000F6EE86 /* AboutNetNewsWireView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutNetNewsWireView.swift; sourceTree = ""; };
+ DFC14F1628EB17A800F6EE86 /* CreditsNetNewsWireView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditsNetNewsWireView.swift; sourceTree = ""; };
+ DFCE4F9028EF26F000405869 /* About.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = About.plist; sourceTree = ""; };
+ DFCE4F9328EF278300405869 /* Thanks.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = Thanks.md; sourceTree = ""; };
DFD6AACB27ADE80900463FAD /* NewsFax.nnwtheme */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = NewsFax.nnwtheme; sourceTree = ""; };
- DFD6AACD27ADE86E00463FAD /* NewsFax.nnwtheme */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = NewsFax.nnwtheme; sourceTree = ""; };
DFFC199727A0D0D7004B7AEF /* NotificationsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsViewController.swift; sourceTree = ""; };
DFFC199927A0D32A004B7AEF /* NotificationsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsTableViewCell.swift; sourceTree = ""; };
+ DFFC4E7328E95C01006B82AF /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; };
FF3ABF09232599450074C542 /* ArticleSorterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleSorterTests.swift; sourceTree = ""; };
FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleSorter.swift; sourceTree = ""; };
FFD43E372340F320009E5CA3 /* MarkAsReadAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkAsReadAlertController.swift; sourceTree = ""; };
@@ -1858,6 +1870,9 @@
511D43CE231FA51100FB1562 /* Resources */ = {
isa = PBXGroup;
children = (
+ DFCE4F9028EF26F000405869 /* About.plist */,
+ DFCE4F9328EF278300405869 /* Thanks.md */,
+ DDF9E1D628EDF2FC000BC355 /* notificationSoundBlip.mp3 */,
DFD6AACB27ADE80900463FAD /* NewsFax.nnwtheme */,
51DEE81126FB9233006DAA56 /* Appanoose.nnwtheme */,
51077C5727A86D16000C71DB /* Hyperlegible.nnwtheme */,
@@ -1970,14 +1985,6 @@
path = Reddit;
sourceTree = "";
};
- 5177C21027B07C8400643901 /* Recovered References */ = {
- isa = PBXGroup;
- children = (
- DFD6AACD27ADE86E00463FAD /* NewsFax.nnwtheme */,
- );
- name = "Recovered References";
- sourceTree = "";
- };
5183CCEA226F70350010922C /* Timer */ = {
isa = PBXGroup;
children = (
@@ -1991,7 +1998,7 @@
5183CCEB227117C70010922C /* Settings */ = {
isa = PBXGroup;
children = (
- 51A16995235E10D600EB091F /* AboutViewController.swift */,
+ DFFC4E7328E95C01006B82AF /* AboutView.swift */,
51A16992235E10D600EB091F /* AddAccountViewController.swift */,
519ED455244828C3007F8E94 /* AddExtensionPointViewController.swift */,
5137C2E926F63AE6009EFEDB /* ArticleThemeImporter.swift */,
@@ -2049,7 +2056,6 @@
children = (
51F9F3FA23DFB25700A314FD /* Animations.swift */,
51F85BFA2275D85000C787DC /* Array-Extensions.swift */,
- 51F85BF42273625800C787DC /* Bundle-Extensions.swift */,
51627A92238A3836007B3B4B /* CroppingPreviewParameters.swift */,
512AF9C1236ED52C0066F8BE /* ImageHeaderView.swift */,
512AF9DC236F05230066F8BE /* InteractiveLabel.swift */,
@@ -2058,7 +2064,6 @@
5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */,
5183CCCF226E1E880010922C /* NonIntrinsicLabel.swift */,
51A9A6092382FD240033AADF /* PoppableGestureRecognizerDelegate.swift */,
- 512363372369155100951F16 /* RoundedProgressView.swift */,
51C45250226506F400C03939 /* String-Extensions.swift */,
5108F6D723763094001ABC45 /* TickMarkSlider.swift */,
C5A6ED6C23C9B0C800AB6BE2 /* UIActivityViewController-Extensions.swift */,
@@ -2081,8 +2086,7 @@
51C45264226508F600C03939 /* MasterFeedViewController.swift */,
51627A6623861DA3007B3B4B /* MasterFeedViewController+Drag.swift */,
51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */,
- 51CE1C0A23622006005548FC /* RefreshProgressView.swift */,
- 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */,
+ 51D5D114291EEDC600AA1278 /* RefreshProgressView.swift */,
5195C1D92720205F00888867 /* ShadowTableChanges.swift */,
51C45260226508F600C03939 /* Cell */,
);
@@ -2260,6 +2264,7 @@
842E45E11ED8C681000A8B52 /* MainWindow */ = {
isa = PBXGroup;
children = (
+ DFC14F0928EA51AB00F6EE86 /* About */,
8483630C2262A3FE00DA1D35 /* MainWindow.storyboard */,
51927A0328E28D1C000AE856 /* MainWindow.swift */,
519279FD28E24CCA000AE856 /* MainWindowController.swift */,
@@ -2268,6 +2273,7 @@
5117715424E1EA0F00A2A836 /* ArticleExtractorButton.swift */,
51FA73B62332D5F70090D516 /* LegacyArticleExtractorButton.swift */,
847CD6C9232F4CBF00FAC46D /* IconView.swift */,
+ B2C12C6528F4C46800373730 /* URLPasteboardWriter+NetNewsWire.swift */,
844B5B6B1FEA224B00C7C76A /* Keyboard */,
849A975F1ED9EB95007D329B /* Sidebar */,
849A97681ED9EBC8007D329B /* Timeline */,
@@ -2398,6 +2404,7 @@
51A66684238075AE00CB272D /* AddWebFeedDefaultContainer.swift */,
849A97731ED9EC04007D329B /* ArticleStringFormatter.swift */,
849A97581ED9EB0D007D329B /* ArticleUtilities.swift */,
+ 51F85BF42273625800C787DC /* Bundle-Extensions.swift */,
5108F6B52375E612001ABC45 /* CacheCleaner.swift */,
516AE9DE2372269A007DEEAA /* IconImage.swift */,
849A97971ED9EFAA007D329B /* Node-Extensions.swift */,
@@ -2512,7 +2519,6 @@
51CD32C324D2CD57009ABAEF /* ArticlesDatabase */,
51CD32C724D2E06C009ABAEF /* Secrets */,
51CD32A824D2CB25009ABAEF /* SyncDatabase */,
- 5177C21027B07C8400643901 /* Recovered References */,
);
sourceTree = "";
usesTabs = 1;
@@ -2585,6 +2591,7 @@
84C9FC6822629C9A00D921D6 /* Shared */ = {
isa = PBXGroup;
children = (
+ DF790D6128E990A900455FC7 /* AboutData.swift */,
842E45CD1ED8C308000A8B52 /* AppNotifications.swift */,
51C4CFEF24D37D1F00AF9874 /* Secrets.swift */,
511B9805237DCAC90028BCAA /* UserInfoKey.swift */,
@@ -2673,6 +2680,7 @@
children = (
849C64671ED37A5D003D8FC0 /* Assets.xcassets */,
84C9FC8922629E8F00D921D6 /* Credits.rtf */,
+ B20180AA28E3B76F0059686A /* Localizable.stringsdict */,
84C9FC8A22629E8F00D921D6 /* NetNewsWire.sdef */,
84C9FC9022629ECB00D921D6 /* NetNewsWire.entitlements */,
51F805D32428499E0022C792 /* NetNewsWire-dev.entitlements */,
@@ -2731,10 +2739,6 @@
84C9FC9A2262A1A900D921D6 /* Resources */ = {
isa = PBXGroup;
children = (
- 51F85BEA22724CB600C787DC /* About.rtf */,
- 51F85BF02272524100C787DC /* Credits.rtf */,
- 51F85BEE2272520B00C787DC /* Thanks.rtf */,
- 51F85BF22272531500C787DC /* Dedication.rtf */,
5103A9B324216A4200410853 /* blank.html */,
51BB7C302335ACDE008E8144 /* page.html */,
514219572353C28900E07E2C /* main_ios.js */,
@@ -2876,6 +2880,16 @@
path = Scriptability;
sourceTree = "";
};
+ DFC14F0928EA51AB00F6EE86 /* About */ = {
+ isa = PBXGroup;
+ children = (
+ DFC14F0E28EA55BD00F6EE86 /* AboutWindowController.swift */,
+ DFC14F1428EB177000F6EE86 /* AboutNetNewsWireView.swift */,
+ DFC14F1628EB17A800F6EE86 /* CreditsNetNewsWireView.swift */,
+ );
+ path = About;
+ sourceTree = "";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -3389,13 +3403,13 @@
65ED405D235DEF6C0081F399 /* SidebarKeyboardShortcuts.plist in Resources */,
514A89A3244FD63F0085E65D /* AddTwitterFeedSheet.xib in Resources */,
51D0214726ED617100FF2E0F /* core.css in Resources */,
+ DDF9E1D828EDF2FC000BC355 /* notificationSoundBlip.mp3 in Resources */,
5103A9F5242258C600410853 /* AccountsAddCloudKit.xib in Resources */,
65ED405E235DEF6C0081F399 /* DefaultFeeds.opml in Resources */,
51333D3C2468615D00EB5C91 /* AddRedditFeedSheet.xib in Resources */,
65ED405F235DEF6C0081F399 /* Preferences.storyboard in Resources */,
65ED4061235DEF6C0081F399 /* Assets.xcassets in Resources */,
65ED4063235DEF6C0081F399 /* RenameSheet.xib in Resources */,
- DFD6AACF27ADE86E00463FAD /* NewsFax.nnwtheme in Resources */,
5177C21227B07C9E00643901 /* NewsFax.nnwtheme in Resources */,
65ED4064235DEF6C0081F399 /* AddFolderSheet.xib in Resources */,
65ED4065235DEF6C0081F399 /* AccountsFeedbin.xib in Resources */,
@@ -3431,6 +3445,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ DFCE4F9228EF26F100405869 /* About.plist in Resources */,
5137C2E626F3F52D009EFEDB /* Sepia.nnwtheme in Resources */,
517630052336215100E15FFF /* main.js in Resources */,
5148F44B2336DB4700F8CD8B /* MasterTimelineTitleView.xib in Resources */,
@@ -3442,31 +3457,28 @@
51BB7C312335ACDE008E8144 /* page.html in Resources */,
512392C324E3451400F11704 /* TwitterAdd.storyboard in Resources */,
516A093723609A3600EAE89B /* SettingsComboTableViewCell.xib in Resources */,
- 51F85BF32272531500C787DC /* Dedication.rtf in Resources */,
51077C5A27A86D16000C71DB /* Hyperlegible.nnwtheme in Resources */,
516A09422361248000EAE89B /* Inspector.storyboard in Resources */,
+ DDF9E1D928EDF2FC000BC355 /* notificationSoundBlip.mp3 in Resources */,
51DEE81A26FBFF84006DAA56 /* Promenade.nnwtheme in Resources */,
1768140B2564BB8300D98635 /* NetNewsWire_iOSwidgetextension_target.xcconfig in Resources */,
5103A9B424216A4200410853 /* blank.html in Resources */,
51D0214826ED617100FF2E0F /* core.css in Resources */,
84C9FCA42262A1B800D921D6 /* LaunchScreenPhone.storyboard in Resources */,
- 51F85BEB22724CB600C787DC /* About.rtf in Resources */,
516A093B2360A4A000EAE89B /* SettingsTableViewCell.xib in Resources */,
511D43D1231FA62800FB1562 /* SidebarKeyboardShortcuts.plist in Resources */,
516A09402361240900EAE89B /* Account.storyboard in Resources */,
51C452AB22650DC600C03939 /* template.html in Resources */,
- 51F85BF12272524100C787DC /* Credits.rtf in Resources */,
84A3EE61223B667F00557320 /* DefaultFeeds.opml in Resources */,
B27EEBFB244D15F3000932E6 /* stylesheet.css in Resources */,
511D43CF231FA62200FB1562 /* DetailKeyboardShortcuts.plist in Resources */,
51A1699A235E10D700EB091F /* Settings.storyboard in Resources */,
49F40DF92335B71000552BF4 /* newsfoot.js in Resources */,
512392C024E33A3C00F11704 /* RedditAdd.storyboard in Resources */,
- 51F85BEF2272520B00C787DC /* Thanks.rtf in Resources */,
5177C21327B07CFE00643901 /* NewsFax.nnwtheme in Resources */,
- 51CE1C0923621EDA005548FC /* RefreshProgressView.xib in Resources */,
84C9FC9D2262A1A900D921D6 /* Assets.xcassets in Resources */,
514219582353C28900E07E2C /* main_ios.js in Resources */,
+ DFCE4F9528EF278300405869 /* Thanks.md in Resources */,
51DEE81426FB9233006DAA56 /* Appanoose.nnwtheme in Resources */,
51E36E8C239D6765006F47A5 /* AddFeedSelectFolderTableViewCell.xib in Resources */,
);
@@ -3491,10 +3503,12 @@
51D0214626ED617100FF2E0F /* core.css in Resources */,
51DEE81826FBFF84006DAA56 /* Promenade.nnwtheme in Resources */,
5142194B2353C1CF00E07E2C /* main_mac.js in Resources */,
+ DFCE4F9128EF26F100405869 /* About.plist in Resources */,
84C9FC8C22629E8F00D921D6 /* KeyboardShortcuts.html in Resources */,
B27EEBF9244D15F3000932E6 /* stylesheet.css in Resources */,
5144EA3B227A379E00D19003 /* ImportOPMLSheet.xib in Resources */,
844B5B691FEA20DF00C7C76A /* SidebarKeyboardShortcuts.plist in Resources */,
+ DDF9E1D728EDF2FC000BC355 /* notificationSoundBlip.mp3 in Resources */,
5103A9F4242258C600410853 /* AccountsAddCloudKit.xib in Resources */,
51077C5827A86D16000C71DB /* Hyperlegible.nnwtheme in Resources */,
84A3EE5F223B667F00557320 /* DefaultFeeds.opml in Resources */,
@@ -3514,12 +3528,14 @@
BDCB516724282C8A00102A80 /* AccountsNewsBlur.xib in Resources */,
514A89A2244FD63F0085E65D /* AddTwitterFeedSheet.xib in Resources */,
5103A9982421643300410853 /* blank.html in Resources */,
+ B20180AB28E3B76F0059686A /* Localizable.stringsdict in Resources */,
515A516E243E7F950089E588 /* ExtensionPointDetail.xib in Resources */,
84BAE64921CEDAF20046DB56 /* CrashReporterWindow.xib in Resources */,
51DEE81226FB9233006DAA56 /* Appanoose.nnwtheme in Resources */,
84C9FC8E22629E8F00D921D6 /* Credits.rtf in Resources */,
84BBB12D20142A4700F054F5 /* Inspector.storyboard in Resources */,
848363022262A3BD00DA1D35 /* AddWebFeedSheet.xib in Resources */,
+ DFCE4F9428EF278300405869 /* Thanks.md in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -3862,6 +3878,7 @@
65ED3FD0235DEF6C0081F399 /* Author+Scriptability.swift in Sources */,
65ED3FD1235DEF6C0081F399 /* PseudoFeed.swift in Sources */,
65ED3FD3235DEF6C0081F399 /* NSScriptCommand+NetNewsWire.swift in Sources */,
+ B2C12C6728F4C46800373730 /* URLPasteboardWriter+NetNewsWire.swift in Sources */,
65ED3FD4235DEF6C0081F399 /* Article+Scriptability.swift in Sources */,
515A5172243E802B0089E588 /* ExtensionPointDetailViewController.swift in Sources */,
65ED3FD5235DEF6C0081F399 /* SmartFeed.swift in Sources */,
@@ -3917,6 +3934,7 @@
65ED3FFA235DEF6C0081F399 /* WebFeedInspectorViewController.swift in Sources */,
65ED3FFB235DEF6C0081F399 /* AccountsReaderAPIWindowController.swift in Sources */,
65ED3FFC235DEF6C0081F399 /* AccountsAddLocalWindowController.swift in Sources */,
+ 514217062921C9DD00963F14 /* Bundle-Extensions.swift in Sources */,
65ED3FFD235DEF6C0081F399 /* PasteboardFolder.swift in Sources */,
51386A8F25673277005F3762 /* AccountCell.swift in Sources */,
65ED3FFE235DEF6C0081F399 /* AccountsFeedbinWindowController.swift in Sources */,
@@ -4022,7 +4040,6 @@
51E36E71239D6610006F47A5 /* AddFeedSelectFolderTableViewCell.swift in Sources */,
512DD4C92430086400C17B1F /* CloudKitAccountViewController.swift in Sources */,
840D617F2029031C009BC708 /* AppDelegate.swift in Sources */,
- 51236339236915B100951F16 /* RoundedProgressView.swift in Sources */,
512E08E72268801200BDCFDD /* WebFeedTreeControllerDelegate.swift in Sources */,
51C452A422650A2D00C03939 /* ArticleUtilities.swift in Sources */,
51EF0F79227716380050506E /* ColorHash.swift in Sources */,
@@ -4071,6 +4088,7 @@
51C4525C226508DF00C03939 /* String-Extensions.swift in Sources */,
51F9F3F923DFB16300A314FD /* UITableView-Extensions.swift in Sources */,
51C452792265091600C03939 /* MasterTimelineTableViewCell.swift in Sources */,
+ DFFC4E7428E95C01006B82AF /* AboutView.swift in Sources */,
51C4526B226508F600C03939 /* MasterFeedViewController.swift in Sources */,
5126EE97226CB48A00C22AFC /* SceneCoordinator.swift in Sources */,
84CAFCB022BC8C35007694F0 /* FetchRequestOperation.swift in Sources */,
@@ -4081,6 +4099,7 @@
517A745B2443665000B553B9 /* UIPageViewController-Extensions.swift in Sources */,
51BB7C272335A8E5008E8144 /* ArticleActivityItemSource.swift in Sources */,
51F85BF52273625800C787DC /* Bundle-Extensions.swift in Sources */,
+ DF790D6228E990A900455FC7 /* AboutData.swift in Sources */,
177A0C2D25454AAB00D7EAF6 /* ReaderAPIAccountViewController.swift in Sources */,
51C452A622650A3500C03939 /* Node-Extensions.swift in Sources */,
51C45294226509C800C03939 /* SearchFeedDelegate.swift in Sources */,
@@ -4119,6 +4138,7 @@
51EF0F802277A8330050506E /* MasterTimelineCellLayout.swift in Sources */,
51F85BF722749FA100C787DC /* UIFont-Extensions.swift in Sources */,
51C452AF2265108300C03939 /* ArticleArray.swift in Sources */,
+ 51D5D116291EEF5600AA1278 /* RefreshProgressView.swift in Sources */,
51C4528E2265099C00C03939 /* SmartFeedsController.swift in Sources */,
51C9DE5823EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift in Sources */,
51B5C87D23F2346200032075 /* ExtensionContainersFile.swift in Sources */,
@@ -4137,7 +4157,6 @@
512AF9C2236ED52C0066F8BE /* ImageHeaderView.swift in Sources */,
512392C124E33A3C00F11704 /* RedditSelectTypeTableViewController.swift in Sources */,
515A5181243E90260089E588 /* ExtensionPointIdentifer.swift in Sources */,
- 51A1699F235E10D700EB091F /* AboutViewController.swift in Sources */,
51C45290226509C100C03939 /* PseudoFeed.swift in Sources */,
512392C624E3451400F11704 /* TwitterSelectAccountTableViewController.swift in Sources */,
51C452A922650DC600C03939 /* ArticleRenderer.swift in Sources */,
@@ -4194,7 +4213,6 @@
51FFF0C4235EE8E5002762AA /* VibrantButton.swift in Sources */,
51C45259226508D300C03939 /* AppDefaults.swift in Sources */,
510FFAB326EEA22C00F32265 /* ArticleThemesTableViewController.swift in Sources */,
- 51CE1C0B23622007005548FC /* RefreshProgressView.swift in Sources */,
511D4419231FC02D00FB1562 /* KeyboardManager.swift in Sources */,
51A1699D235E10D700EB091F /* SettingsViewController.swift in Sources */,
51C45293226509C800C03939 /* StarredFeedDelegate.swift in Sources */,
@@ -4212,6 +4230,7 @@
848B937221C8C5540038DC0D /* CrashReporter.swift in Sources */,
515A5171243E802B0089E588 /* ExtensionPointDetailViewController.swift in Sources */,
847CD6CA232F4CBF00FAC46D /* IconView.swift in Sources */,
+ B2C12C6628F4C46800373730 /* URLPasteboardWriter+NetNewsWire.swift in Sources */,
84BBB12E20142A4700F054F5 /* InspectorWindowController.swift in Sources */,
51EF0F7A22771B890050506E /* ColorHash.swift in Sources */,
84E46C7D1F75EF7B005ECFB3 /* AppDefaults.swift in Sources */,
@@ -4301,6 +4320,7 @@
845A29241FC9255E007B49E3 /* SidebarCellAppearance.swift in Sources */,
515A5107243D0CCD0089E588 /* TwitterFeedProvider-Extensions.swift in Sources */,
845EE7B11FC2366500854A1F /* StarredFeedDelegate.swift in Sources */,
+ DFC14F1228EA5DC500F6EE86 /* AboutData.swift in Sources */,
848F6AE51FC29CFB002D422E /* FaviconDownloader.swift in Sources */,
511B9806237DCAC90028BCAA /* UserInfoKey.swift in Sources */,
84C9FC7722629E1200D921D6 /* AdvancedPreferencesViewController.swift in Sources */,
@@ -4328,12 +4348,15 @@
849A97801ED9EC42007D329B /* DetailViewController.swift in Sources */,
173A64172547BE0900267F6E /* AccountType+Helpers.swift in Sources */,
518C3193237B00D9004D740F /* DetailIconSchemeHandler.swift in Sources */,
+ DFC14F1528EB177000F6EE86 /* AboutNetNewsWireView.swift in Sources */,
84C9FC6722629B9000D921D6 /* AppDelegate.swift in Sources */,
510C417F24E5D1AE008226FD /* ExtensionContainersFile.swift in Sources */,
84C9FC7A22629E1200D921D6 /* PreferencesTableViewBackgroundView.swift in Sources */,
84CAFCAF22BC8C35007694F0 /* FetchRequestOperation.swift in Sources */,
+ DFC14F1328EA677C00F6EE86 /* Bundle-Extensions.swift in Sources */,
8426119E1FCB6ED40086A189 /* HTMLMetadataDownloader.swift in Sources */,
849A976E1ED9EBC8007D329B /* TimelineViewController.swift in Sources */,
+ DFC14F1728EB17A800F6EE86 /* CreditsNetNewsWireView.swift in Sources */,
5154368B229404D1005E1CDF /* FaviconGenerator.swift in Sources */,
5183CCE6226F4E110010922C /* RefreshInterval.swift in Sources */,
849A97771ED9EC04007D329B /* TimelineCellData.swift in Sources */,
@@ -4342,6 +4365,7 @@
8454C3F8263F3AD400E3F9C7 /* IconImageCache.swift in Sources */,
518651B223555EB20078E021 /* NNW3Document.swift in Sources */,
D5F4EDB5200744A700B9E363 /* ScriptingObject.swift in Sources */,
+ DFC14F0F28EA55BD00F6EE86 /* AboutWindowController.swift in Sources */,
D5F4EDB920074D7C00B9E363 /* Folder+Scriptability.swift in Sources */,
849A97781ED9EC04007D329B /* TimelineCellLayout.swift in Sources */,
84E8E0EB202F693600562D8F /* DetailWebView.swift in Sources */,
@@ -4922,7 +4946,7 @@
repositoryURL = "https://github.com/Ranchero-Software/RSCore.git";
requirement = {
kind = upToNextMajorVersion;
- minimumVersion = 1.0.0;
+ minimumVersion = 1.1.0;
};
};
510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */ = {
diff --git a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 484a5cd1b..89207c710 100644
--- a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -51,8 +51,8 @@
"repositoryURL": "https://github.com/microsoft/plcrashreporter.git",
"state": {
"branch": null,
- "revision": "81cdec2b3827feb03286cb297f4c501a8eb98df1",
- "version": "1.10.2"
+ "revision": "b1a342da19ed9b3af61ea2efa7656c2af30aeb7c",
+ "version": "1.11.0"
}
},
{
@@ -60,8 +60,8 @@
"repositoryURL": "https://github.com/Ranchero-Software/RSCore.git",
"state": {
"branch": null,
- "revision": "4425a29db97b97c44e9ebee16e6090b116b10055",
- "version": "1.0.14"
+ "revision": "fd64fb77de2c4b6a87a971d353e7eea75100f694",
+ "version": "1.1.3"
}
},
{
@@ -96,8 +96,8 @@
"repositoryURL": "https://github.com/Ranchero-Software/RSWeb.git",
"state": {
"branch": null,
- "revision": "2f7849a9ad2cb461b3d6c9c920e163596e6b5d7b",
- "version": "1.0.3"
+ "revision": "c8d6212b08ae86142105e828fda391a6503a2ea7",
+ "version": "1.0.6"
}
},
{
diff --git a/Shared/AboutData.swift b/Shared/AboutData.swift
new file mode 100644
index 000000000..4e76aa141
--- /dev/null
+++ b/Shared/AboutData.swift
@@ -0,0 +1,48 @@
+//
+// AboutData.swift
+// NetNewsWire-iOS
+//
+// Created by Stuart Breckenridge on 02/10/2022.
+// Copyright © 2022 Ranchero Software. All rights reserved.
+//
+
+import Foundation
+
+@available(iOS 15, *)
+@available(macOS 12, *)
+protocol LoadableAboutData {
+ var about: AboutData { get }
+}
+
+@available(iOS 15, *)
+@available(macOS 12, *)
+extension LoadableAboutData {
+
+ var about: AboutData {
+ guard let path = Bundle.main.path(forResource: "About", ofType: "plist") else {
+ fatalError("The about plist really should exist.")
+ }
+ let url = URL(fileURLWithPath: path)
+ let data = try! Data(contentsOf: url)
+ return try! PropertyListDecoder().decode(AboutData.self, from: data)
+ }
+
+}
+
+@available(iOS 15, *)
+@available(macOS 12, *)
+struct AboutData: Codable {
+ var PrimaryContributors: [Contributor]
+ var AdditionalContributors: [Contributor]
+
+ var ThanksMarkdown: AttributedString {
+ let dataURL = Bundle.main.url(forResource: "Thanks", withExtension: "md")!
+ return try! AttributedString(markdown: Data(contentsOf: dataURL), options: AttributedString.MarkdownParsingOptions(interpretedSyntax: .inlineOnlyPreservingWhitespace))
+ }
+
+ struct Contributor: Codable {
+ var name: String
+ var url: String?
+ var role: String?
+ }
+}
diff --git a/Shared/Article Rendering/stylesheet.css b/Shared/Article Rendering/stylesheet.css
index a1059a182..902be2fa6 100644
--- a/Shared/Article Rendering/stylesheet.css
+++ b/Shared/Article Rendering/stylesheet.css
@@ -219,6 +219,10 @@ img, figure, video, div, object {
margin: 0 auto;
}
+video {
+ width: 100% !important;
+}
+
iframe {
max-width: 100%;
margin: 0 auto;
diff --git a/Shared/ArticleStyles/ArticleTheme+Notifications.swift b/Shared/ArticleStyles/ArticleTheme+Notifications.swift
index 4c2391ff4..298a28c47 100644
--- a/Shared/ArticleStyles/ArticleTheme+Notifications.swift
+++ b/Shared/ArticleStyles/ArticleTheme+Notifications.swift
@@ -11,5 +11,4 @@ import Foundation
extension Notification.Name {
static let didBeginDownloadingTheme = Notification.Name("didBeginDownloadingTheme")
static let didEndDownloadingTheme = Notification.Name("didEndDownloadingTheme")
- static let didFailToImportThemeWithError = Notification.Name("didFailToImportThemeWithError")
}
diff --git a/Shared/ArticleStyles/ArticleThemesManager.swift b/Shared/ArticleStyles/ArticleThemesManager.swift
index ab66608a9..254cccbd1 100644
--- a/Shared/ArticleStyles/ArticleThemesManager.swift
+++ b/Shared/ArticleStyles/ArticleThemesManager.swift
@@ -30,20 +30,30 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging {
}
set {
if newValue != currentThemeName {
- AppDefaults.shared.currentThemeName = newValue
- updateThemeNames()
- updateCurrentTheme()
+ do {
+ currentTheme = try articleThemeWithThemeName(newValue)
+ AppDefaults.shared.currentThemeName = newValue
+ } catch {
+ logger.error("Unable to set new theme: \(error.localizedDescription, privacy: .public)")
+ }
}
}
}
- var currentTheme: ArticleTheme {
+ lazy var currentTheme = {
+ do {
+ return try articleThemeWithThemeName(currentThemeName)
+ } catch {
+ logger.error("Unable to load theme \(self.currentThemeName): \(error.localizedDescription, privacy: .public)")
+ return ArticleTheme.defaultTheme
+ }
+ }() {
didSet {
NotificationCenter.default.post(name: .CurrentArticleThemeDidChangeNotification, object: self)
}
}
- var themeNames = [AppDefaults.defaultThemeName] {
+ lazy var themeNames = { buildThemeNames() }() {
didSet {
NotificationCenter.default.post(name: .ArticleThemeNamesDidChangeNotification, object: self)
}
@@ -51,7 +61,6 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging {
init(folderPath: String) {
self.folderPath = folderPath
- self.currentTheme = ArticleTheme.defaultTheme
super.init()
@@ -63,15 +72,16 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging {
abort()
}
- updateThemeNames()
- updateCurrentTheme()
-
NSFileCoordinator.addFilePresenter(self)
}
func presentedSubitemDidChange(at url: URL) {
- updateThemeNames()
- updateCurrentTheme()
+ themeNames = buildThemeNames()
+ do {
+ currentTheme = try articleThemeWithThemeName(currentThemeName)
+ } catch {
+ appDelegate.presentThemeImportError(error)
+ }
}
// MARK: API
@@ -93,7 +103,7 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging {
try FileManager.default.copyItem(atPath: filename, toPath: toFilename)
}
- func articleThemeWithThemeName(_ themeName: String) -> ArticleTheme? {
+ func articleThemeWithThemeName(_ themeName: String) throws -> ArticleTheme {
if themeName == AppDefaults.defaultThemeName {
return ArticleTheme.defaultTheme
}
@@ -107,17 +117,10 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging {
path = installedPath
isAppTheme = false
} else {
- return nil
- }
-
- do {
- return try ArticleTheme(path: path, isAppTheme: isAppTheme)
- } catch {
- NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error": error])
- logger.error("Failed to import theme: \(error.localizedDescription, privacy: .public)")
- return nil
+ return ArticleTheme.defaultTheme
}
+ return try ArticleTheme(path: path, isAppTheme: isAppTheme)
}
func deleteTheme(themeName: String) {
@@ -132,7 +135,7 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging {
private extension ArticleThemesManager {
- func updateThemeNames() {
+ func buildThemeNames() -> [String] {
let appThemeFilenames = Bundle.main.paths(forResourcesOfType: ArticleTheme.nnwThemeSuffix, inDirectory: nil)
let appThemeNames = Set(appThemeFilenames.map { ArticleTheme.themeNameForPath($0) })
@@ -140,32 +143,7 @@ private extension ArticleThemesManager {
let allThemeNames = appThemeNames.union(installedThemeNames)
- let sortedThemeNames = allThemeNames.sorted(by: { $0.compare($1, options: .caseInsensitive) == .orderedAscending })
- if sortedThemeNames != themeNames {
- themeNames = sortedThemeNames
- }
- }
-
- func defaultArticleTheme() -> ArticleTheme {
- return articleThemeWithThemeName(AppDefaults.defaultThemeName)!
- }
-
- func updateCurrentTheme() {
- var themeName = currentThemeName
- if !themeNames.contains(themeName) {
- themeName = AppDefaults.defaultThemeName
- currentThemeName = AppDefaults.defaultThemeName
- }
-
- var articleTheme = articleThemeWithThemeName(themeName)
- if articleTheme == nil {
- articleTheme = defaultArticleTheme()
- currentThemeName = AppDefaults.defaultThemeName
- }
-
- if let articleTheme = articleTheme, articleTheme != currentTheme {
- currentTheme = articleTheme
- }
+ return allThemeNames.sorted(by: { $0.localizedStandardCompare($1) == .orderedAscending })
}
func allThemePaths(_ folder: String) -> [String] {
diff --git a/Shared/Commands/MarkStatusCommand.swift b/Shared/Commands/MarkStatusCommand.swift
index 79cf9777c..aa3e39079 100644
--- a/Shared/Commands/MarkStatusCommand.swift
+++ b/Shared/Commands/MarkStatusCommand.swift
@@ -8,9 +8,20 @@
import Foundation
import RSCore
+import Account
import Articles
// Mark articles read/unread, starred/unstarred, deleted/undeleted.
+//
+// Directly marked articles are ones that were statused by selecting with a cursor or were selected by group.
+// Indirectly marked articles didn't have any focus and were picked up using a Mark All command like Mark All as Read.
+//
+// See discussion for details: https://github.com/Ranchero-Software/NetNewsWire/issues/3734
+
+public extension Notification.Name {
+ static let MarkStatusCommandDidDirectMarking = Notification.Name("MarkStatusCommandDid√DirectMarking")
+ static let MarkStatusCommandDidUndoDirectMarking = Notification.Name("MarkStatusCommandDidUndoDirectMarking")
+}
final class MarkStatusCommand: UndoableCommand {
@@ -19,10 +30,11 @@ final class MarkStatusCommand: UndoableCommand {
let articles: Set
let undoManager: UndoManager
let flag: Bool
+ let directlyMarked: Bool
let statusKey: ArticleStatus.Key
var completion: (() -> Void)? = nil
- init?(initialArticles: [Article], statusKey: ArticleStatus.Key, flag: Bool, undoManager: UndoManager, completion: (() -> Void)? = nil) {
+ init?(initialArticles: Set, statusKey: ArticleStatus.Key, flag: Bool, directlyMarked: Bool, undoManager: UndoManager, completion: (() -> Void)? = nil) {
// Filter out articles that already have the desired status or can't be marked.
let articlesToMark = MarkStatusCommand.filteredArticles(initialArticles, statusKey, flag)
@@ -30,8 +42,9 @@ final class MarkStatusCommand: UndoableCommand {
completion?()
return nil
}
- self.articles = Set(articlesToMark)
+ self.articles = articlesToMark
+ self.directlyMarked = directlyMarked
self.flag = flag
self.statusKey = statusKey
self.undoManager = undoManager
@@ -42,21 +55,39 @@ final class MarkStatusCommand: UndoableCommand {
self.redoActionName = actionName
}
- convenience init?(initialArticles: [Article], markingRead: Bool, undoManager: UndoManager, completion: (() -> Void)? = nil) {
- self.init(initialArticles: initialArticles, statusKey: .read, flag: markingRead, undoManager: undoManager, completion: completion)
+ convenience init?(initialArticles: [Article], statusKey: ArticleStatus.Key, flag: Bool, directlyMarked: Bool, undoManager: UndoManager, completion: (() -> Void)? = nil) {
+ self.init(initialArticles: Set(initialArticles), statusKey: .read, flag: flag, directlyMarked: directlyMarked, undoManager: undoManager, completion: completion)
}
- convenience init?(initialArticles: [Article], markingStarred: Bool, undoManager: UndoManager, completion: (() -> Void)? = nil) {
- self.init(initialArticles: initialArticles, statusKey: .starred, flag: markingStarred, undoManager: undoManager, completion: completion)
+ convenience init?(initialArticles: Set, markingRead: Bool, directlyMarked: Bool, undoManager: UndoManager, completion: (() -> Void)? = nil) {
+ self.init(initialArticles: initialArticles, statusKey: .read, flag: markingRead, directlyMarked: directlyMarked, undoManager: undoManager, completion: completion)
+ }
+
+ convenience init?(initialArticles: [Article], markingRead: Bool, directlyMarked: Bool, undoManager: UndoManager, completion: (() -> Void)? = nil) {
+ self.init(initialArticles: initialArticles, statusKey: .read, flag: markingRead, directlyMarked: directlyMarked, undoManager: undoManager, completion: completion)
+ }
+
+ convenience init?(initialArticles: Set, markingStarred: Bool, directlyMarked: Bool, undoManager: UndoManager, completion: (() -> Void)? = nil) {
+ self.init(initialArticles: initialArticles, statusKey: .starred, flag: markingStarred, directlyMarked: directlyMarked, undoManager: undoManager, completion: completion)
+ }
+
+ convenience init?(initialArticles: [Article], markingStarred: Bool, directlyMarked: Bool, undoManager: UndoManager, completion: (() -> Void)? = nil) {
+ self.init(initialArticles: initialArticles, statusKey: .starred, flag: markingStarred, directlyMarked: directlyMarked, undoManager: undoManager, completion: completion)
}
func perform() {
mark(statusKey, flag)
+ if directlyMarked {
+ markStatusCommandDidDirectMarking()
+ }
registerUndo()
}
func undo() {
mark(statusKey, !flag)
+ if directlyMarked {
+ markStatusCommandDidUndoDirectMarking()
+ }
registerRedo()
}
}
@@ -67,6 +98,18 @@ private extension MarkStatusCommand {
markArticles(articles, statusKey: statusKey, flag: flag, completion: completion)
completion = nil
}
+
+ func markStatusCommandDidDirectMarking() {
+ NotificationCenter.default.post(name: .MarkStatusCommandDidDirectMarking, object: self, userInfo: [Account.UserInfoKey.articles: articles,
+ Account.UserInfoKey.statusKey: statusKey,
+ Account.UserInfoKey.statusFlag: flag])
+ }
+
+ func markStatusCommandDidUndoDirectMarking() {
+ NotificationCenter.default.post(name: .MarkStatusCommandDidUndoDirectMarking, object: self, userInfo: [Account.UserInfoKey.articles: articles,
+ Account.UserInfoKey.statusKey: statusKey,
+ Account.UserInfoKey.statusFlag: flag])
+ }
static private let markReadActionName = NSLocalizedString("Mark Read", comment: "command")
static private let markUnreadActionName = NSLocalizedString("Mark Unread", comment: "command")
@@ -83,7 +126,7 @@ private extension MarkStatusCommand {
}
}
- static func filteredArticles(_ articles: [Article], _ statusKey: ArticleStatus.Key, _ flag: Bool) -> [Article] {
+ static func filteredArticles(_ articles: Set, _ statusKey: ArticleStatus.Key, _ flag: Bool) -> Set {
return articles.filter{ article in
guard article.status.boolStatus(forKey: statusKey) != flag else { return false }
@@ -93,4 +136,5 @@ private extension MarkStatusCommand {
}
}
+
}
diff --git a/Shared/Extensions/ArticleUtilities.swift b/Shared/Extensions/ArticleUtilities.swift
index 0b52c15d8..1d19a4742 100644
--- a/Shared/Extensions/ArticleUtilities.swift
+++ b/Shared/Extensions/ArticleUtilities.swift
@@ -14,7 +14,6 @@ import Account
// These handle multiple accounts.
func markArticles(_ articles: Set, statusKey: ArticleStatus.Key, flag: Bool, completion: (() -> Void)? = nil) {
-
let d: [String: Set] = accountAndArticlesDictionary(articles)
let group = DispatchGroup()
@@ -24,7 +23,7 @@ func markArticles(_ articles: Set, statusKey: ArticleStatus.Key, flag:
continue
}
group.enter()
- account.markArticles(accountArticles, statusKey: statusKey, flag: flag) { _ in
+ account.mark(articles: accountArticles, statusKey: statusKey, flag: flag) { _ in
group.leave()
}
}
diff --git a/iOS/UIKit Extensions/Bundle-Extensions.swift b/Shared/Extensions/Bundle-Extensions.swift
similarity index 100%
rename from iOS/UIKit Extensions/Bundle-Extensions.swift
rename to Shared/Extensions/Bundle-Extensions.swift
diff --git a/Shared/Extensions/CacheCleaner.swift b/Shared/Extensions/CacheCleaner.swift
index 2364a59fb..b80896c5d 100644
--- a/Shared/Extensions/CacheCleaner.swift
+++ b/Shared/Extensions/CacheCleaner.swift
@@ -23,23 +23,7 @@ struct CacheCleaner: Logging {
if flushDate.addingTimeInterval(3600 * 24 * 3) < Date() {
if let reachability = try? Reachability(hostname: "apple.com") {
if reachability.connection != .unavailable {
-
- let tempDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
- let faviconsFolderURL = tempDir.appendingPathComponent("Favicons")
- let imagesFolderURL = tempDir.appendingPathComponent("Images")
- let feedURLToIconURL = tempDir.appendingPathComponent("FeedURLToIconURLCache.plist")
- let homePageToIconURL = tempDir.appendingPathComponent("HomePageToIconURLCache.plist")
- let homePagesWithNoIconURL = tempDir.appendingPathComponent("HomePagesWithNoIconURLCache.plist")
-
- for tempItem in [faviconsFolderURL, imagesFolderURL, feedURLToIconURL, homePageToIconURL, homePagesWithNoIconURL] {
- do {
- CacheCleaner.logger.info("Removing cache file: \(tempItem.absoluteString, privacy: .public)")
- try FileManager.default.removeItem(at: tempItem)
- } catch {
- CacheCleaner.logger.error("Could not delete cache file: \(error.localizedDescription, privacy: .public)")
- }
- }
-
+ purge()
AppDefaults.shared.lastImageCacheFlushDate = Date()
}
@@ -48,4 +32,22 @@ struct CacheCleaner: Logging {
}
+ static func purge() {
+ let tempDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
+ let faviconsFolderURL = tempDir.appendingPathComponent("Favicons")
+ let imagesFolderURL = tempDir.appendingPathComponent("Images")
+ let feedURLToIconURL = tempDir.appendingPathComponent("FeedURLToIconURLCache.plist")
+ let homePageToIconURL = tempDir.appendingPathComponent("HomePageToIconURLCache.plist")
+ let homePagesWithNoIconURL = tempDir.appendingPathComponent("HomePagesWithNoIconURLCache.plist")
+
+ for tempItem in [faviconsFolderURL, imagesFolderURL, feedURLToIconURL, homePageToIconURL, homePagesWithNoIconURL] {
+ do {
+ CacheCleaner.logger.info("Removing cache file: \(tempItem.absoluteString, privacy: .public)")
+ try FileManager.default.removeItem(at: tempItem)
+ } catch {
+ CacheCleaner.logger.error("Could not delete cache file: \(error.localizedDescription, privacy: .public)")
+ }
+ }
+ }
+
}
diff --git a/Shared/Extensions/NSView-Extensions.swift b/Shared/Extensions/NSView-Extensions.swift
index e9449208e..2c5aff1d0 100644
--- a/Shared/Extensions/NSView-Extensions.swift
+++ b/Shared/Extensions/NSView-Extensions.swift
@@ -11,20 +11,10 @@ import AppKit
extension NSView {
func constraintsToMakeSubViewFullSize(_ subview: NSView) -> [NSLayoutConstraint] {
-
- if #available(macOS 11, *) {
- let leadingConstraint = NSLayoutConstraint(item: subview, attribute: .leading, relatedBy: .equal, toItem: self.safeAreaLayoutGuide, attribute: .leading, multiplier: 1.0, constant: 0.0)
- let trailingConstraint = NSLayoutConstraint(item: subview, attribute: .trailing, relatedBy: .equal, toItem: self.safeAreaLayoutGuide, attribute: .trailing, multiplier: 1.0, constant: 0.0)
- let topConstraint = NSLayoutConstraint(item: subview, attribute: .top, relatedBy: .equal, toItem: self.safeAreaLayoutGuide, attribute: .top, multiplier: 1.0, constant: 0.0)
- let bottomConstraint = NSLayoutConstraint(item: subview, attribute: .bottom, relatedBy: .equal, toItem: self.safeAreaLayoutGuide, attribute: .bottom, multiplier: 1.0, constant: 0.0)
- return [leadingConstraint, trailingConstraint, topConstraint, bottomConstraint]
- } else {
- let leadingConstraint = NSLayoutConstraint(item: subview, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1.0, constant: 0.0)
- let trailingConstraint = NSLayoutConstraint(item: subview, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1.0, constant: 0.0)
- let topConstraint = NSLayoutConstraint(item: subview, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 0.0)
- let bottomConstraint = NSLayoutConstraint(item: subview, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: 0.0)
- return [leadingConstraint, trailingConstraint, topConstraint, bottomConstraint]
- }
-
+ let leadingConstraint = NSLayoutConstraint(item: subview, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1.0, constant: 0.0)
+ let trailingConstraint = NSLayoutConstraint(item: subview, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1.0, constant: 0.0)
+ let topConstraint = NSLayoutConstraint(item: subview, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 0.0)
+ let bottomConstraint = NSLayoutConstraint(item: subview, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: 0.0)
+ return [leadingConstraint, trailingConstraint, topConstraint, bottomConstraint]
}
}
diff --git a/Shared/Extensions/URL-Extensions.swift b/Shared/Extensions/URL-Extensions.swift
index f28ade9c1..7824ebec8 100644
--- a/Shared/Extensions/URL-Extensions.swift
+++ b/Shared/Extensions/URL-Extensions.swift
@@ -22,15 +22,17 @@ extension URL {
/// URL pointing to current app version release notes.
static var releaseNotes: URL {
- let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? ""
var gitHub = "https://github.com/Ranchero-Software/NetNewsWire/releases/tag/"
+
#if os(macOS)
- gitHub += "mac-\(String(describing: appVersion))"
- return URL(string: gitHub)!
+ gitHub += "mac-"
#else
- gitHub += "ios-\(String(describing: appVersion))"
- return URL(string: gitHub)!
+ gitHub += "ios-"
#endif
+
+ gitHub += "\(Bundle.main.versionNumber)-\(Bundle.main.buildNumber)"
+
+ return URL(string: gitHub)!
}
func valueFor(_ parameter: String) -> String? {
diff --git a/Shared/Importers/DefaultFeeds.opml b/Shared/Importers/DefaultFeeds.opml
index 7c1c6efac..f0581edaf 100644
--- a/Shared/Importers/DefaultFeeds.opml
+++ b/Shared/Importers/DefaultFeeds.opml
@@ -5,11 +5,12 @@
-
+
+
diff --git a/Shared/Resources/About.plist b/Shared/Resources/About.plist
new file mode 100644
index 000000000..d22e15c65
--- /dev/null
+++ b/Shared/Resources/About.plist
@@ -0,0 +1,112 @@
+
+
+
+
+ PrimaryContributors
+
+
+ name
+ Maurice Parker
+ url
+ https://vincode.io
+ role
+ Lead Developer
+
+
+ name
+ Stuart Breckenridge
+ url
+ https://stuartbreckenridge.net
+ role
+ Contributing Developer
+
+
+ url
+ https://twitter.com/BradEllis
+ name
+ Brad Ellis
+ role
+ App Icon
+
+
+ url
+ https://twitter.com/kielgillard
+ name
+ Kiel Gillard
+ role
+ Feedly Syncing
+
+
+ url
+ https://twitter.com/quanganhdo
+ name
+ Anh Do
+ role
+ NewsBlur Syncing
+
+
+ url
+ https://github.com/wevah
+ name
+ Nate Weaver
+ role
+ Under-the-hood magic and CSS stylin’s
+
+
+ url
+ https://github.com/brehaut/
+ name
+ Andrew Brehaut
+ role
+ Newsfoot Footnotes
+
+
+ url
+ https://nostodnayr.net
+ name
+ Ryan Dotson
+ role
+ Help Book
+
+
+ AdditionalContributors
+
+
+ name
+ Daniel Jalkut
+ url
+ https://github.com/danielpunkass
+
+
+ name
+ Joe Heck
+ url
+ https://rhonabwy.com
+
+
+ name
+ Olof Hellman
+ url
+ https://github.com/olofhellman
+
+
+ name
+ Rizwan Mohamed Ibrahim
+ url
+ https://blog.rizwan.dev/
+
+
+ name
+ Phil Viso
+ url
+ https://twitter.com/philviso
+
+
+ name
+ ...and many more
+ url
+ https://github.com/Ranchero-Software/NetNewsWire/graphs/contributors
+
+
+
+
diff --git a/Shared/Resources/Appanoose.nnwtheme/stylesheet.css b/Shared/Resources/Appanoose.nnwtheme/stylesheet.css
index 3cb33d027..0662fbd39 100644
--- a/Shared/Resources/Appanoose.nnwtheme/stylesheet.css
+++ b/Shared/Resources/Appanoose.nnwtheme/stylesheet.css
@@ -1,6 +1,8 @@
/* Shared iOS and macOS CSS rules. Platform specific rules are at the bottom of this file. */
body {
+ margin-left: auto;
+ margin-right: auto;
word-wrap: break-word;
max-width: 44em;
}
@@ -275,6 +277,10 @@ img, figure, video, div, object {
margin: 0 auto;
}
+video {
+ width: 100% !important;
+}
+
iframe {
max-width: 100%;
margin: 0 auto;
@@ -428,11 +434,10 @@ a.footnote:hover,
@supports (-webkit-touch-callout: none) {
body {
- margin-top: 3px;
- margin-bottom: 20px;
- margin-left: 20px;
- margin-right: 20px;
-
+ padding-top: 3px;
+ padding-bottom: 20px;
+ padding-left: 20px;
+ padding-right: 20px;
word-break: break-word;
-webkit-hyphens: auto;
-webkit-text-size-adjust: none;
@@ -482,10 +487,8 @@ a.footnote:hover,
@supports not (-webkit-touch-callout: none) {
body {
- margin-top: 20px;
- margin-bottom: 20px;
- margin-left: auto;
- margin-right: auto;
+ padding-top: 20px;
+ padding-bottom: 20px;
padding-left: 48px;
padding-right: 48px;
font-family: -apple-system;
diff --git a/Shared/Resources/Hyperlegible.nnwtheme/stylesheet.css b/Shared/Resources/Hyperlegible.nnwtheme/stylesheet.css
index c929ac40e..0e2c0c91a 100644
--- a/Shared/Resources/Hyperlegible.nnwtheme/stylesheet.css
+++ b/Shared/Resources/Hyperlegible.nnwtheme/stylesheet.css
@@ -224,6 +224,10 @@ img, figure, video, div, object {
margin: 0 auto;
}
+video {
+ width: 100% !important;
+}
+
iframe {
max-width: 100%;
margin: 0 auto;
diff --git a/Shared/Resources/NewsFax.nnwtheme/stylesheet.css b/Shared/Resources/NewsFax.nnwtheme/stylesheet.css
index 63260d030..f8ad9ea23 100644
--- a/Shared/Resources/NewsFax.nnwtheme/stylesheet.css
+++ b/Shared/Resources/NewsFax.nnwtheme/stylesheet.css
@@ -273,6 +273,10 @@ img, figure, video, div, object {
margin: 0 auto;
}
+video {
+ width: 100% !important;
+}
+
iframe {
max-width: 100%;
margin: 0 auto;
diff --git a/Shared/Resources/Promenade.nnwtheme/stylesheet.css b/Shared/Resources/Promenade.nnwtheme/stylesheet.css
index 8b78fa56d..b76e4a58b 100644
--- a/Shared/Resources/Promenade.nnwtheme/stylesheet.css
+++ b/Shared/Resources/Promenade.nnwtheme/stylesheet.css
@@ -246,6 +246,10 @@ img, figure, video, div, object {
margin: 0 auto;
}
+video {
+ width: 100% !important;
+}
+
iframe {
max-width: 100%;
margin: 0 auto;
diff --git a/Shared/Resources/Sepia.nnwtheme/stylesheet.css b/Shared/Resources/Sepia.nnwtheme/stylesheet.css
index b7d9a02dc..b0c81d1d0 100644
--- a/Shared/Resources/Sepia.nnwtheme/stylesheet.css
+++ b/Shared/Resources/Sepia.nnwtheme/stylesheet.css
@@ -241,6 +241,10 @@ img, figure, video, div, object {
margin: 0 auto;
}
+video {
+ width: 100% !important;
+}
+
iframe {
max-width: 100%;
margin: 0 auto;
diff --git a/Shared/Resources/Thanks.md b/Shared/Resources/Thanks.md
new file mode 100644
index 000000000..93b9df1f4
--- /dev/null
+++ b/Shared/Resources/Thanks.md
@@ -0,0 +1,5 @@
+Thanks to Sheila and my family; thanks to my friends in Seattle and around the globe; thanks to the ever-patient and ever-awesome NetNewsWire beta testers.
+
+Thanks to [Gus Mueller](https://shapeof.com/) for [FMDB](https://github.com/ccgus/fmdb) by [Flying Meat Software](http://flyingmeat.com/). Thanks to [GitHub](https://github.com) and [Slack](https://slack.com) for making open source collaboration easy and fun. Thanks to [Ben Ubois](https://benubois.com/) at [Feedbin](https://feedbin.com/) for all the extra help with syncing and article rendering — and for [hosting the server for the Reader view](https://feedbin.com/blog/2019/03/11/the-future-of-full-content/).
+
+NetNewsWire 6 is dedicated to everyone working to save democracy around the world.
\ No newline at end of file
diff --git a/Shared/Resources/notificationSoundBlip.mp3 b/Shared/Resources/notificationSoundBlip.mp3
new file mode 100644
index 000000000..f22ba4752
Binary files /dev/null and b/Shared/Resources/notificationSoundBlip.mp3 differ
diff --git a/Shared/UserNotifications/UserNotificationManager.swift b/Shared/UserNotifications/UserNotificationManager.swift
index 35523f595..2a20f693b 100644
--- a/Shared/UserNotifications/UserNotificationManager.swift
+++ b/Shared/UserNotifications/UserNotificationManager.swift
@@ -62,7 +62,7 @@ private extension UserNotificationManager {
}
content.body = ArticleStringFormatter.truncatedSummary(article)
content.threadIdentifier = webFeed.webFeedID
- content.sound = UNNotificationSound.default
+ content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: AppAssets.notificationSoundBlipFileName))
content.userInfo = [UserInfoKey.articlePath: article.pathUserInfo]
content.categoryIdentifier = "NEW_ARTICLE_NOTIFICATION_CATEGORY"
if let attachment = thumbnailAttachment(for: article, webFeed: webFeed) {
diff --git a/Shared/Widget/WidgetDataEncoder.swift b/Shared/Widget/WidgetDataEncoder.swift
index 918794f52..1b610a8b5 100644
--- a/Shared/Widget/WidgetDataEncoder.swift
+++ b/Shared/Widget/WidgetDataEncoder.swift
@@ -8,15 +8,16 @@
import Foundation
import WidgetKit
+import os.log
import UIKit
import RSCore
import Articles
import Account
-public final class WidgetDataEncoder: Logging {
-
+public final class WidgetDataEncoder {
+ private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application")
private let fetchLimit = 7
private lazy var appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String
@@ -24,111 +25,132 @@ public final class WidgetDataEncoder: Logging {
private lazy var imageContainer = containerURL?.appendingPathComponent("widgetImages", isDirectory: true)
private lazy var dataURL = containerURL?.appendingPathComponent("widget-data.json")
- private let encodeWidgetDataQueue = CoalescingQueue(name: "Encode the Widget Data", interval: 5.0)
-
+ public var isRunning = false
+
init () {
if imageContainer != nil {
try? FileManager.default.createDirectory(at: imageContainer!, withIntermediateDirectories: true, attributes: nil)
}
+ }
+
+ func encode() {
if #available(iOS 14, *) {
- NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil)
- }
- }
-
- func encodeIfNecessary() {
- encodeWidgetDataQueue.performCallsImmediately()
- }
+ isRunning = true
- @objc func statusesDidChange(_ note: Notification) {
- encodeWidgetDataQueue.add(self, #selector(performEncodeWidgetData))
- }
+ flushSharedContainer()
+ os_log(.debug, log: log, "Starting encoding widget data.")
- @objc private func performEncodeWidgetData() {
- // We will be on the Main Thread when the encodeIfNecessary function is called. We want
- // block the main thread in that case so that the widget data is encoded. If it is on
- // a background Thread, it was called by the CoalescingQueue. In that case we need to
- // move it to the Main Thread and want to execute it async.
- if Thread.isMainThread {
- encodeWidgetData()
- } else {
DispatchQueue.main.async {
- self.encodeWidgetData()
+ self.encodeWidgetData() { latestData in
+ guard let latestData = latestData else {
+ self.isRunning = false
+ return
+ }
+
+ let encodedData = try? JSONEncoder().encode(latestData)
+
+ os_log(.debug, log: self.log, "Finished encoding widget data.")
+
+ if self.fileExists() {
+ try? FileManager.default.removeItem(at: self.dataURL!)
+ os_log(.debug, log: self.log, "Removed widget data from container.")
+ }
+
+ if FileManager.default.createFile(atPath: self.dataURL!.path, contents: encodedData, attributes: nil) {
+ os_log(.debug, log: self.log, "Wrote widget data to container.")
+ WidgetCenter.shared.reloadAllTimelines()
+ }
+
+ self.isRunning = false
+ }
}
}
}
- private func encodeWidgetData() {
- flushSharedContainer()
- logger.debug("Starting encoding widget data.")
+ @available(iOS 14, *)
+ private func encodeWidgetData(completion: @escaping (WidgetData?) -> Void) {
+ var dispatchGroup = DispatchGroup()
+ var groupError: Error? = nil
- do {
- let unreadArticles = Array(try AccountManager.shared.fetchArticles(.unread(fetchLimit))).sortedByDate(.orderedDescending)
- let starredArticles = Array(try AccountManager.shared.fetchArticles(.starred(fetchLimit))).sortedByDate(.orderedDescending)
- let todayArticles = Array(try AccountManager.shared.fetchArticles(.today(fetchLimit))).sortedByDate(.orderedDescending)
-
- var unread = [LatestArticle]()
- var today = [LatestArticle]()
- var starred = [LatestArticle]()
-
- for article in unreadArticles {
- let latestArticle = LatestArticle(id: article.sortableArticleID,
- feedTitle: article.sortableName,
- articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article),
- articleSummary: article.summary,
- feedIconPath: writeImageDataToSharedContainer(article.iconImage()?.image.dataRepresentation()),
- pubDate: article.datePublished?.description ?? "")
- unread.append(latestArticle)
- }
-
- for article in starredArticles {
- let latestArticle = LatestArticle(id: article.sortableArticleID,
- feedTitle: article.sortableName,
- articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article),
- articleSummary: article.summary,
- feedIconPath: writeImageDataToSharedContainer(article.iconImage()?.image.dataRepresentation()),
- pubDate: article.datePublished?.description ?? "")
- starred.append(latestArticle)
- }
-
- for article in todayArticles {
- let latestArticle = LatestArticle(id: article.sortableArticleID,
- feedTitle: article.sortableName,
- articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article),
- articleSummary: article.summary,
- feedIconPath: writeImageDataToSharedContainer(article.iconImage()?.image.dataRepresentation()),
- pubDate: article.datePublished?.description ?? "")
- today.append(latestArticle)
- }
-
- let latestData = WidgetData(currentUnreadCount: SmartFeedsController.shared.unreadFeed.unreadCount,
- currentTodayCount: SmartFeedsController.shared.todayFeed.unreadCount,
- currentStarredCount: try AccountManager.shared.fetchCountForStarredArticles(),
- unreadArticles: unread,
- starredArticles: starred,
- todayArticles:today,
- lastUpdateTime: Date())
-
-
- DispatchQueue.global().async { [weak self] in
- guard let self = self else { return }
-
- let encodedData = try? JSONEncoder().encode(latestData)
-
- self.logger.debug("Finished encoding widget data.")
-
- if self.fileExists() {
- try? FileManager.default.removeItem(at: self.dataURL!)
- self.logger.debug("Removed widget data from container.")
+ var unread = [LatestArticle]()
+
+ dispatchGroup.enter()
+ AccountManager.shared.fetchArticlesAsync(.unread(fetchLimit)) { (articleSetResult) in
+ switch articleSetResult {
+ case .success(let articles):
+ for article in articles {
+ let latestArticle = LatestArticle(id: article.sortableArticleID,
+ feedTitle: article.sortableName,
+ articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article),
+ articleSummary: article.summary,
+ feedIconPath: self.writeImageDataToSharedContainer(article.iconImage()?.image.dataRepresentation()),
+ pubDate: article.datePublished?.description ?? "")
+ unread.append(latestArticle)
}
- if FileManager.default.createFile(atPath: self.dataURL!.path, contents: encodedData, attributes: nil) {
- self.logger.debug("Wrote widget data to container.")
- WidgetCenter.shared.reloadAllTimelines()
- }
-
+ case .failure(let databaseError):
+ groupError = databaseError
}
- } catch {
- logger.error("WidgetDataEncoder failed to write the widget data.")
+ dispatchGroup.leave()
}
+
+ var starred = [LatestArticle]()
+
+ dispatchGroup.enter()
+ AccountManager.shared.fetchArticlesAsync(.starred(fetchLimit)) { (articleSetResult) in
+ switch articleSetResult {
+ case .success(let articles):
+ for article in articles {
+ let latestArticle = LatestArticle(id: article.sortableArticleID,
+ feedTitle: article.sortableName,
+ articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article),
+ articleSummary: article.summary,
+ feedIconPath: self.writeImageDataToSharedContainer(article.iconImage()?.image.dataRepresentation()),
+ pubDate: article.datePublished?.description ?? "")
+ starred.append(latestArticle)
+ }
+ case .failure(let databaseError):
+ groupError = databaseError
+ }
+ dispatchGroup.leave()
+ }
+
+ var today = [LatestArticle]()
+
+ dispatchGroup.enter()
+ AccountManager.shared.fetchArticlesAsync(.today(fetchLimit)) { (articleSetResult) in
+ switch articleSetResult {
+ case .success(let articles):
+ for article in articles {
+ let latestArticle = LatestArticle(id: article.sortableArticleID,
+ feedTitle: article.sortableName,
+ articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article),
+ articleSummary: article.summary,
+ feedIconPath: self.writeImageDataToSharedContainer(article.iconImage()?.image.dataRepresentation()),
+ pubDate: article.datePublished?.description ?? "")
+ today.append(latestArticle)
+ }
+ case .failure(let databaseError):
+ groupError = databaseError
+ }
+ dispatchGroup.leave()
+ }
+
+ dispatchGroup.notify(queue: .main) {
+ if groupError != nil {
+ os_log(.error, log: self.log, "WidgetDataEncoder failed to write the widget data.")
+ completion(nil)
+ } else {
+ let latestData = WidgetData(currentUnreadCount: SmartFeedsController.shared.unreadFeed.unreadCount,
+ currentTodayCount: SmartFeedsController.shared.todayFeed.unreadCount,
+ currentStarredCount: (try? AccountManager.shared.fetchCountForStarredArticles()) ?? 0,
+ unreadArticles: unread,
+ starredArticles: starred,
+ todayArticles:today,
+ lastUpdateTime: Date())
+ completion(latestData)
+ }
+ }
+
}
private func fileExists() -> Bool {
diff --git a/SyncDatabase/Package.swift b/SyncDatabase/Package.swift
index 02fea65c1..25d563644 100644
--- a/SyncDatabase/Package.swift
+++ b/SyncDatabase/Package.swift
@@ -2,7 +2,7 @@
import PackageDescription
var dependencies: [Package.Dependency] = [
- .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "1.0.0")),
+ .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "1.1.0")),
.package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "1.0.0")),
]
diff --git a/Technotes/ReleaseNotes-Mac.markdown b/Technotes/ReleaseNotes-Mac.markdown
index 0abd4eb87..52ecf0762 100644
--- a/Technotes/ReleaseNotes-Mac.markdown
+++ b/Technotes/ReleaseNotes-Mac.markdown
@@ -1,5 +1,10 @@
# Mac Release Notes
+### 6.1.1b1 build 6107 3 Nov 2022
+
+Fixed a bug that could prevent users from accessing BazQux if an article was missing a field
+Fixed an issue that could prevent Feedly users from syncing if they tried to mark too many articles as read at the same time
+
### 6.1 build 6106 6 April 2022
Small cosmetic change — better alignment for items in General Preferences pane
diff --git a/Technotes/ReleaseNotes-iOS.markdown b/Technotes/ReleaseNotes-iOS.markdown
index a35e4b84e..fba9e6a22 100644
--- a/Technotes/ReleaseNotes-iOS.markdown
+++ b/Technotes/ReleaseNotes-iOS.markdown
@@ -1,6 +1,48 @@
# iOS Release Notes
-### 6.1 TestFlight build 6105 = 6 July 2022
+### 6.1 Release build 6110 - 9 Nov 2022
+
+Changes since 6.0.1…
+
+Article themes. Several themes ship with the app, and you can create your own. You can change the theme in Preferences.
+Fixed a bug that could prevent BazQux syncing when an article may not contain all the info we expect
+Fixed a bug that could prevent Feedly syncing when marking a large number of articles as read
+Disallow creation of iCloud account in the app if iCloud and iCloud Drive aren’t both enabled
+Added links to iCloud Syncing Limitations & Solutions on iCloud Account Management UI
+Copy URLs using repaired, rather than raw, feed links
+Fixed bug showing quote tweets that only included an image
+Video autoplay is now disallowed
+Article view now supports RTL layout
+Fixed a few crashing bugs
+Fixed a layout bug that could happen on returning to the Feeds list
+Fixed a bug where go-to-feed might not properly expand disclosure triangles
+Prevented the Delete option from showing in the Edit menu on the Article View
+Fixed Widget article icon lookup bug
+
+
+### 6.1 TestFlight build 6109 - 31 Oct 2022
+
+Enhanced Widget integration to make counts more accurate
+Enhanced Widget integration to make make it more efficient and save on battery life
+
+### 6.1 TestFlight build 6108 - 28 Oct 2022
+
+Fixed a bug that could prevent BazQux syncing when an article may not contain all the info we expect
+Fixed a bug that could prevent Feedly syncing when marking a large number of articles as read
+Prevent Widget integration from running while in the background to remove some crashes
+
+### 6.1 TestFlight build 6107 - 28 Sept 2022
+
+Added links to iCloud Syncing Limitations & Solutions on iCloud Account Management UI
+Prevented the Delete option from showing in the Edit menu on the Article View
+Greatly reduced the possibility of a background crash caused by Widget integration
+Fixed Widget article icon lookup bug
+
+### 6.1 TestFlight build 6106 - 9 July 2022
+
+Fix a bug where images wouldn’t appear in the widget
+
+### 6.1 TestFlight build 6105 - 6 July 2022
Write widget icons to the shared container
Make crashes slightly less likely when building up widget data
@@ -27,6 +69,14 @@ Fixed a bug where go-to-feed might not properly expand disclosure triangles
* Video autoplay is now disallowed.
* Article view now supports RTL layout.
+### 6.0.2 Release - 15 Oct 2021
+
+Makes a particular crash on startup, that happens only on iPad, far less likely.
+
+### 6.0.2 TestFlight build 610 - 25 Sep 2021
+
+Fixed bug with state restoration on launch (bug introduced in previous TestFlight build)
+
### 6.0.1 TestFlight build 608 - 28 Aug 2021
* Fixed our top crashing bug — it could happen when updating a table view
diff --git a/iOS/Account/CloudKitAccountViewController.swift b/iOS/Account/CloudKitAccountViewController.swift
index 657617c04..ca3ce71e9 100644
--- a/iOS/Account/CloudKitAccountViewController.swift
+++ b/iOS/Account/CloudKitAccountViewController.swift
@@ -14,7 +14,7 @@ enum CloudKitAccountViewControllerError: LocalizedError {
case iCloudDriveMissing
var errorDescription: String? {
- return NSLocalizedString("Unable to add iCloud Account. Please make sure you have iCloud and iCloud enabled in System Preferences.", comment: "Unable to add iCloud Account.")
+ return NSLocalizedString("Unable to add iCloud Account. Please make sure you have iCloud and iCloud Drive enabled in System Preferences.", comment: "Unable to add iCloud Account.")
}
}
diff --git a/iOS/Add/Add.storyboard b/iOS/Add/Add.storyboard
index d59b587df..576c27ceb 100644
--- a/iOS/Add/Add.storyboard
+++ b/iOS/Add/Add.storyboard
@@ -1,9 +1,9 @@
-
+
-
+
@@ -19,35 +19,35 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -61,10 +61,10 @@
-
+
-
+
@@ -74,7 +74,7 @@
-
+
@@ -112,7 +112,7 @@
-
+
@@ -144,7 +144,7 @@
-
+
@@ -164,7 +164,7 @@
-
+
@@ -199,7 +199,7 @@
-
+
@@ -256,7 +256,7 @@
-
+
@@ -278,14 +278,14 @@
-
+
-
+
-
+
@@ -303,10 +303,10 @@
-
+
-
+
@@ -316,7 +316,7 @@
-
+
@@ -333,14 +333,14 @@
-
+
-
+
-
+
@@ -388,7 +388,7 @@
-
+
diff --git a/iOS/Add/AddFeedViewController.swift b/iOS/Add/AddFeedViewController.swift
index f3eb6cad2..d82ffae07 100644
--- a/iOS/Add/AddFeedViewController.swift
+++ b/iOS/Add/AddFeedViewController.swift
@@ -54,10 +54,8 @@ class AddFeedViewController: UITableViewController {
activityIndicator.isHidden = true
activityIndicator.color = .label
- if initialFeed == nil, let urlString = UIPasteboard.general.string {
- if urlString.mayBeURL {
- initialFeed = urlString.normalizedURL
- }
+ if initialFeed == nil && UIPasteboard.general.hasURLs, let url = UIPasteboard.general.url {
+ initialFeed = url.absoluteString.normalizedURL
}
urlTextField.autocorrectionType = .no
diff --git a/iOS/AppAssets.swift b/iOS/AppAssets.swift
index 23e3a4d91..9c7faca3d 100644
--- a/iOS/AppAssets.swift
+++ b/iOS/AppAssets.swift
@@ -325,4 +325,9 @@ struct AppAssets {
}
}
+ static var notificationSoundBlipFileName: String = {
+ // https://freesound.org/people/cabled_mess/sounds/350862/
+ return "notificationSoundBlip.mp3"
+ }()
+
}
diff --git a/iOS/AppDefaults.swift b/iOS/AppDefaults.swift
index 4c6ec9d75..10ed0af43 100644
--- a/iOS/AppDefaults.swift
+++ b/iOS/AppDefaults.swift
@@ -53,7 +53,6 @@ final class AppDefaults {
static let articleFullscreenEnabled = "articleFullscreenEnabled"
static let hasUsedFullScreenPreviously = "hasUsedFullScreenPreviously"
static let confirmMarkAllAsRead = "confirmMarkAllAsRead"
- static let lastRefresh = "lastRefresh"
static let addWebFeedAccountID = "addWebFeedAccountID"
static let addWebFeedFolderName = "addWebFeedFolderName"
static let addFolderAccountID = "addFolderAccountID"
@@ -196,15 +195,6 @@ final class AppDefaults {
}
}
- var lastRefresh: Date? {
- get {
- return AppDefaults.date(for: Key.lastRefresh)
- }
- set {
- AppDefaults.setDate(for: Key.lastRefresh, newValue)
- }
- }
-
var timelineNumberOfLines: Int {
get {
return AppDefaults.int(for: Key.timelineNumberOfLines)
diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift
index 80048543c..bf145a3b0 100644
--- a/iOS/AppDelegate.swift
+++ b/iOS/AppDelegate.swift
@@ -10,6 +10,7 @@ import UIKit
import RSCore
import RSWeb
import Account
+import Articles
import BackgroundTasks
import Secrets
import WidgetKit
@@ -73,7 +74,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
FeedProviderManager.shared.delegate = ExtensionPointManager.shared
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
- NotificationCenter.default.addObserver(self, selector: #selector(accountRefreshDidFinish(_:)), name: .AccountRefreshDidFinish, object: nil)
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
@@ -150,10 +150,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
}
}
- @objc func accountRefreshDidFinish(_ note: Notification) {
- AppDefaults.shared.lastRefresh = Date()
- }
-
// MARK: - API
func manualRefresh(errorHandler: @escaping (Error) -> ()) {
@@ -172,10 +168,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
func prepareAccountsForBackground() {
extensionFeedAddRequestFile.suspend()
- widgetDataEncoder.encodeIfNecessary()
syncTimer?.invalidate()
scheduleBackgroundFeedRefresh()
syncArticleStatus()
+ widgetDataEncoder.encode()
waitForSyncTasksToFinish()
}
@@ -183,7 +179,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
extensionFeedAddRequestFile.resume()
syncTimer?.update()
- if let lastRefresh = AppDefaults.shared.lastRefresh {
+ if let lastRefresh = AccountManager.shared.lastArticleFetchEndTime {
if Date() > lastRefresh.addingTimeInterval(15 * 60) {
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log)
} else {
@@ -219,6 +215,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
}
+ func presentThemeImportError(_ error: Error) {
+ let windowScene = {
+ let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
+ return scenes.filter { $0.activationState == .foregroundActive }.first ?? scenes.first
+ }()
+ guard let sceneDelegate = windowScene?.delegate as? SceneDelegate else { return }
+ sceneDelegate.presentError(error)
+ }
+
}
// MARK: App Initialization
@@ -294,7 +299,7 @@ private extension AppDelegate {
return
}
- if AccountManager.shared.refreshInProgress || isSyncArticleStatusRunning {
+ if AccountManager.shared.refreshInProgress || isSyncArticleStatusRunning || widgetDataEncoder.isRunning {
logger.info("Waiting for sync to finish...")
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
self?.waitToComplete(completion: completion)
@@ -423,55 +428,40 @@ private extension AppDelegate {
private extension AppDelegate {
func handleMarkAsRead(userInfo: [AnyHashable: Any]) {
- guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any],
- let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
- let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else {
- return
- }
- resumeDatabaseProcessingIfNecessary()
- let account = AccountManager.shared.existingAccount(with: accountID)
- guard account != nil else {
- logger.debug("No account found from notification.")
- return
- }
- let article = try? account!.fetchArticles(.articleIDs([articleID]))
- guard article != nil else {
- logger.debug("No account found from search using \(articleID, privacy: .public)")
- return
- }
- account!.markArticles(article!, statusKey: .read, flag: true) { _ in }
- self.prepareAccountsForBackground()
- account!.syncArticleStatus(completion: { [weak self] _ in
- if !AccountManager.shared.isSuspended {
- self?.prepareAccountsForBackground()
- self?.suspendApplication()
- }
- })
+ markArticle(userInfo: userInfo, statusKey: .read)
}
func handleMarkAsStarred(userInfo: [AnyHashable: Any]) {
+ markArticle(userInfo: userInfo, statusKey: .starred)
+ }
+
+ func markArticle(userInfo: [AnyHashable: Any], statusKey: ArticleStatus.Key) {
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any],
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else {
return
}
+
resumeDatabaseProcessingIfNecessary()
- let account = AccountManager.shared.existingAccount(with: accountID)
- guard account != nil else {
+
+ guard let account = AccountManager.shared.existingAccount(with: accountID) else {
logger.debug("No account found from notification.")
return
}
- let article = try? account!.fetchArticles(.articleIDs([articleID]))
- guard article != nil else {
+
+ guard let articles = try? account.fetchArticles(.articleIDs([articleID])), !articles.isEmpty else {
logger.debug("No article found from search using \(articleID, privacy: .public)")
return
}
- account!.markArticles(article!, statusKey: .starred, flag: true) { _ in }
- account!.syncArticleStatus(completion: { [weak self] _ in
- if !AccountManager.shared.isSuspended {
- self?.prepareAccountsForBackground()
- self?.suspendApplication()
- }
- })
+
+ account.mark(articles: articles, statusKey: statusKey, flag: true) { [weak self] _ in
+ account.syncArticleStatus(completion: { [weak self] _ in
+ if !AccountManager.shared.isSuspended {
+ self?.prepareAccountsForBackground()
+ self?.suspendApplication()
+ }
+ })
+ }
}
+
}
diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift
index 12ce2aa90..b76ece0ba 100644
--- a/iOS/Article/ArticleViewController.swift
+++ b/iOS/Article/ArticleViewController.swift
@@ -11,8 +11,9 @@ import WebKit
import Account
import Articles
import SafariServices
+import RSCore
-class ArticleViewController: UIViewController, MainControllerIdentifiable {
+class ArticleViewController: UIViewController, MainControllerIdentifiable, Logging {
typealias State = (extractedArticle: ExtractedArticle?,
isShowingExtractedArticle: Bool,
@@ -87,6 +88,8 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable {
return keyboardManager.keyCommands
}
+ private var lastKnownDisplayMode: UISplitViewController.DisplayMode?
+
var currentUnreadCount: Int = 0 {
didSet {
updateUnreadCountIndicator()
@@ -102,7 +105,6 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable {
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(reloadDueToThemeChange(_:)), name: .CurrentArticleThemeDidChangeNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(configureAppearanceMenu(_:)), name: .ArticleThemeNamesDidChangeNotification, object: nil)
- NotificationCenter.default.addObserver(self, selector: #selector(updateUnreadCountIndicator(_:)), name: UIDevice.orientationDidChangeNotification, object: nil)
articleExtractorButton.addTarget(self, action: #selector(toggleArticleExtractor(_:)), for: .touchUpInside)
toolbarItems?.insert(UIBarButtonItem(customView: articleExtractorButton), at: 6)
@@ -258,7 +260,7 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable {
identifier: nil,
discoverabilityTitle: nil,
attributes: [],
- state: ArticleThemesManager.shared.currentThemeName == themeName ? .on : .off,
+ state: ArticleThemesManager.shared.currentTheme.name == themeName ? .on : .off,
handler: { action in
ArticleThemesManager.shared.currentThemeName = themeName
})
@@ -270,7 +272,7 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable {
identifier: nil,
discoverabilityTitle: nil,
attributes: [],
- state: ArticleThemesManager.shared.currentThemeName == AppDefaults.defaultThemeName ? .on : .off,
+ state: ArticleThemesManager.shared.currentTheme.name == AppDefaults.defaultThemeName ? .on : .off,
handler: { _ in
ArticleThemesManager.shared.currentThemeName = AppDefaults.defaultThemeName
})
@@ -343,42 +345,6 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable {
configureAppearanceMenu()
}
-
- /// Updates the indicator count in the navigation bar.
- /// For iPhone, this indicator is visible if the unread count is > 0.
- /// For iPad, this indicator is visible if it is in `portrait` or `unknown`
- /// orientation, **and** the unread count is > 0.
- /// - Parameter sender: `Any` (optional)
- @objc
- public func updateUnreadCountIndicator(_ sender: Any? = nil) {
- if UIDevice.current.userInterfaceIdiom == .phone {
- if currentUnreadCount > 0 {
- let unreadCountView = MasterTimelineUnreadCountView(frame: .zero)
- unreadCountView.unreadCount = currentUnreadCount
- unreadCountView.setFrameIfNotEqual(CGRect(x: 0, y: 0, width: unreadCountView.intrinsicContentSize.width, height: unreadCountView.intrinsicContentSize.height))
- navigationItem.leftBarButtonItem = UIBarButtonItem(customView: unreadCountView)
- } else {
- navigationItem.leftBarButtonItem = nil
- }
- } else {
-
- if UIDevice.current.orientation.isPortrait || !UIDevice.current.orientation.isValidInterfaceOrientation {
- if currentUnreadCount > 0 {
- let unreadCountView = MasterTimelineUnreadCountView(frame: .zero)
- unreadCountView.unreadCount = currentUnreadCount
- unreadCountView.setFrameIfNotEqual(CGRect(x: 0, y: 0, width: unreadCountView.intrinsicContentSize.width, height: unreadCountView.intrinsicContentSize.height))
- navigationItem.leftBarButtonItem = UIBarButtonItem(customView: unreadCountView)
- } else {
- navigationItem.leftBarButtonItem = nil
- }
- } else {
- navigationItem.leftBarButtonItem = nil
- }
- }
- }
-
-
-
// MARK: Notifications
@objc dynamic func unreadCountDidChange(_ notification: Notification) {
@@ -486,6 +452,12 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable {
func setScrollPosition(isShowingExtractedArticle: Bool, articleWindowScrollY: Int) {
currentWebViewController?.setScrollPosition(isShowingExtractedArticle: isShowingExtractedArticle, articleWindowScrollY: articleWindowScrollY)
}
+
+ public func splitViewControllerWillChangeTo(displayMode: UISplitViewController.DisplayMode) {
+ lastKnownDisplayMode = displayMode
+ updateUnreadCountIndicator()
+ }
+
}
// MARK: Find in Article
@@ -644,4 +616,15 @@ private extension ArticleViewController {
return controller
}
+ func updateUnreadCountIndicator() {
+ if currentUnreadCount > 0 && (traitCollection.userInterfaceIdiom == .phone || lastKnownDisplayMode == .secondaryOnly) {
+ let unreadCountView = MasterTimelineUnreadCountView(frame: .zero)
+ unreadCountView.unreadCount = currentUnreadCount
+ unreadCountView.setFrameIfNotEqual(CGRect(x: 0, y: 0, width: unreadCountView.intrinsicContentSize.width, height: unreadCountView.intrinsicContentSize.height))
+ navigationItem.leftBarButtonItem = UIBarButtonItem(customView: unreadCountView)
+ } else {
+ navigationItem.leftBarButtonItem = nil
+ }
+ }
+
}
diff --git a/iOS/Article/OpenInSafariActivity.swift b/iOS/Article/OpenInSafariActivity.swift
index 2c9ae9eab..05738081a 100644
--- a/iOS/Article/OpenInSafariActivity.swift
+++ b/iOS/Article/OpenInSafariActivity.swift
@@ -17,7 +17,7 @@ class OpenInBrowserActivity: UIActivity {
}
override var activityImage: UIImage? {
- return UIImage(systemName: "globe", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular))
+ return AppAssets.safariImage
}
override var activityType: UIActivity.ActivityType? {
diff --git a/iOS/Article/WebViewController.swift b/iOS/Article/WebViewController.swift
index bba8f7432..8215abbc1 100644
--- a/iOS/Article/WebViewController.swift
+++ b/iOS/Article/WebViewController.swift
@@ -386,12 +386,8 @@ extension WebViewController: WKNavigationDelegate {
} else if components?.scheme == "mailto" {
decisionHandler(.cancel)
- guard let emailAddress = url.percentEncodedEmailAddress else {
- return
- }
-
- if UIApplication.shared.canOpenURL(emailAddress) {
- UIApplication.shared.open(emailAddress, options: [.universalLinksOnly : false], completionHandler: nil)
+ if UIApplication.shared.canOpenURL(url) {
+ UIApplication.shared.open(url, options: [.universalLinksOnly : false], completionHandler: nil)
} else {
let alert = UIAlertController(title: NSLocalizedString("Error", comment: "Error"), message: NSLocalizedString("This device cannot send emails.", comment: "This device cannot send emails."), preferredStyle: .alert)
alert.addAction(.init(title: NSLocalizedString("Dismiss", comment: "Dismiss"), style: .cancel, handler: nil))
diff --git a/iOS/MasterFeed/Cell/MasterFeedTableViewCell.swift b/iOS/MasterFeed/Cell/MasterFeedTableViewCell.swift
index ac55c60dc..4b0004c1f 100644
--- a/iOS/MasterFeed/Cell/MasterFeedTableViewCell.swift
+++ b/iOS/MasterFeed/Cell/MasterFeedTableViewCell.swift
@@ -128,14 +128,17 @@ class MasterFeedTableViewCell : VibrantTableViewCell {
self.disclosureButton?.accessibilityLabel = NSLocalizedString("Collapse Folder", comment: "Collapse Folder")
self.disclosureButton?.imageView?.transform = CGAffineTransform(rotationAngle: 1.570796)
} else {
- self.disclosureButton?.accessibilityLabel = NSLocalizedString("Expand Folder", comment: "Expand Folder")
+ self.disclosureButton?.accessibilityLabel = NSLocalizedString("Expand Folder", comment: "Expand Folder")
self.disclosureButton?.imageView?.transform = CGAffineTransform(rotationAngle: 0)
}
}
}
- override func applyThemeProperties() {
- super.applyThemeProperties()
+ override func updateConfiguration(using state: UICellConfigurationState) {
+ backgroundConfiguration = UIBackgroundConfiguration.listSidebarCell().updated(for: state)
+ if state.isSelected {
+ backgroundConfiguration?.backgroundColor = AppAssets.secondaryAccentColor
+ }
}
override func willTransition(to state: UITableViewCell.StateMask) {
@@ -171,8 +174,10 @@ class MasterFeedTableViewCell : VibrantTableViewCell {
let iconTintColor: UIColor
if isHighlighted || isSelected {
+ disclosureButton?.tintColor = AppAssets.vibrantTextColor
iconTintColor = AppAssets.vibrantTextColor
} else {
+ disclosureButton?.tintColor = AppAssets.secondaryAccentColor
if let preferredColor = iconImage?.preferredColor {
iconTintColor = UIColor(cgColor: preferredColor)
} else {
diff --git a/iOS/MasterFeed/MasterFeedViewController+Drop.swift b/iOS/MasterFeed/MasterFeedViewController+Drop.swift
index c7f925c07..184515d64 100644
--- a/iOS/MasterFeed/MasterFeedViewController+Drop.swift
+++ b/iOS/MasterFeed/MasterFeedViewController+Drop.swift
@@ -18,33 +18,63 @@ extension MasterFeedViewController: UITableViewDropDelegate {
}
func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
- guard let destIndexPath = destinationIndexPath, destIndexPath.section > 0, tableView.hasActiveDrag else {
+ guard tableView.hasActiveDrag else {
+ return UITableViewDropProposal(operation: .forbidden)
+ }
+
+ guard let sourceNode = session.localDragSession?.items.first?.localObject as? Node,
+ let sourceWebFeed = sourceNode.representedObject as? WebFeed else {
return UITableViewDropProposal(operation: .forbidden)
}
-
- guard let destFeed = coordinator.nodeFor(destIndexPath)?.representedObject as? Feed,
- let destAccount = destFeed.account,
- let destCell = tableView.cellForRow(at: destIndexPath) else {
- return UITableViewDropProposal(operation: .forbidden)
- }
+ var successOperation = UIDropOperation.move
+
+ if let destinationIndexPath = destinationIndexPath,
+ let sourceIndexPath = coordinator.indexPathFor(sourceNode),
+ destinationIndexPath.section != sourceIndexPath.section {
+ successOperation = .copy
+ }
+
+ guard let correctedIndexPath = correctDestinationIndexPath(session: session) else {
+ // We didn't hit the corrected indexPath, but this at least it gets the section right
+ guard let section = destinationIndexPath?.section,
+ let account = coordinator.nodeFor(section)?.representedObject as? Account,
+ !account.hasChildWebFeed(withURL: sourceWebFeed.url) else {
+ return UITableViewDropProposal(operation: .forbidden)
+ }
+
+ return UITableViewDropProposal(operation: successOperation, intent: .insertAtDestinationIndexPath)
+ }
+
+ guard correctedIndexPath.section > 0 else {
+ return UITableViewDropProposal(operation: .forbidden)
+ }
+
+ guard let correctDestNode = coordinator.nodeFor(correctedIndexPath),
+ let correctDestFeed = correctDestNode.representedObject as? Feed,
+ let correctDestAccount = correctDestFeed.account else {
+ return UITableViewDropProposal(operation: .forbidden)
+ }
+
// Validate account specific behaviors...
- if destAccount.behaviors.contains(.disallowFeedInMultipleFolders),
- let sourceNode = session.localDragSession?.items.first?.localObject as? Node,
- let sourceWebFeed = sourceNode.representedObject as? WebFeed,
- sourceWebFeed.account?.accountID != destAccount.accountID && destAccount.hasWebFeed(withURL: sourceWebFeed.url) {
+ if correctDestAccount.behaviors.contains(.disallowFeedInMultipleFolders),
+ sourceWebFeed.account?.accountID != correctDestAccount.accountID && correctDestAccount.hasWebFeed(withURL: sourceWebFeed.url) {
return UITableViewDropProposal(operation: .forbidden)
}
// Determine the correct drop proposal
- if destFeed is Folder {
- if session.location(in: destCell).y >= 0 {
- return UITableViewDropProposal(operation: .move, intent: .insertIntoDestinationIndexPath)
+ if let correctFolder = correctDestFeed as? Folder {
+ if correctFolder.hasChildWebFeed(withURL: sourceWebFeed.url) {
+ return UITableViewDropProposal(operation: .forbidden)
} else {
- return UITableViewDropProposal(operation: .move, intent: .unspecified)
+ return UITableViewDropProposal(operation: successOperation, intent: .insertIntoDestinationIndexPath)
}
} else {
- return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
+ if let parentContainer = correctDestNode.parent?.representedObject as? Container, !parentContainer.hasChildWebFeed(withURL: sourceWebFeed.url) {
+ return UITableViewDropProposal(operation: successOperation, intent: .insertAtDestinationIndexPath)
+ } else {
+ return UITableViewDropProposal(operation: .forbidden)
+ }
}
}
@@ -52,33 +82,23 @@ extension MasterFeedViewController: UITableViewDropDelegate {
func tableView(_ tableView: UITableView, performDropWith dropCoordinator: UITableViewDropCoordinator) {
guard let dragItem = dropCoordinator.items.first?.dragItem,
let dragNode = dragItem.localObject as? Node,
- let source = dragNode.parent?.representedObject as? Container,
- let destIndexPath = dropCoordinator.destinationIndexPath else {
- return
- }
-
- let isFolderDrop: Bool = {
- if coordinator.nodeFor(destIndexPath)?.representedObject is Folder, let propCell = tableView.cellForRow(at: destIndexPath) {
- return dropCoordinator.session.location(in: propCell).y >= 0
- }
- return false
- }()
+ let source = dragNode.parent?.representedObject as? Container else {
+ return
+ }
// Based on the drop we have to determine a node to start looking for a parent container.
let destNode: Node? = {
+ guard let destIndexPath = correctDestinationIndexPath(session: dropCoordinator.session) else { return nil }
- if isFolderDrop {
- return coordinator.nodeFor(destIndexPath)
- } else {
- if destIndexPath.row == 0 {
- return coordinator.nodeFor(IndexPath(row: 0, section: destIndexPath.section))
- } else if destIndexPath.row > 0 {
- return coordinator.nodeFor(IndexPath(row: destIndexPath.row - 1, section: destIndexPath.section))
+ if coordinator.nodeFor(destIndexPath)?.representedObject is Folder {
+ if dropCoordinator.proposal.intent == .insertAtDestinationIndexPath {
+ return coordinator.nodeFor(destIndexPath.section)
} else {
- return nil
+ return coordinator.nodeFor(destIndexPath)
}
+ } else {
+ return nil
}
-
}()
// Now we start looking for the parent container
@@ -86,8 +106,11 @@ extension MasterFeedViewController: UITableViewDropDelegate {
if let container = (destNode?.representedObject as? Container) ?? (destNode?.parent?.representedObject as? Container) {
return container
} else {
+ // We didn't hit the corrected indexPath, but this at least gets the section right
+ guard let section = dropCoordinator.destinationIndexPath?.section 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
+ return coordinator.nodeFor(section)?.representedObject as? Account
}
}()
@@ -96,10 +119,25 @@ extension MasterFeedViewController: UITableViewDropDelegate {
if source.account == destination.account {
moveWebFeedInAccount(feed: webFeed, sourceContainer: source, destinationContainer: destination)
} else {
- moveWebFeedBetweenAccounts(feed: webFeed, sourceContainer: source, destinationContainer: destination)
+ copyWebFeedBetweenAccounts(feed: webFeed, sourceContainer: source, destinationContainer: destination)
}
}
+
+}
+private extension MasterFeedViewController {
+
+ func correctDestinationIndexPath(session: UIDropSession) -> IndexPath? {
+ let location = session.location(in: tableView)
+
+ var correctDestination: IndexPath?
+ tableView.performUsingPresentationValues {
+ correctDestination = tableView.indexPathForRow(at: location)
+ }
+
+ return correctDestination
+ }
+
func moveWebFeedInAccount(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) {
guard sourceContainer !== destinationContainer else { return }
@@ -115,7 +153,7 @@ extension MasterFeedViewController: UITableViewDropDelegate {
}
}
- func moveWebFeedBetweenAccounts(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) {
+ func copyWebFeedBetweenAccounts(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) {
if let existingFeed = destinationContainer.account?.existingWebFeed(withURL: feed.url) {
@@ -123,15 +161,7 @@ extension MasterFeedViewController: UITableViewDropDelegate {
destinationContainer.account?.addWebFeed(existingFeed, to: destinationContainer) { result in
switch result {
case .success:
- sourceContainer.account?.removeWebFeed(feed, from: sourceContainer) { result in
- BatchUpdate.shared.end()
- switch result {
- case .success:
- break
- case .failure(let error):
- self.presentError(error)
- }
- }
+ BatchUpdate.shared.end()
case .failure(let error):
BatchUpdate.shared.end()
self.presentError(error)
@@ -144,15 +174,7 @@ extension MasterFeedViewController: UITableViewDropDelegate {
destinationContainer.account?.createWebFeed(url: feed.url, name: feed.editedName, container: destinationContainer, validateFeed: false) { result in
switch result {
case .success:
- sourceContainer.account?.removeWebFeed(feed, from: sourceContainer) { result in
- BatchUpdate.shared.end()
- switch result {
- case .success:
- break
- case .failure(let error):
- self.presentError(error)
- }
- }
+ BatchUpdate.shared.end()
case .failure(let error):
BatchUpdate.shared.end()
self.presentError(error)
@@ -164,3 +186,11 @@ extension MasterFeedViewController: UITableViewDropDelegate {
}
+
+private extension Container {
+
+ func hasChildWebFeed(withURL url: String) -> Bool {
+ return topLevelWebFeeds.contains(where: { $0.url == url })
+ }
+
+}
diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift
index 8af457743..f41ade15e 100644
--- a/iOS/MasterFeed/MasterFeedViewController.swift
+++ b/iOS/MasterFeed/MasterFeedViewController.swift
@@ -7,6 +7,7 @@
//
import UIKit
+import SwiftUI
import Account
import Articles
import RSCore
@@ -16,13 +17,15 @@ import SafariServices
class MasterFeedViewController: UITableViewController, UndoableCommandRunner, MainControllerIdentifiable {
@IBOutlet weak var filterButton: UIBarButtonItem!
- private var refreshProgressView: RefreshProgressView?
@IBOutlet weak var addNewItemButton: UIBarButtonItem! {
didSet {
addNewItemButton.primaryAction = nil
}
}
+ let refreshProgressModel = RefreshProgressModel()
+ lazy var progressBarViewController = UIHostingController(rootView: RefreshProgressView(progressBarMode: refreshProgressModel))
+
var mainControllerIdentifer = MainControllerIdentifier.masterFeed
weak var coordinator: SceneCoordinator!
@@ -71,11 +74,17 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner, Ma
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(configureContextMenu(_:)), name: .ActiveExtensionPointsDidChange, object: nil)
+ NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
refreshControl = UIRefreshControl()
refreshControl!.addTarget(self, action: #selector(refreshAccounts(_:)), for: .valueChanged)
+ refreshControl!.tintColor = .clear
+
+ progressBarViewController.view.backgroundColor = .clear
+ progressBarViewController.view.translatesAutoresizingMaskIntoConstraints = false
+ let refreshProgressItemButton = UIBarButtonItem(customView: progressBarViewController.view)
+ toolbarItems?.insert(refreshProgressItemButton, at: 2)
- configureToolbar()
becomeFirstResponder()
}
@@ -139,6 +148,13 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner, Ma
}
}
+ @objc func displayNameDidChange(_ note: Notification) {
+ guard let object = note.object as? AnyObject else {
+ return
+ }
+ reloadCell(for: object)
+ }
+
@objc func contentSizeCategoryDidChange(_ note: Notification) {
resetEstimatedRowHeight()
tableView.reloadData()
@@ -515,7 +531,9 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner, Ma
func updateFeedSelection(animations: Animations) {
if let indexPath = coordinator.currentFeedIndexPath {
- tableView.selectRowAndScrollIfNotVisible(at: indexPath, animations: animations)
+ if indexPath != tableView.indexPathForSelectedRow {
+ tableView.selectRowAndScrollIfNotVisible(at: indexPath, animations: animations)
+ }
} else {
if let indexPath = tableView.indexPathForSelectedRow {
if animations.contains(.select) {
@@ -587,7 +605,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner, Ma
} else {
setFilterButtonToInactive()
}
- refreshProgressView?.update()
+ refreshProgressModel.update()
addNewItemButton?.isEnabled = !AccountManager.shared.activeAccounts.isEmpty
configureContextMenu()
@@ -720,16 +738,6 @@ extension MasterFeedViewController: MasterFeedTableViewCellDelegate {
private extension MasterFeedViewController {
- func configureToolbar() {
- guard let refreshProgressView = Bundle.main.loadNibNamed("RefreshProgressView", owner: self, options: nil)?[0] as? RefreshProgressView else {
- return
- }
-
- self.refreshProgressView = refreshProgressView
- let refreshProgressItemButton = UIBarButtonItem(customView: refreshProgressView)
- toolbarItems?.insert(refreshProgressItemButton, at: 2)
- }
-
func setFilterButtonToActive() {
filterButton?.image = AppAssets.filterActiveImage
filterButton?.accLabelText = NSLocalizedString("Selected - Filter Read Feeds", comment: "Selected - Filter Read Feeds")
@@ -831,6 +839,12 @@ private extension MasterFeedViewController {
completion(cell as! MasterFeedTableViewCell, indexPath)
}
}
+
+ private func reloadCell(for object: AnyObject) {
+ guard let indexPath = coordinator.indexPathFor(object) else { return }
+ tableView.reloadRows(at: [indexPath], with: .none)
+ restoreSelectionIfNecessary(adjustScroll: false)
+ }
private func reloadAllVisibleCells(completion: (() -> Void)? = nil) {
guard let indexPaths = tableView.indexPathsForVisibleRows else { return }
@@ -1050,8 +1064,7 @@ private extension MasterFeedViewController {
return nil
}
- let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
- let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, webFeed.nameForDisplay) as String
+ let title = NSLocalizedString("Mark All as Read", comment: "Command")
let cancel = {
completion(true)
}
@@ -1131,8 +1144,7 @@ private extension MasterFeedViewController {
return nil
}
- let localizedMenuText = NSLocalizedString("Mark All as Read", comment: "Command")
- let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String
+ let title = NSLocalizedString("Mark All as Read", comment: "Command")
let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
if let articles = try? feed.fetchUnreadArticles() {
@@ -1233,8 +1245,7 @@ private extension MasterFeedViewController {
return nil
}
- let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
- let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, account.nameForDisplay) as String
+ let title = NSLocalizedString("Mark All as Read", comment: "Command")
let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
// If you don't have this delay the screen flashes when it executes this code
diff --git a/iOS/MasterFeed/RefreshProgressView.swift b/iOS/MasterFeed/RefreshProgressView.swift
index 6c20ce95d..5ecd13e1e 100644
--- a/iOS/MasterFeed/RefreshProgressView.swift
+++ b/iOS/MasterFeed/RefreshProgressView.swift
@@ -1,27 +1,82 @@
//
-// RefeshProgressView.swift
-// NetNewsWire-iOS
+// ProgressBarView.swift
+// NetNewsWire
//
-// Created by Maurice Parker on 10/24/19.
-// Copyright © 2019 Ranchero Software. All rights reserved.
+// Created by Maurice Parker on 11/11/22.
+// Copyright © 2022 Ranchero Software. All rights reserved.
//
+// IndetermineProgressView inspired by https://daringsnowball.net/articles/indeterminate-linear-progress-view/
-import UIKit
+import SwiftUI
import Account
-class RefreshProgressView: UIView {
+struct RefreshProgressView: View {
- @IBOutlet weak var progressView: UIProgressView!
- @IBOutlet weak var label: UILabel!
+ static let width: CGFloat = 100
+ static let height: CGFloat = 5
- override func awakeFromNib() {
- NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil)
- NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange(_:)), name: UIContentSizeCategory.didChangeNotification, object: nil)
- update()
- scheduleUpdateRefreshLabel()
+ @ObservedObject var refreshProgressModel: RefreshProgressModel
+ @State private var offset: CGFloat = 0
- isAccessibilityElement = true
- accessibilityTraits = [.updatesFrequently, .notEnabled]
+ init(progressBarMode: RefreshProgressModel) {
+ self.refreshProgressModel = progressBarMode
+ }
+
+ var body: some View {
+ ZStack {
+ if refreshProgressModel.isRefreshing {
+ if refreshProgressModel.isIndeterminate {
+ indeterminateProgressView
+ } else {
+ ProgressView(value: refreshProgressModel.progress)
+ .progressViewStyle(LinearProgressViewStyle())
+ .frame(width: Self.width, height: Self.height)
+ }
+ } else {
+ Text(refreshProgressModel.label)
+ .accessibilityLabel(refreshProgressModel.label)
+ .font(.footnote)
+ .foregroundColor(.secondary)
+ }
+ }
+ .frame(width: 200, height: 44)
+ }
+
+ var indeterminateProgressView: some View {
+ Rectangle()
+ .foregroundColor(.gray.opacity(0.15))
+ .overlay(
+ Rectangle()
+ .foregroundColor(Color.accentColor)
+ .frame(width: Self.width * 0.26, height: Self.height)
+ .clipShape(Capsule())
+ .offset(x: -Self.width * 0.6, y: 0)
+ .offset(x: Self.width * 1.2 * self.offset, y: 0)
+ .animation(.default.repeatForever().speed(0.265), value: self.offset)
+ .onAppear {
+ withAnimation {
+ self.offset = 1
+ }
+ }
+ .onDisappear {
+ self.offset = 0
+ }
+ )
+ .clipShape(Capsule())
+ .frame(width: Self.width, height: Self.height)
+ }
+
+}
+
+class RefreshProgressModel: ObservableObject {
+
+ @Published var isRefreshing = false
+ @Published var isIndeterminate = false
+ @Published var progress = 0.0
+ @Published var label = String()
+
+ init() {
+ NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil)
}
func update() {
@@ -31,52 +86,32 @@ class RefreshProgressView: UIView {
updateRefreshLabel()
}
}
-
- override func didMoveToSuperview() {
- progressChanged(animated: false)
- }
-
+
@objc func progressDidChange(_ note: Notification) {
progressChanged(animated: true)
}
-
- @objc func contentSizeCategoryDidChange(_ note: Notification) {
- // This hack is probably necessary because custom views in the toolbar don't get
- // notifications that the content size changed.
- label.font = UIFont.preferredFont(forTextStyle: .footnote)
- }
-
+
deinit {
NotificationCenter.default.removeObserver(self)
}
}
-// MARK: Private
-
-private extension RefreshProgressView {
-
+private extension RefreshProgressModel {
+
func progressChanged(animated: Bool) {
- // Layout may crash if not in the view hierarchy.
- // https://github.com/Ranchero-Software/NetNewsWire/issues/1764
- let isInViewHierarchy = self.superview != nil
-
- let progress = AccountManager.shared.combinedRefreshProgress
-
- if progress.isComplete {
- if isInViewHierarchy {
- progressView.setProgress(1, animated: animated)
- }
+ let combinedRefreshProgress = AccountManager.shared.combinedRefreshProgress
+ isIndeterminate = combinedRefreshProgress.isIndeterminate
+
+ if combinedRefreshProgress.isComplete {
+ isRefreshing = false
+ progress = 1
func completeLabel() {
// Check that there are no pending downloads.
if AccountManager.shared.combinedRefreshProgress.isComplete {
- self.updateRefreshLabel()
- self.label.isHidden = false
- self.progressView.isHidden = true
- if self.superview != nil {
- self.progressView.setProgress(0, animated: animated)
- }
+ updateRefreshLabel()
+ progress = 0
}
}
@@ -88,19 +123,16 @@ private extension RefreshProgressView {
completeLabel()
}
} else {
- label.isHidden = true
- progressView.isHidden = false
- if isInViewHierarchy {
- let percent = Float(progress.numberCompleted) / Float(progress.numberOfTasks)
+ isRefreshing = true
+ let percent = Double(combinedRefreshProgress.numberCompleted) / Double(combinedRefreshProgress.numberOfTasks)
- // Don't let the progress bar go backwards unless we need to go back more than 25%
- if percent > progressView.progress || progressView.progress - percent > 0.25 {
- progressView.setProgress(percent, animated: animated)
- }
+ // Don't let the progress bar go backwards unless we need to go back more than 25%
+ if percent > progress || (progress - percent) > 0.25 {
+ progress = percent
}
}
}
-
+
func updateRefreshLabel() {
if let accountLastArticleFetchEndTime = AccountManager.shared.lastArticleFetchEndTime {
@@ -111,17 +143,15 @@ private extension RefreshProgressView {
let refreshed = relativeDateTimeFormatter.localizedString(for: accountLastArticleFetchEndTime, relativeTo: Date())
let localizedRefreshText = NSLocalizedString("Updated %@", comment: "Updated")
let refreshText = NSString.localizedStringWithFormat(localizedRefreshText as NSString, refreshed) as String
- label.text = refreshText
+ label = refreshText
} else {
- label.text = NSLocalizedString("Updated Just Now", comment: "Updated Just Now")
+ label = NSLocalizedString("Updated Just Now", comment: "Updated Just Now")
}
} else {
- label.text = ""
+ label = ""
}
-
- accessibilityLabel = label.text
}
func scheduleUpdateRefreshLabel() {
diff --git a/iOS/MasterFeed/RefreshProgressView.xib b/iOS/MasterFeed/RefreshProgressView.xib
deleted file mode 100644
index bab0971af..000000000
--- a/iOS/MasterFeed/RefreshProgressView.xib
+++ /dev/null
@@ -1,61 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift
index 999aec449..c55c36f4a 100644
--- a/iOS/MasterTimeline/MasterTimelineViewController.swift
+++ b/iOS/MasterTimeline/MasterTimelineViewController.swift
@@ -7,6 +7,7 @@
//
import UIKit
+import SwiftUI
import RSCore
import Account
import Articles
@@ -21,7 +22,9 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
@IBOutlet weak var markAllAsReadButton: UIBarButtonItem!
- private var refreshProgressView: RefreshProgressView!
+ let refreshProgressModel = RefreshProgressModel()
+ lazy var progressBarViewController = UIHostingController(rootView: RefreshProgressView(progressBarMode: refreshProgressModel))
+
private var refreshProgressItemButton: UIBarButtonItem!
private var firstUnreadButton: UIBarButtonItem!
@@ -95,13 +98,14 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
refreshControl = UIRefreshControl()
refreshControl!.addTarget(self, action: #selector(refreshAccounts(_:)), for: .valueChanged)
+ refreshControl!.tintColor = .clear
+
+ progressBarViewController.view.backgroundColor = .clear
+ progressBarViewController.view.translatesAutoresizingMaskIntoConstraints = false
+ refreshProgressItemButton = UIBarButtonItem(customView: progressBarViewController.view)
configureToolbar()
- refreshProgressView = Bundle.main.loadNibNamed("RefreshProgressView", owner: self, options: nil)?[0] as? RefreshProgressView
- refreshProgressItemButton = UIBarButtonItem(customView: refreshProgressView!)
-
-
resetUI(resetScroll: true)
// Load the table and then scroll to the saved position if available
@@ -243,7 +247,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
}
func updateUI() {
- refreshProgressView?.update()
+ refreshProgressModel.update()
updateTitleUnreadCount()
updateToolbar()
}
@@ -614,12 +618,6 @@ private extension MasterTimelineViewController {
return
}
- guard let refreshProgressView = Bundle.main.loadNibNamed("RefreshProgressView", owner: self, options: nil)?[0] as? RefreshProgressView else {
- return
- }
-
- self.refreshProgressView = refreshProgressView
- let refreshProgressItemButton = UIBarButtonItem(customView: refreshProgressView)
toolbarItems?.insert(refreshProgressItemButton, at: 2)
}
diff --git a/iOS/Resources/About.rtf b/iOS/Resources/About.rtf
deleted file mode 100644
index 8bf10c2d4..000000000
--- a/iOS/Resources/About.rtf
+++ /dev/null
@@ -1,12 +0,0 @@
-{\rtf1\ansi\ansicpg1252\cocoartf2513
-\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 LucidaGrande-Bold;}
-{\colortbl;\red255\green255\blue255;\red0\green0\blue0;\red10\green96\blue255;}
-{\*\expandedcolortbl;;\cssrgb\c0\c0\c0;\cssrgb\c0\c47843\c100000\cname systemBlueColor;}
-\margl1440\margr1440\vieww8340\viewh9300\viewkind0
-\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\li363\fi-364\pardirnatural\partightenfactor0
-
-\f0\b\fs28 \cf2 By Brent Simmons and the Ranchero Software team
-\fs22 \
-\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0
-{\field{\*\fldinst{HYPERLINK "https://netnewswire.com/"}}{\fldrslt
-\fs28 \cf3 netnewswire.com}}}
\ No newline at end of file
diff --git a/iOS/Resources/Assets.xcassets/About.imageset/AppIcon-1024px 1.png b/iOS/Resources/Assets.xcassets/About.imageset/AppIcon-1024px 1.png
new file mode 100644
index 000000000..efdd38490
Binary files /dev/null and b/iOS/Resources/Assets.xcassets/About.imageset/AppIcon-1024px 1.png differ
diff --git a/iOS/Resources/Assets.xcassets/About.imageset/AppIcon-1024px.png b/iOS/Resources/Assets.xcassets/About.imageset/AppIcon-1024px.png
new file mode 100644
index 000000000..efdd38490
Binary files /dev/null and b/iOS/Resources/Assets.xcassets/About.imageset/AppIcon-1024px.png differ
diff --git a/iOS/Resources/Assets.xcassets/About.imageset/Contents.json b/iOS/Resources/Assets.xcassets/About.imageset/Contents.json
new file mode 100644
index 000000000..d3a718431
--- /dev/null
+++ b/iOS/Resources/Assets.xcassets/About.imageset/Contents.json
@@ -0,0 +1,22 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "AppIcon-1024px 1.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "AppIcon-1024px.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/iOS/Resources/Credits.rtf b/iOS/Resources/Credits.rtf
deleted file mode 100644
index 1124bdb6f..000000000
--- a/iOS/Resources/Credits.rtf
+++ /dev/null
@@ -1,17 +0,0 @@
-{\rtf1\ansi\ansicpg1252\cocoartf2513
-\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 LucidaGrande;}
-{\colortbl;\red255\green255\blue255;\red0\green0\blue0;}
-{\*\expandedcolortbl;;\cssrgb\c0\c0\c0\cname textColor;}
-\margl1440\margr1440\vieww14220\viewh13280\viewkind0
-\deftab720
-\pard\pardeftab720\sa60\partightenfactor0
-
-\f0\fs22 \cf2 Lead developer: {\field{\*\fldinst{HYPERLINK "https://github.com/vincode-io"}}{\fldrslt Maurice Parker}}\
-App icon: {\field{\*\fldinst{HYPERLINK "https://twitter.com/BradEllis"}}{\fldrslt Brad Ellis}}\
-Feedly syncing: {\field{\*\fldinst{HYPERLINK "https://twitter.com/kielgillard"}}{\fldrslt Kiel Gillard}}\
-NewsBlur syncing: {\field{\*\fldinst{HYPERLINK "https://twitter.com/quanganhdo"}}{\fldrslt Anh Do}}\
-Under-the-hood magic and CSS stylin\'92s: {\field{\*\fldinst{HYPERLINK "https://github.com/wevah"}}{\fldrslt Nate Weaver}}\
-Newsfoot (JS footnote displayer): {\field{\*\fldinst{HYPERLINK "https://github.com/brehaut/"}}{\fldrslt Andrew Brehaut}}\
-Help book: {\field{\*\fldinst{HYPERLINK "https://nostodnayr.net/"}}{\fldrslt Ryan Dotson}}\
-And featuring contributions from {\field{\*\fldinst{HYPERLINK "https://github.com/danielpunkass"}}{\fldrslt Daniel Jalkut}}, {\field{\*\fldinst{HYPERLINK "https://rhonabwy.com/"}}{\fldrslt Joe Heck}}, {\field{\*\fldinst{HYPERLINK "https://github.com/olofhellman"}}{\fldrslt Olof Hellman}}, {\field{\*\fldinst{HYPERLINK "https://blog.rizwan.dev/"}}{\fldrslt Rizwan Mohamed Ibrahim}}, {\field{\*\fldinst{HYPERLINK "https://mynameisstuart.com/"}}{\fldrslt Stuart Breckenridge}}, {\field{\*\fldinst{HYPERLINK "https://twitter.com/philviso"}}{\fldrslt Phil Viso}}, and {\field{\*\fldinst{HYPERLINK "https://github.com/Ranchero-Software/NetNewsWire/graphs/contributors"}}{\fldrslt many more}}!\
-}
diff --git a/iOS/Resources/Dedication.rtf b/iOS/Resources/Dedication.rtf
deleted file mode 100644
index 974f1b818..000000000
--- a/iOS/Resources/Dedication.rtf
+++ /dev/null
@@ -1,9 +0,0 @@
-{\rtf1\ansi\ansicpg1252\cocoartf2513
-\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 LucidaGrande;}
-{\colortbl;\red255\green255\blue255;\red0\green0\blue0;}
-{\*\expandedcolortbl;;\cssrgb\c0\c0\c0\cname textColor;}
-\margl1440\margr1440\vieww9000\viewh8400\viewkind0
-\deftab720
-\pard\pardeftab720\sa60\partightenfactor0
-
-\f0\fs22 \cf2 NetNewsWire 6 is dedicated to everyone working to save democracy around the world.}
\ No newline at end of file
diff --git a/iOS/Resources/Thanks.rtf b/iOS/Resources/Thanks.rtf
deleted file mode 100644
index c3cde3b4c..000000000
--- a/iOS/Resources/Thanks.rtf
+++ /dev/null
@@ -1,11 +0,0 @@
-{\rtf1\ansi\ansicpg1252\cocoartf2511
-\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 LucidaGrande;}
-{\colortbl;\red255\green255\blue255;\red0\green0\blue0;}
-{\*\expandedcolortbl;;\cssrgb\c0\c0\c0\cname textColor;}
-\margl1440\margr1440\vieww11780\viewh11640\viewkind0
-\deftab720
-\pard\pardeftab720\li365\fi-366\sa60\partightenfactor0
-
-\f0\fs22 \cf2 Thanks to Sheila and my family; thanks to my friends in Seattle and around the globe; thanks to the ever-patient and ever-awesome NetNewsWire beta testers. \
-\pard\tx0\pardeftab720\li360\fi-361\sa60\partightenfactor0
-\cf2 Thanks to {\field{\*\fldinst{HYPERLINK "https://shapeof.com/"}}{\fldrslt Gus Mueller}} for {\field{\*\fldinst{HYPERLINK "https://github.com/ccgus/fmdb"}}{\fldrslt FMDB}} by {\field{\*\fldinst{HYPERLINK "http://flyingmeat.com/"}}{\fldrslt Flying Meat Software}}. Thanks to {\field{\*\fldinst{HYPERLINK "https://github.com"}}{\fldrslt GitHub}} and {\field{\*\fldinst{HYPERLINK "https://slack.com"}}{\fldrslt Slack}} for making open source collaboration easy and fun. Thanks to {\field{\*\fldinst{HYPERLINK "https://benubois.com/"}}{\fldrslt Ben Ubois}} at {\field{\*\fldinst{HYPERLINK "https://feedbin.com/"}}{\fldrslt Feedbin}} for all the extra help with syncing and article rendering \'97\'a0and for {\field{\*\fldinst{HYPERLINK "https://feedbin.com/blog/2019/03/11/the-future-of-full-content/"}}{\fldrslt hosting the server for the Reader view}}.}
\ No newline at end of file
diff --git a/iOS/RootSplitViewController.swift b/iOS/RootSplitViewController.swift
index 302d101ec..7b8c9d0c0 100644
--- a/iOS/RootSplitViewController.swift
+++ b/iOS/RootSplitViewController.swift
@@ -25,6 +25,11 @@ class RootSplitViewController: UISplitViewController {
coordinator.resetFocus()
}
+ override func show(_ column: UISplitViewController.Column) {
+ guard !coordinator.isNavigationDisabled else { return }
+ super.show(column)
+ }
+
// MARK: Keyboard Shortcuts
@objc func scrollOrGoToNextUnread(_ sender: Any?) {
diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift
index 79fe5be0f..89e028c85 100644
--- a/iOS/SceneCoordinator.swift
+++ b/iOS/SceneCoordinator.swift
@@ -40,9 +40,11 @@ struct FeedNode: Hashable {
var node: Node
var feedID: FeedIdentifier
- init(_ node: Node) {
+ init?(_ node: Node) {
+ guard let feed = node.representedObject as? Feed else { return nil }
+
self.node = node
- self.feedID = (node.representedObject as! Feed).feedID!
+ self.feedID = feed.feedID!
}
func hash(into hasher: inout Hasher) {
@@ -108,6 +110,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging {
}
}
+ private var directlyMarkedAsUnreadArticles = Set()
+
var prefersStatusBarHidden = false
private let treeControllerDelegate = WebFeedTreeControllerDelegate()
@@ -127,6 +131,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging {
return activity
}
+ var isNavigationDisabled = false
+
var isRootSplitCollapsed: Bool {
return rootSplitViewController.isCollapsed
}
@@ -289,15 +295,24 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging {
self.masterFeedViewController = rootSplitViewController.viewController(for: .primary) as? MasterFeedViewController
self.masterFeedViewController.coordinator = self
- self.masterFeedViewController?.navigationController?.delegate = self
-
+ if let navController = self.masterFeedViewController?.navigationController {
+ navController.delegate = self
+ configureNavigationController(navController)
+ }
+
self.masterTimelineViewController = rootSplitViewController.viewController(for: .supplementary) as? MasterTimelineViewController
self.masterTimelineViewController?.coordinator = self
- self.masterTimelineViewController?.navigationController?.delegate = self
+ if let navController = self.masterTimelineViewController?.navigationController {
+ navController.delegate = self
+ configureNavigationController(navController)
+ }
self.articleViewController = rootSplitViewController.viewController(for: .secondary) as? ArticleViewController
self.articleViewController?.coordinator = self
-
+ if let navController = self.articleViewController?.navigationController {
+ configureNavigationController(navController)
+ }
+
for sectionNode in treeController.rootNode.childNodes {
markExpanded(sectionNode)
shadowTable.append((sectionID: "", feedNodes: [FeedNode]()))
@@ -317,7 +332,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging {
NotificationCenter.default.addObserver(self, selector: #selector(accountDidDownloadArticles(_:)), name: .AccountDidDownloadArticles, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(importDownloadedTheme(_:)), name: .didEndDownloadingTheme, object: nil)
- NotificationCenter.default.addObserver(self, selector: #selector(themeDownloadDidFail(_:)), name: .didFailToImportThemeWithError, object: nil)
+ NotificationCenter.default.addObserver(self, selector: #selector(markStatusCommandDidDirectMarking(_:)), name: .MarkStatusCommandDidDirectMarking, object: nil)
+ NotificationCenter.default.addObserver(self, selector: #selector(markStatusCommandDidUndoDirectMarking(_:)), name: .MarkStatusCommandDidUndoDirectMarking, object: nil)
}
func restoreWindowState(_ activity: NSUserActivity?) {
@@ -534,16 +550,28 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging {
}
}
- @objc func themeDownloadDidFail(_ note: Notification) {
+ @objc func markStatusCommandDidDirectMarking(_ note: Notification) {
guard let userInfo = note.userInfo,
- let error = userInfo["error"] as? Error else {
- return
- }
- DispatchQueue.main.async {
- self.rootSplitViewController.presentError(error, dismiss: nil)
+ let articles = userInfo[Account.UserInfoKey.articles] as? Set,
+ let statusKey = userInfo[Account.UserInfoKey.statusKey] as? ArticleStatus.Key,
+ let flag = userInfo[Account.UserInfoKey.statusFlag] as? Bool else { return }
+
+ if statusKey == .read && flag == false {
+ directlyMarkedAsUnreadArticles.formUnion(articles)
}
}
+ @objc func markStatusCommandDidUndoDirectMarking(_ note: Notification) {
+ guard let userInfo = note.userInfo,
+ let articles = userInfo[Account.UserInfoKey.articles] as? Set,
+ let statusKey = userInfo[Account.UserInfoKey.statusKey] as? ArticleStatus.Key,
+ let flag = userInfo[Account.UserInfoKey.statusFlag] as? Bool else { return }
+
+ if statusKey == .read && flag == false {
+ directlyMarkedAsUnreadArticles.subtract(articles)
+ }
+ }
+
// MARK: API
func suspend() {
@@ -603,6 +631,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging {
return shadowTable[section].feedNodes.count
}
+ func nodeFor(_ section: Int) -> Node? {
+ return treeController.rootNode.childAtIndex(section)
+ }
+
func nodeFor(_ indexPath: IndexPath) -> Node? {
guard indexPath.section < shadowTable.count && indexPath.row < shadowTable[indexPath.section].feedNodes.count else {
return nil
@@ -610,9 +642,18 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging {
return shadowTable[indexPath.section].feedNodes[indexPath.row].node
}
+ func indexPathFor(_ object: AnyObject) -> IndexPath? {
+ guard let node = treeController.rootNode.descendantNodeRepresentingObject(object) else {
+ return nil
+ }
+ return indexPathFor(node)
+ }
+
func indexPathFor(_ node: Node) -> IndexPath? {
+ guard let feedNode = FeedNode(node) else { return nil }
+
for i in 0.. Void)? = nil) {
- markArticlesWithUndo(articles, statusKey: .read, flag: true, completion: completion)
+ var markableArticles = Set(articles)
+ markableArticles.subtract(directlyMarkedAsUnreadArticles)
+ markArticlesWithUndo(markableArticles, statusKey: .read, flag: true, directlyMarked: false, completion: completion)
}
func markAllAsReadInTimeline(completion: (() -> Void)? = nil) {
@@ -1020,13 +1072,13 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging {
func markAsReadForCurrentArticle() {
if let article = currentArticle {
- markArticlesWithUndo([article], statusKey: .read, flag: true)
+ markArticlesWithUndo([article], statusKey: .read, flag: true, directlyMarked: true)
}
}
func markAsUnreadForCurrentArticle() {
if let article = currentArticle {
- markArticlesWithUndo([article], statusKey: .read, flag: false)
+ markArticlesWithUndo([article], statusKey: .read, flag: false, directlyMarked: true)
}
}
@@ -1038,7 +1090,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging {
func toggleRead(_ article: Article) {
guard !article.status.read || article.isAvailableToMarkUnread else { return }
- markArticlesWithUndo([article], statusKey: .read, flag: !article.status.read)
+ markArticlesWithUndo([article], statusKey: .read, flag: !article.status.read, directlyMarked: true)
}
func toggleStarredForCurrentArticle() {
@@ -1048,7 +1100,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging {
}
func toggleStar(_ article: Article) {
- markArticlesWithUndo([article], statusKey: .starred, flag: !article.status.starred)
+ markArticlesWithUndo([article], statusKey: .starred, flag: !article.status.starred, directlyMarked: true)
}
func timelineFeedIsEqualTo(_ feed: WebFeed) -> Bool {
@@ -1085,9 +1137,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging {
rebuildBackingStores(initialLoad: initialLoad, completion: {
self.treeControllerDelegate.resetFilterExceptions()
- self.selectFeed(nil) {
- self.selectFeed(webFeed, animations: animations, completion: completion)
- }
+ self.selectFeed(webFeed, animations: animations, completion: completion)
})
}
@@ -1259,13 +1309,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging {
}
func importTheme(filename: String) {
- do {
- try ArticleThemeImporter.importTheme(controller: rootSplitViewController, filename: filename)
- } catch {
- NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error" : error])
- logger.error("Failed to import theme with error: \(error.localizedDescription, privacy: .public)")
- }
-
+ ArticleThemeImporter.importTheme(controller: rootSplitViewController, filename: filename)
}
/// This will dismiss the foremost view controller if the user
@@ -1314,6 +1358,10 @@ extension SceneCoordinator: UISplitViewControllerDelegate {
}
}
+ func splitViewController(_ svc: UISplitViewController, willChangeTo displayMode: UISplitViewController.DisplayMode) {
+ articleViewController?.splitViewControllerWillChangeTo(displayMode: displayMode)
+ }
+
}
// MARK: UINavigationControllerDelegate
@@ -1370,9 +1418,43 @@ extension SceneCoordinator: UINavigationControllerDelegate {
private extension SceneCoordinator {
- func markArticlesWithUndo(_ articles: [Article], statusKey: ArticleStatus.Key, flag: Bool, completion: (() -> Void)? = nil) {
+ func configureNavigationController(_ navController: UINavigationController) {
+
+ let scrollEdge = UINavigationBarAppearance()
+ scrollEdge.configureWithOpaqueBackground()
+ scrollEdge.shadowColor = nil
+ scrollEdge.shadowImage = UIImage()
+
+ let standard = UINavigationBarAppearance()
+ standard.shadowColor = .opaqueSeparator
+ standard.shadowImage = UIImage()
+
+ navController.navigationBar.standardAppearance = standard
+ navController.navigationBar.compactAppearance = standard
+ navController.navigationBar.scrollEdgeAppearance = scrollEdge
+ navController.navigationBar.compactScrollEdgeAppearance = scrollEdge
+
+ navController.navigationBar.tintColor = AppAssets.primaryAccentColor
+
+ let toolbarAppearance = UIToolbarAppearance()
+ navController.toolbar.standardAppearance = toolbarAppearance
+ navController.toolbar.compactAppearance = toolbarAppearance
+ navController.toolbar.scrollEdgeAppearance = toolbarAppearance
+ navController.toolbar.tintColor = AppAssets.primaryAccentColor
+ }
+
+ func markArticlesWithUndo(_ articles: [Article], statusKey: ArticleStatus.Key, flag: Bool, directlyMarked: Bool, completion: (() -> Void)? = nil) {
+ markArticlesWithUndo(Set(articles), statusKey: statusKey, flag: flag, directlyMarked: directlyMarked, completion: completion)
+ }
+
+ func markArticlesWithUndo(_ articles: Set, statusKey: ArticleStatus.Key, flag: Bool, directlyMarked: Bool, completion: (() -> Void)? = nil) {
guard let undoManager = undoManager,
- let markReadCommand = MarkStatusCommand(initialArticles: articles, statusKey: statusKey, flag: flag, undoManager: undoManager, completion: completion) else {
+ let markReadCommand = MarkStatusCommand(initialArticles: articles,
+ statusKey: statusKey,
+ flag: flag,
+ directlyMarked: directlyMarked,
+ undoManager: undoManager,
+ completion: completion) else {
completion?()
return
}
@@ -1478,10 +1560,12 @@ private extension SceneCoordinator {
if isExpanded(sectionNode) {
for node in sectionNode.childNodes {
- feedNodes.append(FeedNode(node))
+ guard let feedNode = FeedNode(node) else { continue }
+ feedNodes.append(feedNode)
if isExpanded(node) {
for child in node.childNodes {
- feedNodes.append(FeedNode(child))
+ guard let childNode = FeedNode(child) else { continue }
+ feedNodes.append(childNode)
}
}
}
@@ -1588,13 +1672,6 @@ private extension SceneCoordinator {
}
}
- func indexPathFor(_ object: AnyObject) -> IndexPath? {
- guard let node = treeController.rootNode.descendantNodeRepresentingObject(object) else {
- return nil
- }
- return indexPathFor(node)
- }
-
func setTimelineFeed(_ feed: Feed?, animated: Bool, completion: (() -> Void)? = nil) {
timelineFeed = feed
@@ -1895,6 +1972,7 @@ private extension SceneCoordinator {
func emptyTheTimeline() {
if !articles.isEmpty {
+ directlyMarkedAsUnreadArticles = Set()
replaceArticles(with: Set(), animated: false)
}
}
@@ -1965,8 +2043,10 @@ private extension SceneCoordinator {
// To be called when we need to do an entire fetch, but an async delay is okay.
// Example: we have the Today feed selected, and the calendar day just changed.
cancelPendingAsyncFetches()
+
+ emptyTheTimeline()
+
guard let timelineFeed = timelineFeed else {
- emptyTheTimeline()
completion()
return
}
@@ -1982,7 +2062,6 @@ private extension SceneCoordinator {
self?.replaceArticles(with: articles, animated: animated)
completion()
}
-
}
func fetchUnsortedArticlesAsync(for representedObjects: [Any], completion: @escaping ArticleSetBlock) {
diff --git a/iOS/SceneDelegate.swift b/iOS/SceneDelegate.swift
index 402f822ef..4c2fc6966 100644
--- a/iOS/SceneDelegate.swift
+++ b/iOS/SceneDelegate.swift
@@ -28,6 +28,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, Logging {
coordinator = SceneCoordinator(rootSplitViewController: rootViewController)
rootViewController.coordinator = coordinator
rootViewController.delegate = coordinator
+ rootViewController.showsSecondaryOnlyButton = true
coordinator.restoreWindowState(session.stateRestorationActivity)
@@ -95,6 +96,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, Logging {
coordinator.cleanUp(conditional: conditional)
}
+ func presentError(_ error: Error) {
+ self.window!.rootViewController?.presentError(error)
+ }
+
// Handle Opening of URLs
func scene(_ scene: UIScene, openURLContexts urlContexts: Set) {
@@ -185,14 +190,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, Logging {
NotificationCenter.default.post(name: .didBeginDownloadingTheme, object: nil)
}
let task = URLSession.shared.downloadTask(with: request) { [weak self] location, response, error in
- guard
- let location = location else { return }
+ guard let self, let location else { return }
do {
try ArticleThemeDownloader.shared.handleFile(at: location)
} catch {
- NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error": error])
- self?.logger.error("Failed to import theme with error: \(error.localizedDescription, privacy: .public)")
+ self.presentError(error)
}
}
task.resume()
@@ -204,7 +207,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, Logging {
return
}
-
}
}
}
diff --git a/iOS/Settings/AboutView.swift b/iOS/Settings/AboutView.swift
new file mode 100644
index 000000000..70cd411e2
--- /dev/null
+++ b/iOS/Settings/AboutView.swift
@@ -0,0 +1,108 @@
+//
+// AboutView.swift
+// NetNewsWire-iOS
+//
+// Created by Stuart Breckenridge on 02/10/2022.
+// Copyright © 2022 Ranchero Software. All rights reserved.
+//
+
+import SwiftUI
+
+struct AboutView: View, LoadableAboutData {
+
+ var body: some View {
+ List {
+ Section(header: aboutHeaderView) {}
+ Section(header: Text("Primary Contributors")) {
+ ForEach(0.. some View {
+ HStack {
+ Text(appCredit.name)
+ Spacer()
+ if let role = appCredit.role {
+ Text(role)
+ .font(.footnote)
+ .foregroundColor(.secondary)
+ .multilineTextAlignment(.trailing)
+ }
+ if let _ = appCredit.url {
+ Image(systemName: "info.circle")
+ .foregroundColor(.secondary)
+ }
+ }
+ .onTapGesture {
+ guard let url = appCredit.url else { return }
+ if let creditURL = URL(string: url) {
+ UIApplication.shared.open(creditURL)
+ }
+ }
+ }
+
+
+ var thanks: some View {
+ Text(about.ThanksMarkdown)
+ .multilineTextAlignment(.center)
+ .font(.callout)
+ }
+
+ var copyright: some View {
+ HStack {
+ Spacer()
+ Text(verbatim: "Copyright © Brent Simmons 2002 - \(Calendar.current.component(.year, from: .now))")
+ Spacer()
+ }
+ }
+
+}
+
+
+struct AboutView_Previews: PreviewProvider {
+ static var previews: some View {
+ NavigationView {
+ AboutView()
+ }
+ }
+}
diff --git a/iOS/Settings/AboutViewController.swift b/iOS/Settings/AboutViewController.swift
deleted file mode 100644
index bd7e76e16..000000000
--- a/iOS/Settings/AboutViewController.swift
+++ /dev/null
@@ -1,59 +0,0 @@
-//
-// AboutViewController.swift
-// NetNewsWire-iOS
-//
-// Created by Maurice Parker on 4/25/19.
-// Copyright © 2019 Ranchero Software. All rights reserved.
-//
-
-import UIKit
-
-class AboutViewController: UITableViewController {
-
- @IBOutlet weak var aboutTextView: UITextView!
- @IBOutlet weak var creditsTextView: UITextView!
- @IBOutlet weak var acknowledgmentsTextView: UITextView!
- @IBOutlet weak var thanksTextView: UITextView!
- @IBOutlet weak var dedicationTextView: UITextView!
-
- override func viewDidLoad() {
-
- super.viewDidLoad()
-
- configureCell(file: "About", textView: aboutTextView)
- configureCell(file: "Credits", textView: creditsTextView)
- configureCell(file: "Thanks", textView: thanksTextView)
- configureCell(file: "Dedication", textView: dedicationTextView)
-
- let buildLabel = NonIntrinsicLabel(frame: CGRect(x: 32.0, y: 0.0, width: 0.0, height: 0.0))
- buildLabel.font = UIFont.systemFont(ofSize: 11.0)
- buildLabel.textColor = UIColor.gray
- buildLabel.text = NSLocalizedString("Copyright © 2002-2022 Brent Simmons", comment: "Copyright")
- buildLabel.numberOfLines = 0
- buildLabel.sizeToFit()
- buildLabel.translatesAutoresizingMaskIntoConstraints = false
-
- let wrapperView = UIView(frame: CGRect(x: 0, y: 0, width: buildLabel.frame.width, height: buildLabel.frame.height + 10.0))
- wrapperView.translatesAutoresizingMaskIntoConstraints = false
- wrapperView.addSubview(buildLabel)
- tableView.tableFooterView = wrapperView
- }
-
- override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
- return UITableView.automaticDimension
- }
-
-}
-
-private extension AboutViewController {
-
- func configureCell(file: String, textView: UITextView) {
- let url = Bundle.main.url(forResource: file, withExtension: "rtf")!
- let string = try! NSAttributedString(url: url, options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.rtf], documentAttributes: nil)
- textView.attributedText = string
- textView.textColor = UIColor.label
- textView.adjustsFontForContentSizeCategory = true
- textView.font = .preferredFont(forTextStyle: .body)
- }
-
-}
diff --git a/iOS/Settings/ArticleThemeImporter.swift b/iOS/Settings/ArticleThemeImporter.swift
index 1f71e6033..8e184842a 100644
--- a/iOS/Settings/ArticleThemeImporter.swift
+++ b/iOS/Settings/ArticleThemeImporter.swift
@@ -11,8 +11,14 @@ import RSCore
struct ArticleThemeImporter: Logging {
- static func importTheme(controller: UIViewController, filename: String) throws {
- let theme = try ArticleTheme(path: filename, isAppTheme: false)
+ static func importTheme(controller: UIViewController, filename: String) {
+ let theme: ArticleTheme
+ do {
+ theme = try ArticleTheme(path: filename, isAppTheme: false)
+ } catch {
+ controller.presentError(error)
+ return
+ }
let localizedTitleText = NSLocalizedString("Install theme “%@” by %@?", comment: "Theme message text")
let title = NSString.localizedStringWithFormat(localizedTitleText as NSString, theme.name, theme.creatorName) as String
@@ -29,7 +35,7 @@ struct ArticleThemeImporter: Logging {
let visitSiteTitle = NSLocalizedString("Show Website", comment: "Show Website")
let visitSiteAction = UIAlertAction(title: visitSiteTitle, style: .default) { action in
UIApplication.shared.open(url)
- try? Self.importTheme(controller: controller, filename: filename)
+ Self.importTheme(controller: controller, filename: filename)
}
alertController.addAction(visitSiteAction)
}
diff --git a/iOS/Settings/ArticleThemesTableViewController.swift b/iOS/Settings/ArticleThemesTableViewController.swift
index 66d118caf..deefb41e9 100644
--- a/iOS/Settings/ArticleThemesTableViewController.swift
+++ b/iOS/Settings/ArticleThemesTableViewController.swift
@@ -72,9 +72,9 @@ class ArticleThemesTableViewController: UITableViewController, Logging {
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
guard let cell = tableView.cellForRow(at: indexPath),
- let themeName = cell.textLabel?.text,
- let theme = ArticleThemesManager.shared.articleThemeWithThemeName(themeName),
- !theme.isAppTheme else { return nil }
+ let themeName = cell.textLabel?.text else { return nil }
+
+ guard let theme = try? ArticleThemesManager.shared.articleThemeWithThemeName(themeName), !theme.isAppTheme else { return nil }
let deleteTitle = NSLocalizedString("Delete", comment: "Delete")
let deleteAction = UIContextualAction(style: .normal, title: deleteTitle) { [weak self] (action, view, completion) in
@@ -114,12 +114,7 @@ extension ArticleThemesTableViewController: UIDocumentPickerDelegate {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
guard let url = urls.first else { return }
- do {
- try ArticleThemeImporter.importTheme(controller: self, filename: url.standardizedFileURL.path)
- } catch {
- NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error": error])
- logger.error("Did fail to import theme: \(error.localizedDescription, privacy: .public)")
- }
+ try ArticleThemeImporter.importTheme(controller: self, filename: url.standardizedFileURL.path)
}
}
diff --git a/iOS/Settings/Settings.storyboard b/iOS/Settings/Settings.storyboard
index b9515ae5e..68ff0e72c 100644
--- a/iOS/Settings/Settings.storyboard
+++ b/iOS/Settings/Settings.storyboard
@@ -1,8 +1,9 @@
-
+
-
+
+
@@ -20,14 +21,14 @@
-
+
-
+
-
+
@@ -37,14 +38,14 @@
-
+
-
+
-
+
@@ -58,14 +59,14 @@
-
+
-
+
-
+
@@ -79,14 +80,14 @@
-
+
-
+
-
+
@@ -100,7 +101,7 @@
-
+
@@ -117,7 +118,7 @@
-
+
@@ -134,7 +135,7 @@
-
+
@@ -155,14 +156,14 @@
-
+
-
+
@@ -188,14 +189,14 @@
-
+
-
+
@@ -221,14 +222,14 @@
-
+
-
+
@@ -254,14 +255,14 @@
-
+
-
+
-
+
@@ -281,20 +282,20 @@
-
+
-
+
-
+
-
+
@@ -313,14 +314,14 @@
-
+
-
+
@@ -346,20 +347,20 @@
-
+
-
+
-
-
+
+
-
+
@@ -384,19 +385,19 @@
-
+
-
+
-
+
-
+
@@ -420,14 +421,14 @@
-
+
-
-
+
+
@@ -437,13 +438,13 @@
-
+
-
+
@@ -454,13 +455,13 @@
-
+
-
+
@@ -471,7 +472,7 @@
-
+
@@ -488,7 +489,7 @@
-
+
@@ -505,7 +506,7 @@
-
+
@@ -522,7 +523,7 @@
-
+
@@ -539,7 +540,7 @@
-
+
@@ -556,14 +557,14 @@
-
+
-
+
-
+
@@ -610,14 +611,14 @@
-
+
-
+
@@ -628,7 +629,7 @@
-
+
@@ -663,7 +664,7 @@
-
+
@@ -717,7 +718,7 @@
-
+
@@ -743,7 +744,7 @@
-
+
@@ -769,7 +770,7 @@
-
+
@@ -815,11 +816,11 @@
-
+
-
+
@@ -839,19 +840,19 @@
-
+
-
+
-
+
@@ -871,7 +872,7 @@
-
+
@@ -918,7 +919,7 @@
-
+
@@ -942,7 +943,7 @@
-
+
@@ -974,12 +975,12 @@
-
+
-
+
@@ -1017,14 +1018,14 @@
-
+
-
+
@@ -1035,7 +1036,7 @@
-
+
@@ -1078,27 +1079,27 @@
-
+
-
+
-
+
-
+
-
+
@@ -1123,7 +1124,7 @@
-
+
@@ -1170,7 +1171,7 @@
-
+
@@ -1192,14 +1193,14 @@
-
+
-
+
-
+
@@ -1218,7 +1219,7 @@
-
+
@@ -1273,12 +1274,12 @@
-
+
-
+
@@ -1310,8 +1311,8 @@
-
-
+
+
@@ -1325,7 +1326,7 @@
-
+
diff --git a/iOS/Settings/SettingsViewController.swift b/iOS/Settings/SettingsViewController.swift
index a1ab1f4af..2112e62dd 100644
--- a/iOS/Settings/SettingsViewController.swift
+++ b/iOS/Settings/SettingsViewController.swift
@@ -86,18 +86,6 @@ class SettingsViewController: UITableViewController, Logging {
colorPaletteDetailLabel.text = String(describing: AppDefaults.userInterfaceColorPalette)
openLinksInNetNewsWire.isOn = !AppDefaults.shared.useSystemBrowser
- let buildLabel = NonIntrinsicLabel(frame: CGRect(x: 32.0, y: 0.0, width: 0.0, height: 0.0))
- buildLabel.font = UIFont.systemFont(ofSize: 11.0)
- buildLabel.textColor = UIColor.gray
- buildLabel.text = "\(Bundle.main.appName) \(Bundle.main.versionNumber) (Build \(Bundle.main.buildNumber))"
- buildLabel.sizeToFit()
- buildLabel.translatesAutoresizingMaskIntoConstraints = false
-
- let wrapperView = UIView(frame: CGRect(x: 0, y: 0, width: buildLabel.frame.width, height: buildLabel.frame.height + 10.0))
- wrapperView.translatesAutoresizingMaskIntoConstraints = false
- wrapperView.addSubview(buildLabel)
- tableView.tableFooterView = wrapperView
-
}
override func viewDidAppear(_ animated: Bool) {
@@ -283,8 +271,8 @@ class SettingsViewController: UITableViewController, Logging {
openURL("https://netnewswire.com/slack")
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
case 8:
- let timeline = UIStoryboard.settings.instantiateController(ofType: AboutViewController.self)
- self.navigationController?.pushViewController(timeline, animated: true)
+ let hosting = UIHostingController(rootView: AboutView())
+ self.navigationController?.pushViewController(hosting, animated: true)
default:
break
}
diff --git a/iOS/UIKit Extensions/RoundedProgressView.swift b/iOS/UIKit Extensions/RoundedProgressView.swift
deleted file mode 100644
index a48aa6bf1..000000000
--- a/iOS/UIKit Extensions/RoundedProgressView.swift
+++ /dev/null
@@ -1,21 +0,0 @@
-//
-// RoundedProgressView.swift
-// NetNewsWire
-//
-// Created by Maurice Parker on 10/29/19.
-// Copyright © 2019 Ranchero Software. All rights reserved.
-//
-
-import UIKit
-
-class RoundedProgressView: UIProgressView {
-
- override func layoutSubviews() {
- super.layoutSubviews()
- subviews.forEach { subview in
- subview.layer.masksToBounds = true
- subview.layer.cornerRadius = bounds.height / 2.0
- }
- }
-
-}
diff --git a/xcconfig/common/NetNewsWire_ios_target_common.xcconfig b/xcconfig/common/NetNewsWire_ios_target_common.xcconfig
index 50059a9e4..0a90fb07f 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 = 6.1
-CURRENT_PROJECT_VERSION = 6105
+CURRENT_PROJECT_VERSION = 6110
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 70ad30b32..fc8972368 100644
--- a/xcconfig/common/NetNewsWire_mac_target_common.xcconfig
+++ b/xcconfig/common/NetNewsWire_mac_target_common.xcconfig
@@ -1,6 +1,6 @@
// High Level Settings common to both the Mac application and any extensions we bundle with it
-MARKETING_VERSION = 6.1
-CURRENT_PROJECT_VERSION = 6106
+MARKETING_VERSION = 6.1.1
+CURRENT_PROJECT_VERSION = 6107
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;