Continue fixing concurrency warnings.
This commit is contained in:
parent
6ab10e871c
commit
d0760f3d12
@ -709,7 +709,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
public func articles(feed: Feed) async throws -> Set<Article> {
|
public func articles(feed: Feed) async throws -> Set<Article> {
|
||||||
|
|
||||||
let articles = try await database.articles(feedID: feed.feedID)
|
let articles = try await database.articles(feedID: feed.feedID)
|
||||||
validateUnreadCount(feed, articles)
|
await validateUnreadCount(feed, articles)
|
||||||
return articles
|
return articles
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -801,12 +801,15 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
precondition(type == .onMyMac || type == .cloudKit)
|
precondition(type == .onMyMac || type == .cloudKit)
|
||||||
|
|
||||||
database.update(with: parsedItems, feedID: feedID, deleteOlder: deleteOlder) { updateArticlesResult in
|
database.update(with: parsedItems, feedID: feedID, deleteOlder: deleteOlder) { updateArticlesResult in
|
||||||
switch updateArticlesResult {
|
|
||||||
case .success(let articleChanges):
|
MainActor.assumeIsolated {
|
||||||
self.sendNotificationAbout(articleChanges)
|
switch updateArticlesResult {
|
||||||
completion(.success(articleChanges))
|
case .success(let articleChanges):
|
||||||
case .failure(let databaseError):
|
self.sendNotificationAbout(articleChanges)
|
||||||
completion(.failure(databaseError))
|
completion(.success(articleChanges))
|
||||||
|
case .failure(let databaseError):
|
||||||
|
completion(.failure(databaseError))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -821,12 +824,15 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
}
|
}
|
||||||
|
|
||||||
database.update(feedIDsAndItems: feedIDsAndItems, defaultRead: defaultRead) { updateArticlesResult in
|
database.update(feedIDsAndItems: feedIDsAndItems, defaultRead: defaultRead) { updateArticlesResult in
|
||||||
switch updateArticlesResult {
|
|
||||||
case .success(let newAndUpdatedArticles):
|
MainActor.assumeIsolated {
|
||||||
self.sendNotificationAbout(newAndUpdatedArticles)
|
switch updateArticlesResult {
|
||||||
completion(nil)
|
case .success(let newAndUpdatedArticles):
|
||||||
case .failure(let databaseError):
|
self.sendNotificationAbout(newAndUpdatedArticles)
|
||||||
completion(databaseError)
|
completion(nil)
|
||||||
|
case .failure(let databaseError):
|
||||||
|
completion(databaseError)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -839,14 +845,17 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
}
|
}
|
||||||
|
|
||||||
database.mark(articles, statusKey: statusKey, flag: flag) { result in
|
database.mark(articles, statusKey: statusKey, flag: flag) { result in
|
||||||
switch result {
|
|
||||||
case .success(let updatedStatuses):
|
MainActor.assumeIsolated {
|
||||||
let updatedArticleIDs = updatedStatuses.articleIDs()
|
switch result {
|
||||||
let updatedArticles = Set(articles.filter{ updatedArticleIDs.contains($0.articleID) })
|
case .success(let updatedStatuses):
|
||||||
self.noteStatusesForArticlesDidChange(updatedArticles)
|
let updatedArticleIDs = updatedStatuses.articleIDs()
|
||||||
completion(.success(updatedArticles))
|
let updatedArticles = Set(articles.filter{ updatedArticleIDs.contains($0.articleID) })
|
||||||
case .failure(let error):
|
self.noteStatusesForArticlesDidChange(updatedArticles)
|
||||||
completion(.failure(error))
|
completion(.success(updatedArticles))
|
||||||
|
case .failure(let error):
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1118,12 +1127,15 @@ private extension Account {
|
|||||||
|
|
||||||
func fetchArticlesAsync(feed: Feed, _ completion: @escaping ArticleSetResultBlock) {
|
func fetchArticlesAsync(feed: Feed, _ completion: @escaping ArticleSetResultBlock) {
|
||||||
database.fetchArticlesAsync(feed.feedID) { [weak self] articleSetResult in
|
database.fetchArticlesAsync(feed.feedID) { [weak self] articleSetResult in
|
||||||
switch articleSetResult {
|
|
||||||
case .success(let articles):
|
MainActor.assumeIsolated {
|
||||||
self?.validateUnreadCount(feed, articles)
|
switch articleSetResult {
|
||||||
completion(.success(articles))
|
case .success(let articles):
|
||||||
case .failure(let databaseError):
|
self?.validateUnreadCount(feed, articles)
|
||||||
completion(.failure(databaseError))
|
completion(.success(articles))
|
||||||
|
case .failure(let databaseError):
|
||||||
|
completion(.failure(databaseError))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1227,7 +1239,7 @@ private extension Account {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateUnreadCount(_ feed: Feed, _ articles: Set<Article>) {
|
@MainActor func validateUnreadCount(_ feed: Feed, _ articles: Set<Article>) {
|
||||||
// articles must contain all the unread articles for the feed.
|
// articles must contain all the unread articles for the feed.
|
||||||
// The unread number should match the feed’s unread count.
|
// The unread number should match the feed’s unread count.
|
||||||
|
|
||||||
@ -1301,7 +1313,7 @@ private extension Account {
|
|||||||
unreadCount = updatedUnreadCount
|
unreadCount = updatedUnreadCount
|
||||||
}
|
}
|
||||||
|
|
||||||
func noteStatusesForArticlesDidChange(_ articles: Set<Article>) {
|
@MainActor func noteStatusesForArticlesDidChange(_ articles: Set<Article>) {
|
||||||
let feeds = Set(articles.compactMap { $0.feed })
|
let feeds = Set(articles.compactMap { $0.feed })
|
||||||
let statuses = Set(articles.map { $0.status })
|
let statuses = Set(articles.map { $0.status })
|
||||||
let articleIDs = Set(articles.map { $0.articleID })
|
let articleIDs = Set(articles.map { $0.articleID })
|
||||||
@ -1389,7 +1401,7 @@ private extension Account {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendNotificationAbout(_ articleChanges: ArticleChanges) {
|
@MainActor func sendNotificationAbout(_ articleChanges: ArticleChanges) {
|
||||||
var feeds = Set<Feed>()
|
var feeds = Set<Feed>()
|
||||||
|
|
||||||
if let newArticles = articleChanges.newArticles {
|
if let newArticles = articleChanges.newArticles {
|
||||||
|
@ -18,7 +18,7 @@ import Secrets
|
|||||||
|
|
||||||
public final class AccountManager: UnreadCountProvider {
|
public final class AccountManager: UnreadCountProvider {
|
||||||
|
|
||||||
public static var shared: AccountManager!
|
@MainActor public static var shared: AccountManager!
|
||||||
public static let netNewsWireNewsURL = "https://netnewswire.blog/feed.xml"
|
public static let netNewsWireNewsURL = "https://netnewswire.blog/feed.xml"
|
||||||
private static let jsonNetNewsWireNewsURL = "https://netnewswire.blog/feed.json"
|
private static let jsonNetNewsWireNewsURL = "https://netnewswire.blog/feed.json"
|
||||||
|
|
||||||
@ -79,7 +79,7 @@ public final class AccountManager: UnreadCountProvider {
|
|||||||
return lastArticleFetchEndTime
|
return lastArticleFetchEndTime
|
||||||
}
|
}
|
||||||
|
|
||||||
public func existingActiveAccount(forDisplayName displayName: String) -> Account? {
|
@MainActor public func existingActiveAccount(forDisplayName displayName: String) -> Account? {
|
||||||
return AccountManager.shared.activeAccounts.first(where: { $0.nameForDisplay == displayName })
|
return AccountManager.shared.activeAccounts.first(where: { $0.nameForDisplay == displayName })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +86,7 @@ final class CloudKitArticlesZone: CloudKitZone {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveNewArticles(_ articles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
@MainActor func saveNewArticles(_ articles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||||
guard !articles.isEmpty else {
|
guard !articles.isEmpty else {
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
return
|
return
|
||||||
@ -112,7 +112,7 @@ final class CloudKitArticlesZone: CloudKitZone {
|
|||||||
delete(ckQuery: ckQuery, completion: completion)
|
delete(ckQuery: ckQuery, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func modifyArticles(_ statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result<Void, Error>) -> Void)) {
|
@MainActor func modifyArticles(_ statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||||
guard !statusUpdates.isEmpty else {
|
guard !statusUpdates.isEmpty else {
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
return
|
return
|
||||||
@ -164,7 +164,7 @@ final class CloudKitArticlesZone: CloudKitZone {
|
|||||||
|
|
||||||
private extension CloudKitArticlesZone {
|
private extension CloudKitArticlesZone {
|
||||||
|
|
||||||
func handleModifyArticlesError(_ error: Error, statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result<Void, Error>) -> Void)) {
|
@MainActor func handleModifyArticlesError(_ error: Error, statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||||
if case CloudKitZoneError.userDeletedZone = error {
|
if case CloudKitZoneError.userDeletedZone = error {
|
||||||
self.createZoneRecord() { result in
|
self.createZoneRecord() { result in
|
||||||
switch result {
|
switch result {
|
||||||
@ -187,7 +187,7 @@ private extension CloudKitArticlesZone {
|
|||||||
return "a|\(id)"
|
return "a|\(id)"
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeStatusRecord(_ article: Article) -> CKRecord {
|
@MainActor func makeStatusRecord(_ article: Article) -> CKRecord {
|
||||||
let recordID = CKRecord.ID(recordName: statusID(article.articleID), zoneID: zoneID)
|
let recordID = CKRecord.ID(recordName: statusID(article.articleID), zoneID: zoneID)
|
||||||
let record = CKRecord(recordType: CloudKitArticleStatus.recordType, recordID: recordID)
|
let record = CKRecord(recordType: CloudKitArticleStatus.recordType, recordID: recordID)
|
||||||
if let feedExternalID = article.feed?.externalID {
|
if let feedExternalID = article.feed?.externalID {
|
||||||
@ -198,7 +198,7 @@ private extension CloudKitArticlesZone {
|
|||||||
return record
|
return record
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeStatusRecord(_ statusUpdate: CloudKitArticleStatusUpdate) -> CKRecord {
|
@MainActor func makeStatusRecord(_ statusUpdate: CloudKitArticleStatusUpdate) -> CKRecord {
|
||||||
let recordID = CKRecord.ID(recordName: statusID(statusUpdate.articleID), zoneID: zoneID)
|
let recordID = CKRecord.ID(recordName: statusID(statusUpdate.articleID), zoneID: zoneID)
|
||||||
let record = CKRecord(recordType: CloudKitArticleStatus.recordType, recordID: recordID)
|
let record = CKRecord(recordType: CloudKitArticleStatus.recordType, recordID: recordID)
|
||||||
|
|
||||||
@ -212,7 +212,7 @@ private extension CloudKitArticlesZone {
|
|||||||
return record
|
return record
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeArticleRecord(_ article: Article) -> CKRecord {
|
@MainActor func makeArticleRecord(_ article: Article) -> CKRecord {
|
||||||
let recordID = CKRecord.ID(recordName: articleID(article.articleID), zoneID: zoneID)
|
let recordID = CKRecord.ID(recordName: articleID(article.articleID), zoneID: zoneID)
|
||||||
let record = CKRecord(recordType: CloudKitArticle.recordType, recordID: recordID)
|
let record = CKRecord(recordType: CloudKitArticle.recordType, recordID: recordID)
|
||||||
|
|
||||||
|
@ -139,22 +139,24 @@ private extension CloudKitSendStatusOperation {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
articlesZone.modifyArticles(statusUpdates) { result in
|
|
||||||
switch result {
|
Task { @MainActor in
|
||||||
case .success:
|
articlesZone.modifyArticles(statusUpdates) { result in
|
||||||
self.database.deleteSelectedForProcessing(statusUpdates.map({ $0.articleID })) { _ in
|
switch result {
|
||||||
done(false)
|
case .success:
|
||||||
}
|
self.database.deleteSelectedForProcessing(statusUpdates.map({ $0.articleID })) { _ in
|
||||||
case .failure(let error):
|
done(false)
|
||||||
self.database.resetSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in
|
}
|
||||||
self.processAccountError(account, error)
|
case .failure(let error):
|
||||||
os_log(.error, log: self.log, "Send article status modify articles error: %@.", error.localizedDescription)
|
self.database.resetSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in
|
||||||
completion(true)
|
self.processAccountError(account, error)
|
||||||
|
os_log(.error, log: self.log, "Send article status modify articles error: %@.", error.localizedDescription)
|
||||||
|
completion(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
|
@ -48,7 +48,7 @@ extension Feed {
|
|||||||
|
|
||||||
public extension Article {
|
public extension Article {
|
||||||
|
|
||||||
var account: Account? {
|
@MainActor var account: Account? {
|
||||||
// The force unwrapped shared instance was crashing Account.framework unit tests.
|
// The force unwrapped shared instance was crashing Account.framework unit tests.
|
||||||
guard let manager = AccountManager.shared else {
|
guard let manager = AccountManager.shared else {
|
||||||
return nil
|
return nil
|
||||||
@ -56,7 +56,7 @@ public extension Article {
|
|||||||
return manager.existingAccount(with: accountID)
|
return manager.existingAccount(with: accountID)
|
||||||
}
|
}
|
||||||
|
|
||||||
var feed: Feed? {
|
@MainActor var feed: Feed? {
|
||||||
return account?.existingFeed(withFeedID: feedID)
|
return account?.existingFeed(withFeedID: feedID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,7 +133,7 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError {
|
|||||||
return anchor
|
return anchor
|
||||||
}
|
}
|
||||||
|
|
||||||
private func didEndRequestingAccessToken(_ result: Result<OAuthAuthorizationGrant, Error>) {
|
@MainActor private func didEndRequestingAccessToken(_ result: Result<OAuthAuthorizationGrant, Error>) {
|
||||||
guard !isCanceled else {
|
guard !isCanceled else {
|
||||||
didFinish()
|
didFinish()
|
||||||
return
|
return
|
||||||
@ -147,7 +147,7 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func saveAccount(for grant: OAuthAuthorizationGrant) {
|
@MainActor private func saveAccount(for grant: OAuthAuthorizationGrant) {
|
||||||
guard !AccountManager.shared.duplicateServiceAccount(type: .feedly, username: grant.accessToken.username) else {
|
guard !AccountManager.shared.duplicateServiceAccount(type: .feedly, username: grant.accessToken.username) else {
|
||||||
didFinish(OAuthAccountAuthorizationOperationError.duplicateAccount)
|
didFinish(OAuthAccountAuthorizationOperationError.duplicateAccount)
|
||||||
return
|
return
|
||||||
|
@ -12,226 +12,144 @@ import Account
|
|||||||
|
|
||||||
struct AppAssets {
|
struct AppAssets {
|
||||||
|
|
||||||
static var accountBazQux: RSImage! = {
|
static let accountBazQux = RSImage(named: "accountBazQux")
|
||||||
return RSImage(named: "accountBazQux")
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var accountCloudKit: RSImage! = {
|
static let accountCloudKit = RSImage(named: "accountCloudKit")
|
||||||
return RSImage(named: "accountCloudKit")
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var accountFeedbin: RSImage! = {
|
static let accountFeedbin = RSImage(named: "accountFeedbin")
|
||||||
return RSImage(named: "accountFeedbin")
|
|
||||||
}()
|
static let accountFeedly = RSImage(named: "accountFeedly")
|
||||||
|
|
||||||
static var accountFeedly: RSImage! = {
|
static let accountFreshRSS = RSImage(named: "accountFreshRSS")
|
||||||
return RSImage(named: "accountFeedly")
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var accountFreshRSS: RSImage! = {
|
|
||||||
return RSImage(named: "accountFreshRSS")
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var accountInoreader: RSImage! = {
|
static let accountInoreader = RSImage(named: "accountInoreader")
|
||||||
return RSImage(named: "accountInoreader")
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var accountLocal: RSImage! = {
|
static let accountLocal = RSImage(named: "accountLocal")
|
||||||
return RSImage(named: "accountLocal")
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var accountNewsBlur: RSImage! = {
|
static let accountNewsBlur = RSImage(named: "accountNewsBlur")
|
||||||
return RSImage(named: "accountNewsBlur")
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var accountTheOldReader: RSImage! = {
|
|
||||||
return RSImage(named: "accountTheOldReader")
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var addNewSidebarItemImage: RSImage = {
|
static let accountTheOldReader = RSImage(named: "accountTheOldReader")
|
||||||
return NSImage(systemSymbolName: "plus", accessibilityDescription: nil)!
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var articleExtractorError: RSImage = {
|
static let addNewSidebarItemImage = NSImage(systemSymbolName: "plus", accessibilityDescription: nil)!
|
||||||
return RSImage(named: "articleExtractorError")!
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var articleExtractorOff: RSImage = {
|
static let articleExtractorError = RSImage(named: "articleExtractorError")!
|
||||||
return RSImage(named: "articleExtractorOff")!
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var articleExtractorOn: RSImage = {
|
static let articleExtractorOff = RSImage(named: "articleExtractorOff")!
|
||||||
return RSImage(named: "articleExtractorOn")!
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var articleTheme: RSImage = {
|
static let articleExtractorOn = RSImage(named: "articleExtractorOn")!
|
||||||
return NSImage(systemSymbolName: "doc.richtext", accessibilityDescription: nil)!
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var cleanUpImage: RSImage = {
|
static let articleTheme = NSImage(systemSymbolName: "doc.richtext", accessibilityDescription: nil)!
|
||||||
return NSImage(systemSymbolName: "wind", accessibilityDescription: nil)!
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var marsEditIcon: RSImage = {
|
static let cleanUpImage = NSImage(systemSymbolName: "wind", accessibilityDescription: nil)!
|
||||||
return RSImage(named: "MarsEditIcon")!
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var microblogIcon: RSImage = {
|
|
||||||
return RSImage(named: "MicroblogIcon")!
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var faviconTemplateImage: RSImage = {
|
|
||||||
return RSImage(named: "faviconTemplateImage")!
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var filterActive = NSImage(systemSymbolName: "line.horizontal.3.decrease.circle.fill", accessibilityDescription: nil)!
|
static let marsEditIcon = RSImage(named: "MarsEditIcon")!
|
||||||
|
|
||||||
static var filterInactive = NSImage(systemSymbolName: "line.horizontal.3.decrease.circle", accessibilityDescription: nil)!
|
static let microblogIcon = RSImage(named: "MicroblogIcon")!
|
||||||
|
|
||||||
static var iconLightBackgroundColor: NSColor = {
|
static let faviconTemplateImage = RSImage(named: "faviconTemplateImage")!
|
||||||
return NSColor(named: NSColor.Name("iconLightBackgroundColor"))!
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var iconDarkBackgroundColor: NSColor = {
|
static let filterActive = NSImage(systemSymbolName: "line.horizontal.3.decrease.circle.fill", accessibilityDescription: nil)!
|
||||||
return NSColor(named: NSColor.Name("iconDarkBackgroundColor"))!
|
|
||||||
}()
|
static let filterInactive = NSImage(systemSymbolName: "line.horizontal.3.decrease.circle", accessibilityDescription: nil)!
|
||||||
|
|
||||||
static var legacyArticleExtractor: RSImage! = {
|
static let iconLightBackgroundColor = NSColor(named: NSColor.Name("iconLightBackgroundColor"))!
|
||||||
return RSImage(named: "legacyArticleExtractor")
|
|
||||||
}()
|
static let iconDarkBackgroundColor = NSColor(named: NSColor.Name("iconDarkBackgroundColor"))!
|
||||||
|
|
||||||
static var legacyArticleExtractorError: RSImage! = {
|
static let legacyArticleExtractor = RSImage(named: "legacyArticleExtractor")!
|
||||||
return RSImage(named: "legacyArticleExtractorError")
|
|
||||||
}()
|
static let legacyArticleExtractorError = RSImage(named: "legacyArticleExtractorError")!
|
||||||
|
|
||||||
static var legacyArticleExtractorInactiveDark: RSImage! = {
|
static let legacyArticleExtractorInactiveDark = RSImage(named: "legacyArticleExtractorInactiveDark")!
|
||||||
return RSImage(named: "legacyArticleExtractorInactiveDark")
|
|
||||||
}()
|
static let legacyArticleExtractorInactiveLight = RSImage(named: "legacyArticleExtractorInactiveLight")!
|
||||||
|
|
||||||
static var legacyArticleExtractorInactiveLight: RSImage! = {
|
static let legacyArticleExtractorProgress1 = RSImage(named: "legacyArticleExtractorProgress1")
|
||||||
return RSImage(named: "legacyArticleExtractorInactiveLight")
|
|
||||||
}()
|
static let legacyArticleExtractorProgress2 = RSImage(named: "legacyArticleExtractorProgress2")
|
||||||
|
|
||||||
static var legacyArticleExtractorProgress1: RSImage! = {
|
static let legacyArticleExtractorProgress3 = RSImage(named: "legacyArticleExtractorProgress3")
|
||||||
return RSImage(named: "legacyArticleExtractorProgress1")
|
|
||||||
}()
|
static let legacyArticleExtractorProgress4 = RSImage(named: "legacyArticleExtractorProgress4")
|
||||||
|
|
||||||
static var legacyArticleExtractorProgress2: RSImage! = {
|
static let folderImage: IconImage = {
|
||||||
return RSImage(named: "legacyArticleExtractorProgress2")
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var legacyArticleExtractorProgress3: RSImage! = {
|
|
||||||
return RSImage(named: "legacyArticleExtractorProgress3")
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var legacyArticleExtractorProgress4: RSImage! = {
|
|
||||||
return RSImage(named: "legacyArticleExtractorProgress4")
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var folderImage: IconImage = {
|
|
||||||
let image = NSImage(systemSymbolName: "folder", accessibilityDescription: nil)!
|
let image = NSImage(systemSymbolName: "folder", accessibilityDescription: nil)!
|
||||||
let preferredColor = NSColor(named: "AccentColor")!
|
let preferredColor = NSColor(named: "AccentColor")!
|
||||||
let coloredImage = image.tinted(with: preferredColor)
|
let coloredImage = image.tinted(with: preferredColor)
|
||||||
return IconImage(coloredImage, isSymbol: true, isBackgroundSupressed: true, preferredColor: preferredColor.cgColor)
|
return IconImage(coloredImage, isSymbol: true, isBackgroundSupressed: true, preferredColor: preferredColor.cgColor)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
static var markAllAsReadImage: RSImage = {
|
static let markAllAsReadImage = RSImage(named: "markAllAsRead")!
|
||||||
return RSImage(named: "markAllAsRead")!
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var nextUnreadImage: RSImage = {
|
static let nextUnreadImage = NSImage(systemSymbolName: "chevron.down.circle", accessibilityDescription: nil)!
|
||||||
return NSImage(systemSymbolName: "chevron.down.circle", accessibilityDescription: nil)!
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var openInBrowserImage: RSImage = {
|
static let openInBrowserImage = NSImage(systemSymbolName: "safari", accessibilityDescription: nil)!
|
||||||
return NSImage(systemSymbolName: "safari", accessibilityDescription: nil)!
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var preferencesToolbarAccountsImage = NSImage(systemSymbolName: "at", accessibilityDescription: nil)!
|
static let preferencesToolbarAccountsImage = NSImage(systemSymbolName: "at", accessibilityDescription: nil)!
|
||||||
|
|
||||||
static var preferencesToolbarGeneralImage = NSImage(systemSymbolName: "gearshape", accessibilityDescription: nil)!
|
static let preferencesToolbarGeneralImage = NSImage(systemSymbolName: "gearshape", accessibilityDescription: nil)!
|
||||||
|
|
||||||
static var preferencesToolbarAdvancedImage = NSImage(systemSymbolName: "gearshape.2", accessibilityDescription: nil)!
|
static let preferencesToolbarAdvancedImage = NSImage(systemSymbolName: "gearshape.2", accessibilityDescription: nil)!
|
||||||
|
|
||||||
static var readClosedImage = NSImage(systemSymbolName: "largecircle.fill.circle", accessibilityDescription: nil)!
|
static let readClosedImage = NSImage(systemSymbolName: "largecircle.fill.circle", accessibilityDescription: nil)!
|
||||||
|
|
||||||
static var readOpenImage: RSImage = {
|
static let readOpenImage = NSImage(systemSymbolName: "circle", accessibilityDescription: nil)!
|
||||||
return NSImage(systemSymbolName: "circle", accessibilityDescription: nil)!
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var refreshImage: RSImage = {
|
static let refreshImage = NSImage(systemSymbolName: "arrow.clockwise", accessibilityDescription: nil)!
|
||||||
return NSImage(systemSymbolName: "arrow.clockwise", accessibilityDescription: nil)!
|
|
||||||
}()
|
static let searchFeedImage: IconImage = {
|
||||||
|
|
||||||
static var searchFeedImage: IconImage = {
|
|
||||||
return IconImage(RSImage(named: NSImage.smartBadgeTemplateName)!, isSymbol: true, isBackgroundSupressed: true)
|
return IconImage(RSImage(named: NSImage.smartBadgeTemplateName)!, isSymbol: true, isBackgroundSupressed: true)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
static var shareImage: RSImage = {
|
static let shareImage = NSImage(systemSymbolName: "square.and.arrow.up", accessibilityDescription: nil)!
|
||||||
return NSImage(systemSymbolName: "square.and.arrow.up", accessibilityDescription: nil)!
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var sidebarToggleImage: RSImage = {
|
static let sidebarToggleImage = NSImage(systemSymbolName: "sidebar.left", accessibilityDescription: nil)!
|
||||||
return NSImage(systemSymbolName: "sidebar.left", accessibilityDescription: nil)!
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var starClosedImage: RSImage = {
|
|
||||||
return NSImage(systemSymbolName: "star.fill", accessibilityDescription: nil)!
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var starOpenImage: RSImage = {
|
static let starClosedImage = NSImage(systemSymbolName: "star.fill", accessibilityDescription: nil)!
|
||||||
return NSImage(systemSymbolName: "star", accessibilityDescription: nil)!
|
|
||||||
}()
|
static let starOpenImage = NSImage(systemSymbolName: "star", accessibilityDescription: nil)!
|
||||||
|
|
||||||
static var starredFeedImage: IconImage = {
|
static let starredFeedImage: IconImage = {
|
||||||
let image = NSImage(systemSymbolName: "star.fill", accessibilityDescription: nil)!
|
let image = NSImage(systemSymbolName: "star.fill", accessibilityDescription: nil)!
|
||||||
let preferredColor = NSColor(named: "StarColor")!
|
let preferredColor = NSColor(named: "StarColor")!
|
||||||
let coloredImage = image.tinted(with: preferredColor)
|
let coloredImage = image.tinted(with: preferredColor)
|
||||||
return IconImage(coloredImage, isSymbol: true, isBackgroundSupressed: true, preferredColor: preferredColor.cgColor)
|
return IconImage(coloredImage, isSymbol: true, isBackgroundSupressed: true, preferredColor: preferredColor.cgColor)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
static var timelineSeparatorColor: NSColor = {
|
static let timelineSeparatorColor = NSColor(named: "timelineSeparatorColor")!
|
||||||
return NSColor(named: "timelineSeparatorColor")!
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var timelineStarSelected: RSImage! = {
|
|
||||||
return RSImage(named: "timelineStar")?.tinted(with: .white)
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var timelineStarUnselected: RSImage! = {
|
static let timelineStarSelected = RSImage(named: "timelineStar")?.tinted(with: .white)
|
||||||
return RSImage(named: "timelineStar")?.tinted(with: starColor)
|
|
||||||
}()
|
|
||||||
|
|
||||||
static var todayFeedImage: IconImage = {
|
static let timelineStarUnselected = RSImage(named: "timelineStar")?.tinted(with: starColor)
|
||||||
|
|
||||||
|
static let todayFeedImage: IconImage = {
|
||||||
let image = NSImage(systemSymbolName: "sun.max.fill", accessibilityDescription: nil)!
|
let image = NSImage(systemSymbolName: "sun.max.fill", accessibilityDescription: nil)!
|
||||||
let preferredColor = NSColor.orange
|
let preferredColor = NSColor.orange
|
||||||
let coloredImage = image.tinted(with: preferredColor)
|
let coloredImage = image.tinted(with: preferredColor)
|
||||||
return IconImage(coloredImage, isSymbol: true, isBackgroundSupressed: true, preferredColor: preferredColor.cgColor)
|
return IconImage(coloredImage, isSymbol: true, isBackgroundSupressed: true, preferredColor: preferredColor.cgColor)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
static var unreadFeedImage: IconImage = {
|
static let unreadFeedImage: IconImage = {
|
||||||
let image = NSImage(systemSymbolName: "largecircle.fill.circle", accessibilityDescription: nil)!
|
let image = NSImage(systemSymbolName: "largecircle.fill.circle", accessibilityDescription: nil)!
|
||||||
let preferredColor = NSColor(named: "AccentColor")!
|
let preferredColor = NSColor(named: "AccentColor")!
|
||||||
let coloredImage = image.tinted(with: preferredColor)
|
let coloredImage = image.tinted(with: preferredColor)
|
||||||
return IconImage(coloredImage, isSymbol: true, isBackgroundSupressed: true, preferredColor: preferredColor.cgColor)
|
return IconImage(coloredImage, isSymbol: true, isBackgroundSupressed: true, preferredColor: preferredColor.cgColor)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
static var swipeMarkReadImage = RSImage(systemSymbolName: "circle", accessibilityDescription: "Mark Read")!
|
static let swipeMarkReadImage = RSImage(systemSymbolName: "circle", accessibilityDescription: "Mark Read")!
|
||||||
.withSymbolConfiguration(.init(scale: .large))
|
.withSymbolConfiguration(.init(scale: .large))
|
||||||
|
|
||||||
static var swipeMarkUnreadImage = RSImage(systemSymbolName: "largecircle.fill.circle", accessibilityDescription: "Mark Unread")!
|
static let swipeMarkUnreadImage = RSImage(systemSymbolName: "largecircle.fill.circle", accessibilityDescription: "Mark Unread")!
|
||||||
.withSymbolConfiguration(.init(scale: .large))
|
.withSymbolConfiguration(.init(scale: .large))
|
||||||
|
|
||||||
static var swipeMarkStarredImage = RSImage(systemSymbolName: "star.fill", accessibilityDescription: "Star")!
|
static let swipeMarkStarredImage = RSImage(systemSymbolName: "star.fill", accessibilityDescription: "Star")!
|
||||||
.withSymbolConfiguration(.init(scale: .large))
|
.withSymbolConfiguration(.init(scale: .large))
|
||||||
|
|
||||||
static var swipeMarkUnstarredImage = RSImage(systemSymbolName: "star", accessibilityDescription: "Unstar")!
|
static let swipeMarkUnstarredImage = RSImage(systemSymbolName: "star", accessibilityDescription: "Unstar")!
|
||||||
.withSymbolConfiguration(.init(scale: .large))!
|
.withSymbolConfiguration(.init(scale: .large))!
|
||||||
|
|
||||||
static var starColor: NSColor = {
|
static let starColor = NSColor(named: NSColor.Name("StarColor"))!
|
||||||
return NSColor(named: NSColor.Name("StarColor"))!
|
|
||||||
}()
|
|
||||||
|
|
||||||
static func image(for accountType: AccountType) -> NSImage? {
|
static func image(for accountType: AccountType) -> NSImage? {
|
||||||
switch accountType {
|
switch accountType {
|
||||||
case .onMyMac:
|
case .onMyMac:
|
||||||
@ -254,5 +172,4 @@ struct AppAssets {
|
|||||||
return AppAssets.accountTheOldReader
|
return AppAssets.accountTheOldReader
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -15,8 +15,8 @@ enum FontSize: Int {
|
|||||||
case veryLarge = 3
|
case veryLarge = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
final class AppDefaults {
|
final class AppDefaults: Sendable {
|
||||||
|
|
||||||
static let defaultThemeName = "Default"
|
static let defaultThemeName = "Default"
|
||||||
|
|
||||||
static let shared = AppDefaults()
|
static let shared = AppDefaults()
|
||||||
@ -66,7 +66,7 @@ final class AppDefaults {
|
|||||||
return false
|
return false
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var isFirstRun: Bool = {
|
let isFirstRun: Bool = {
|
||||||
if let _ = UserDefaults.standard.object(forKey: Key.firstRunDate) as? Date {
|
if let _ = UserDefaults.standard.object(forKey: Key.firstRunDate) as? Date {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -27,10 +27,10 @@ protocol SPUUpdaterDelegate {}
|
|||||||
import Sparkle
|
import Sparkle
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var appDelegate: AppDelegate!
|
@MainActor var appDelegate: AppDelegate!
|
||||||
|
|
||||||
@NSApplicationMain
|
@NSApplicationMain
|
||||||
final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, UNUserNotificationCenterDelegate, UnreadCountProvider, SPUStandardUserDriverDelegate, SPUUpdaterDelegate {
|
@MainActor final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, UNUserNotificationCenterDelegate, UnreadCountProvider, SPUStandardUserDriverDelegate, SPUUpdaterDelegate {
|
||||||
|
|
||||||
private struct WindowRestorationIdentifiers {
|
private struct WindowRestorationIdentifiers {
|
||||||
static let mainWindow = "mainWindow"
|
static let mainWindow = "mainWindow"
|
||||||
@ -109,9 +109,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
|||||||
|
|
||||||
private var themeImportPath: String?
|
private var themeImportPath: String?
|
||||||
private let secretsProvider = Secrets()
|
private let secretsProvider = Secrets()
|
||||||
|
private let accountManager: AccountManager
|
||||||
|
private let articleThemesManager: ArticleThemesManager
|
||||||
|
|
||||||
|
@MainActor override init() {
|
||||||
|
|
||||||
override init() {
|
|
||||||
NSWindow.allowsAutomaticWindowTabbing = false
|
NSWindow.allowsAutomaticWindowTabbing = false
|
||||||
|
|
||||||
|
self.accountManager = AccountManager(accountsFolder: Platform.dataSubfolder(forApplication: nil, folderName: "Accounts")!, secretsProvider: secretsProvider)
|
||||||
|
AccountManager.shared = self.accountManager
|
||||||
|
|
||||||
|
self.articleThemesManager = ArticleThemesManager(folderPath: Platform.dataSubfolder(forApplication: nil, folderName: "Themes")!)
|
||||||
|
ArticleThemesManager.shared = self.articleThemesManager
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
#if !MAC_APP_STORE
|
#if !MAC_APP_STORE
|
||||||
@ -120,9 +130,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
|||||||
crashReporter.enable()
|
crashReporter.enable()
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
AccountManager.shared = AccountManager(accountsFolder: Platform.dataSubfolder(forApplication: nil, folderName: "Accounts")!, secretsProvider: secretsProvider)
|
|
||||||
ArticleThemesManager.shared = ArticleThemesManager(folderPath: Platform.dataSubfolder(forApplication: nil, folderName: "Themes")!)
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
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(inspectableObjectsDidChange(_:)), name: .InspectableObjectsDidChange, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(importDownloadedTheme(_:)), name: .didEndDownloadingTheme, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(importDownloadedTheme(_:)), name: .didEndDownloadingTheme, object: nil)
|
||||||
@ -199,9 +206,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
|||||||
if isFirstRun {
|
if isFirstRun {
|
||||||
os_log(.debug, "Is first run.")
|
os_log(.debug, "Is first run.")
|
||||||
}
|
}
|
||||||
let localAccount = AccountManager.shared.defaultAccount
|
let localAccount = accountManager.defaultAccount
|
||||||
|
|
||||||
if isFirstRun && !AccountManager.shared.anyAccountHasAtLeastOneFeed() {
|
if isFirstRun && !accountManager.anyAccountHasAtLeastOneFeed() {
|
||||||
// Import feeds. Either old NNW 3 feeds or the default feeds.
|
// Import feeds. Either old NNW 3 feeds or the default feeds.
|
||||||
if !NNW3ImportController.importSubscriptionsIfFileExists(account: localAccount) {
|
if !NNW3ImportController.importSubscriptionsIfFileExists(account: localAccount) {
|
||||||
DefaultFeedsImporter.importDefaultFeeds(account: localAccount)
|
DefaultFeedsImporter.importDefaultFeeds(account: localAccount)
|
||||||
@ -223,8 +230,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
|||||||
NotificationCenter.default.addObserver(self, selector: #selector(feedSettingDidChange(_:)), name: .FeedSettingDidChange, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(feedSettingDidChange(_:)), name: .FeedSettingDidChange, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
Task { @MainActor in
|
||||||
self.unreadCount = AccountManager.shared.unreadCount
|
self.unreadCount = self.accountManager.unreadCount
|
||||||
}
|
}
|
||||||
|
|
||||||
if InspectorWindowController.shouldOpenAtStartup {
|
if InspectorWindowController.shouldOpenAtStartup {
|
||||||
@ -241,7 +248,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
|||||||
|
|
||||||
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
|
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
|
||||||
if settings.authorizationStatus == .authorized {
|
if settings.authorizationStatus == .authorized {
|
||||||
DispatchQueue.main.async {
|
Task { @MainActor in
|
||||||
NSApplication.shared.registerForRemoteNotifications()
|
NSApplication.shared.registerForRemoteNotifications()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -258,7 +265,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
|||||||
refreshTimer!.update()
|
refreshTimer!.update()
|
||||||
syncTimer!.update()
|
syncTimer!.update()
|
||||||
} else {
|
} else {
|
||||||
DispatchQueue.main.async {
|
Task { @MainActor in
|
||||||
self.refreshTimer!.timedRefresh(nil)
|
self.refreshTimer!.timedRefresh(nil)
|
||||||
self.syncTimer!.timedRefresh(nil)
|
self.syncTimer!.timedRefresh(nil)
|
||||||
}
|
}
|
||||||
@ -279,7 +286,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if !MAC_APP_STORE
|
#if !MAC_APP_STORE
|
||||||
DispatchQueue.main.async {
|
Task { @MainActor in
|
||||||
CrashReporter.check(crashReporter: self.crashReporter)
|
CrashReporter.check(crashReporter: self.crashReporter)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@ -318,7 +325,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
|||||||
}
|
}
|
||||||
|
|
||||||
func application(_ application: NSApplication, didReceiveRemoteNotification userInfo: [String : Any]) {
|
func application(_ application: NSApplication, didReceiveRemoteNotification userInfo: [String : Any]) {
|
||||||
AccountManager.shared.receiveRemoteNotification(userInfo: userInfo)
|
accountManager.receiveRemoteNotification(userInfo: userInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
func application(_ sender: NSApplication, openFile filename: String) -> Bool {
|
func application(_ sender: NSApplication, openFile filename: String) -> Bool {
|
||||||
@ -333,7 +340,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
|||||||
|
|
||||||
ArticleThemeDownloader.shared.cleanUp()
|
ArticleThemeDownloader.shared.cleanUp()
|
||||||
|
|
||||||
AccountManager.shared.sendArticleStatusAll() {
|
accountManager.sendArticleStatusAll() {
|
||||||
self.isShutDownSyncDone = true
|
self.isShutDownSyncDone = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,7 +351,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
|||||||
// MARK: Notifications
|
// MARK: Notifications
|
||||||
@objc func unreadCountDidChange(_ note: Notification) {
|
@objc func unreadCountDidChange(_ note: Notification) {
|
||||||
if note.object is AccountManager {
|
if note.object is AccountManager {
|
||||||
unreadCount = AccountManager.shared.unreadCount
|
unreadCount = accountManager.unreadCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -385,7 +392,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
|||||||
let url = userInfo["url"] as? URL else {
|
let url = userInfo["url"] as? URL else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
DispatchQueue.main.async {
|
Task { @MainActor in
|
||||||
self.importTheme(filename: url.path)
|
self.importTheme(filename: url.path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -444,15 +451,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
|||||||
let isDisplayingSheet = mainWindowController?.isDisplayingSheet ?? false
|
let isDisplayingSheet = mainWindowController?.isDisplayingSheet ?? false
|
||||||
|
|
||||||
if item.action == #selector(refreshAll(_:)) {
|
if item.action == #selector(refreshAll(_:)) {
|
||||||
return !AccountManager.shared.refreshInProgress && !AccountManager.shared.activeAccounts.isEmpty
|
return !accountManager.refreshInProgress && !accountManager.activeAccounts.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.action == #selector(importOPMLFromFile(_:)) {
|
if item.action == #selector(importOPMLFromFile(_:)) {
|
||||||
return AccountManager.shared.activeAccounts.contains(where: { !$0.behaviors.contains(where: { $0 == .disallowOPMLImports }) })
|
return accountManager.activeAccounts.contains(where: { !$0.behaviors.contains(where: { $0 == .disallowOPMLImports }) })
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.action == #selector(addAppNews(_:)) {
|
if item.action == #selector(addAppNews(_:)) {
|
||||||
return !isDisplayingSheet && !AccountManager.shared.anyAccountHasNetNewsWireNewsSubscription() && !AccountManager.shared.activeAccounts.isEmpty
|
return !isDisplayingSheet && !accountManager.anyAccountHasNetNewsWireNewsSubscription() && !accountManager.activeAccounts.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.action == #selector(sortByNewestArticleOnTop(_:)) || item.action == #selector(sortByOldestArticleOnTop(_:)) {
|
if item.action == #selector(sortByNewestArticleOnTop(_:)) || item.action == #selector(sortByOldestArticleOnTop(_:)) {
|
||||||
@ -460,7 +467,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
|||||||
}
|
}
|
||||||
|
|
||||||
if item.action == #selector(showAddFeedWindow(_:)) || item.action == #selector(showAddFolderWindow(_:)) {
|
if item.action == #selector(showAddFeedWindow(_:)) || item.action == #selector(showAddFolderWindow(_:)) {
|
||||||
return !isDisplayingSheet && !AccountManager.shared.activeAccounts.isEmpty
|
return !isDisplayingSheet && !accountManager.activeAccounts.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !MAC_APP_STORE
|
#if !MAC_APP_STORE
|
||||||
@ -474,23 +481,31 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
|||||||
|
|
||||||
// MARK: UNUserNotificationCenterDelegate
|
// MARK: UNUserNotificationCenterDelegate
|
||||||
|
|
||||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||||
completionHandler([.banner, .badge, .sound])
|
completionHandler([.banner, .badge, .sound])
|
||||||
}
|
}
|
||||||
|
|
||||||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||||
|
|
||||||
let userInfo = response.notification.request.content.userInfo
|
let userInfo = response.notification.request.content.userInfo
|
||||||
|
guard let articlePathInfo = ArticlePathInfo(userInfo: userInfo) else {
|
||||||
switch response.actionIdentifier {
|
completionHandler()
|
||||||
case "MARK_AS_READ":
|
return
|
||||||
handleMarkAsRead(userInfo: userInfo)
|
}
|
||||||
case "MARK_AS_STARRED":
|
|
||||||
handleMarkAsStarred(userInfo: userInfo)
|
let actionIdentifier = response.actionIdentifier
|
||||||
default:
|
|
||||||
mainWindowController?.handle(response)
|
Task { @MainActor in
|
||||||
|
switch actionIdentifier {
|
||||||
|
case "MARK_AS_READ":
|
||||||
|
handleMarkAsRead(articlePathInfo: articlePathInfo)
|
||||||
|
case "MARK_AS_STARRED":
|
||||||
|
handleMarkAsStarred(articlePathInfo: articlePathInfo)
|
||||||
|
default:
|
||||||
|
mainWindowController?.handle(articlePathInfo: articlePathInfo)
|
||||||
|
}
|
||||||
|
completionHandler()
|
||||||
}
|
}
|
||||||
completionHandler()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Add Feed
|
// MARK: Add Feed
|
||||||
@ -529,7 +544,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
|||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func refreshAll(_ sender: Any?) {
|
@IBAction func refreshAll(_ sender: Any?) {
|
||||||
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present)
|
accountManager.refreshAll(errorHandler: ErrorHandler.present)
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func showAddFeedWindow(_ sender: Any?) {
|
@IBAction func showAddFeedWindow(_ sender: Any?) {
|
||||||
@ -603,7 +618,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
|||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func addAppNews(_ sender: Any?) {
|
@IBAction func addAppNews(_ sender: Any?) {
|
||||||
if AccountManager.shared.anyAccountHasNetNewsWireNewsSubscription() {
|
if accountManager.anyAccountHasNetNewsWireNewsSubscription() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
addFeed(AccountManager.netNewsWireNewsURL, name: "NetNewsWire News")
|
addFeed(AccountManager.netNewsWireNewsURL, name: "NetNewsWire News")
|
||||||
@ -700,12 +715,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
|
|||||||
extension AppDelegate {
|
extension AppDelegate {
|
||||||
|
|
||||||
@IBAction func debugSearch(_ sender: Any?) {
|
@IBAction func debugSearch(_ sender: Any?) {
|
||||||
AccountManager.shared.defaultAccount.debugRunSearch()
|
accountManager.defaultAccount.debugRunSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func debugDropConditionalGetInfo(_ sender: Any?) {
|
@IBAction func debugDropConditionalGetInfo(_ sender: Any?) {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
AccountManager.shared.activeAccounts.forEach{ $0.debugDropConditionalGetInfo() }
|
accountManager.activeAccounts.forEach{ $0.debugDropConditionalGetInfo() }
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -817,7 +832,7 @@ internal extension AppDelegate {
|
|||||||
|
|
||||||
func importTheme() {
|
func importTheme() {
|
||||||
do {
|
do {
|
||||||
try ArticleThemesManager.shared.importTheme(filename: filename)
|
try articleThemesManager.importTheme(filename: filename)
|
||||||
confirmImportSuccess(themeName: theme.name)
|
confirmImportSuccess(themeName: theme.name)
|
||||||
} catch {
|
} catch {
|
||||||
NSApplication.shared.presentError(error)
|
NSApplication.shared.presentError(error)
|
||||||
@ -827,7 +842,7 @@ internal extension AppDelegate {
|
|||||||
alert.beginSheetModal(for: window) { result in
|
alert.beginSheetModal(for: window) { result in
|
||||||
if result == NSApplication.ModalResponse.alertFirstButtonReturn {
|
if result == NSApplication.ModalResponse.alertFirstButtonReturn {
|
||||||
|
|
||||||
if ArticleThemesManager.shared.themeExists(filename: filename) {
|
if self.articleThemesManager.themeExists(filename: filename) {
|
||||||
let alert = NSAlert()
|
let alert = NSAlert()
|
||||||
alert.alertStyle = .warning
|
alert.alertStyle = .warning
|
||||||
|
|
||||||
@ -901,14 +916,14 @@ internal extension AppDelegate {
|
|||||||
informativeText = error.localizedDescription
|
informativeText = error.localizedDescription
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
Task { @MainActor in
|
||||||
let alert = NSAlert()
|
let alert = NSAlert()
|
||||||
alert.alertStyle = .warning
|
alert.alertStyle = .warning
|
||||||
alert.messageText = NSLocalizedString("Theme Error", comment: "Theme download error")
|
alert.messageText = NSLocalizedString("Theme Error", comment: "Theme download error")
|
||||||
alert.informativeText = informativeText
|
alert.informativeText = informativeText
|
||||||
alert.addButton(withTitle: NSLocalizedString("Open Theme Folder", comment: "Open Theme Folder"))
|
alert.addButton(withTitle: NSLocalizedString("Open Theme Folder", comment: "Open Theme Folder"))
|
||||||
alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK"))
|
alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK"))
|
||||||
|
|
||||||
let button = alert.buttons.first
|
let button = alert.buttons.first
|
||||||
button?.target = self
|
button?.target = self
|
||||||
button?.action = #selector(self.openThemesFolder(_:))
|
button?.action = #selector(self.openThemesFolder(_:))
|
||||||
@ -920,7 +935,7 @@ internal extension AppDelegate {
|
|||||||
|
|
||||||
@objc func openThemesFolder(_ sender: Any) {
|
@objc func openThemesFolder(_ sender: Any) {
|
||||||
if themeImportPath == nil {
|
if themeImportPath == nil {
|
||||||
let url = URL(fileURLWithPath: ArticleThemesManager.shared.folderPath)
|
let url = URL(fileURLWithPath: articleThemesManager.folderPath)
|
||||||
NSWorkspace.shared.open(url)
|
NSWorkspace.shared.open(url)
|
||||||
} else {
|
} else {
|
||||||
let url = URL(fileURLWithPath: themeImportPath!)
|
let url = URL(fileURLWithPath: themeImportPath!)
|
||||||
@ -968,16 +983,15 @@ extension AppDelegate: NSWindowRestoration {
|
|||||||
|
|
||||||
private extension AppDelegate {
|
private extension AppDelegate {
|
||||||
|
|
||||||
func handleMarkAsRead(userInfo: [AnyHashable: Any]) {
|
func handleMarkAsRead(articlePathInfo: ArticlePathInfo) {
|
||||||
|
|
||||||
guard let articlePathInfo = ArticlePathInfo(userInfo: userInfo) else {
|
guard let accountID = articlePathInfo.accountID, let account = accountManager.existingAccount(with: accountID) else {
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let account = AccountManager.shared.existingAccount(with: articlePathInfo.accountID) else {
|
|
||||||
os_log(.debug, "No account found from notification.")
|
os_log(.debug, "No account found from notification.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let articleID = articlePathInfo.articleID
|
guard let articleID = articlePathInfo.articleID else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
guard let articles = try? await account.articles(for: .articleIDs([articleID])) else {
|
guard let articles = try? await account.articles(for: .articleIDs([articleID])) else {
|
||||||
@ -989,16 +1003,15 @@ private extension AppDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleMarkAsStarred(userInfo: [AnyHashable: Any]) {
|
func handleMarkAsStarred(articlePathInfo: ArticlePathInfo) {
|
||||||
|
|
||||||
guard let articlePathInfo = ArticlePathInfo(userInfo: userInfo) else {
|
guard let accountID = articlePathInfo.accountID, let account = accountManager.existingAccount(with: accountID) else {
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let account = AccountManager.shared.existingAccount(with: articlePathInfo.accountID) else {
|
|
||||||
os_log(.debug, "No account found from notification.")
|
os_log(.debug, "No account found from notification.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let articleID = articlePathInfo.articleID
|
guard let articleID = articlePathInfo.articleID else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ import CrashReporter
|
|||||||
// At some point this code should probably move into RSCore, so Rainier and any other
|
// At some point this code should probably move into RSCore, so Rainier and any other
|
||||||
// future apps can use it.
|
// future apps can use it.
|
||||||
|
|
||||||
struct CrashReporter {
|
@MainActor struct CrashReporter {
|
||||||
|
|
||||||
struct DefaultsKey {
|
struct DefaultsKey {
|
||||||
static let sendCrashLogsAutomaticallyKey = "SendCrashLogsAutomatically"
|
static let sendCrashLogsAutomaticallyKey = "SendCrashLogsAutomatically"
|
||||||
|
@ -12,14 +12,14 @@ import os.log
|
|||||||
|
|
||||||
struct ErrorHandler {
|
struct ErrorHandler {
|
||||||
|
|
||||||
private static var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Account")
|
private static let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Account")
|
||||||
|
|
||||||
public static func present(_ error: Error) {
|
@MainActor public static func present(_ error: Error) {
|
||||||
NSApplication.shared.presentError(error)
|
NSApplication.shared.presentError(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func log(_ error: Error) {
|
public static func log(_ error: Error) {
|
||||||
os_log(.error, log: self.log, "%@", error.localizedDescription)
|
os_log(.error, log: log, "%@", error.localizedDescription)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
final class BuiltinSmartFeedInspectorViewController: NSViewController, Inspector {
|
@MainActor final class BuiltinSmartFeedInspectorViewController: NSViewController, Inspector {
|
||||||
|
|
||||||
@IBOutlet var nameTextField: NSTextField?
|
@IBOutlet var nameTextField: NSTextField?
|
||||||
@IBOutlet weak var smartFeedImageView: NSImageView!
|
@IBOutlet weak var smartFeedImageView: NSImageView!
|
||||||
|
@ -11,7 +11,7 @@ import Articles
|
|||||||
import Account
|
import Account
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
|
|
||||||
final class FeedInspectorViewController: NSViewController, Inspector {
|
@MainActor final class FeedInspectorViewController: NSViewController, Inspector {
|
||||||
|
|
||||||
@IBOutlet weak var iconView: IconView!
|
@IBOutlet weak var iconView: IconView!
|
||||||
@IBOutlet weak var nameTextField: NSTextField?
|
@IBOutlet weak var nameTextField: NSTextField?
|
||||||
|
@ -10,7 +10,7 @@ import AppKit
|
|||||||
import Account
|
import Account
|
||||||
import RSCore
|
import RSCore
|
||||||
|
|
||||||
final class FolderInspectorViewController: NSViewController, Inspector {
|
@MainActor final class FolderInspectorViewController: NSViewController, Inspector {
|
||||||
|
|
||||||
@IBOutlet var nameTextField: NSTextField?
|
@IBOutlet var nameTextField: NSTextField?
|
||||||
@IBOutlet weak var folderImageView: NSImageView!
|
@IBOutlet weak var folderImageView: NSImageView!
|
||||||
|
@ -10,11 +10,11 @@ import AppKit
|
|||||||
|
|
||||||
protocol Inspector: AnyObject {
|
protocol Inspector: AnyObject {
|
||||||
|
|
||||||
var objects: [Any]? { get set }
|
@MainActor var objects: [Any]? { get set }
|
||||||
var isFallbackInspector: Bool { get } // Can handle nothing-to-inspect or unexpected type of objects.
|
@MainActor var isFallbackInspector: Bool { get } // Can handle nothing-to-inspect or unexpected type of objects.
|
||||||
var windowTitle: String { get }
|
@MainActor var windowTitle: String { get }
|
||||||
|
|
||||||
func canInspect(_ objects: [Any]) -> Bool
|
@MainActor func canInspect(_ objects: [Any]) -> Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias InspectorViewController = Inspector & NSViewController
|
typealias InspectorViewController = Inspector & NSViewController
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
final class NothingInspectorViewController: NSViewController, Inspector {
|
@MainActor final class NothingInspectorViewController: NSViewController, Inspector {
|
||||||
|
|
||||||
@IBOutlet var nothingTextField: NSTextField?
|
@IBOutlet var nothingTextField: NSTextField?
|
||||||
@IBOutlet var multipleTextField: NSTextField?
|
@IBOutlet var multipleTextField: NSTextField?
|
||||||
|
@ -22,7 +22,7 @@ import RSParser
|
|||||||
// Else,
|
// Else,
|
||||||
// display error sheet.
|
// display error sheet.
|
||||||
|
|
||||||
class AddFeedController: AddFeedWindowControllerDelegate {
|
@MainActor final class AddFeedController: AddFeedWindowControllerDelegate {
|
||||||
|
|
||||||
private let hostWindow: NSWindow
|
private let hostWindow: NSWindow
|
||||||
private var addFeedWindowController: AddFeedWindowController?
|
private var addFeedWindowController: AddFeedWindowController?
|
||||||
|
@ -11,7 +11,7 @@ import RSCore
|
|||||||
import RSTree
|
import RSTree
|
||||||
import Account
|
import Account
|
||||||
|
|
||||||
class FolderTreeMenu {
|
@MainActor final class FolderTreeMenu {
|
||||||
|
|
||||||
static func createFolderPopupMenu(with rootNode: Node, restrictToSpecialAccounts: Bool = false) -> NSMenu {
|
static func createFolderPopupMenu(with rootNode: Node, restrictToSpecialAccounts: Bool = false) -> NSMenu {
|
||||||
|
|
||||||
|
@ -16,31 +16,32 @@ class DetailIconSchemeHandler: NSObject, WKURLSchemeHandler {
|
|||||||
|
|
||||||
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
|
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
|
||||||
|
|
||||||
guard let responseURL = urlSchemeTask.request.url, let iconImage = self.currentArticle?.iconImage() else {
|
Task { @MainActor in
|
||||||
urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let iconView = IconView(frame: CGRect(x: 0, y: 0, width: 48, height: 48))
|
guard let responseURL = urlSchemeTask.request.url, let iconImage = self.currentArticle?.iconImage() else {
|
||||||
iconView.iconImage = iconImage
|
urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist))
|
||||||
let renderedImage = iconView.asImage()
|
return
|
||||||
|
}
|
||||||
guard let data = renderedImage.dataRepresentation() else {
|
|
||||||
urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let headerFields = ["Cache-Control": "no-cache"]
|
|
||||||
if let response = HTTPURLResponse(url: responseURL, statusCode: 200, httpVersion: nil, headerFields: headerFields) {
|
|
||||||
urlSchemeTask.didReceive(response)
|
|
||||||
urlSchemeTask.didReceive(data)
|
|
||||||
urlSchemeTask.didFinish()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
let iconView = IconView(frame: CGRect(x: 0, y: 0, width: 48, height: 48))
|
||||||
|
iconView.iconImage = iconImage
|
||||||
|
let renderedImage = iconView.asImage()
|
||||||
|
|
||||||
|
guard let data = renderedImage.dataRepresentation() else {
|
||||||
|
urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let headerFields = ["Cache-Control": "no-cache"]
|
||||||
|
if let response = HTTPURLResponse(url: responseURL, statusCode: 200, httpVersion: nil, headerFields: headerFields) {
|
||||||
|
urlSchemeTask.didReceive(response)
|
||||||
|
urlSchemeTask.didReceive(data)
|
||||||
|
urlSchemeTask.didFinish()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
|
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
|
||||||
urlSchemeTask.didFailWithError(URLError(.unknown))
|
urlSchemeTask.didFailWithError(URLError(.unknown))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -216,13 +216,26 @@ final class DetailWebViewController: NSViewController {
|
|||||||
|
|
||||||
extension DetailWebViewController: WKScriptMessageHandler {
|
extension DetailWebViewController: WKScriptMessageHandler {
|
||||||
|
|
||||||
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
nonisolated func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||||
|
|
||||||
if message.name == MessageName.windowDidScroll {
|
if message.name == MessageName.windowDidScroll {
|
||||||
windowScrollY = message.body as? CGFloat
|
|
||||||
|
let updatedWindowScrollY = message.body as? CGFloat
|
||||||
|
Task { @MainActor in
|
||||||
|
windowScrollY = updatedWindowScrollY
|
||||||
|
}
|
||||||
|
|
||||||
} else if message.name == MessageName.mouseDidEnter, let link = message.body as? String {
|
} else if message.name == MessageName.mouseDidEnter, let link = message.body as? String {
|
||||||
delegate?.mouseDidEnter(self, link: link)
|
|
||||||
|
Task { @MainActor in
|
||||||
|
delegate?.mouseDidEnter(self, link: link)
|
||||||
|
}
|
||||||
|
|
||||||
} else if message.name == MessageName.mouseDidExit {
|
} else if message.name == MessageName.mouseDidExit {
|
||||||
delegate?.mouseDidExit(self)
|
|
||||||
|
Task { @MainActor in
|
||||||
|
delegate?.mouseDidExit(self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -239,10 +252,13 @@ extension DetailWebViewController: WKNavigationDelegate, WKUIDelegate {
|
|||||||
|
|
||||||
// WKNavigationDelegate
|
// WKNavigationDelegate
|
||||||
|
|
||||||
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
nonisolated public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
||||||
if navigationAction.navigationType == .linkActivated {
|
if navigationAction.navigationType == .linkActivated {
|
||||||
if let url = navigationAction.request.url {
|
if let url = navigationAction.request.url {
|
||||||
self.openInBrowser(url, flags: navigationAction.modifierFlags)
|
let flags = navigationAction.modifierFlags
|
||||||
|
Task { @MainActor in
|
||||||
|
self.openInBrowser(url, flags: flags)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
decisionHandler(.cancel)
|
decisionHandler(.cancel)
|
||||||
return
|
return
|
||||||
@ -251,35 +267,42 @@ extension DetailWebViewController: WKNavigationDelegate, WKUIDelegate {
|
|||||||
decisionHandler(.allow)
|
decisionHandler(.allow)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
nonisolated public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||||
// See note in viewDidLoad()
|
|
||||||
if waitingForFirstReload {
|
|
||||||
assert(webView.isHidden)
|
|
||||||
waitingForFirstReload = false
|
|
||||||
reloadHTML()
|
|
||||||
|
|
||||||
// Waiting for the first navigation to complete isn't long enough to avoid the flash of white.
|
Task { @MainActor in
|
||||||
// A hard coded value is awful, but 5/100th of a second seems to be enough.
|
// See note in viewDidLoad()
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
if waitingForFirstReload {
|
||||||
webView.isHidden = false
|
assert(webView.isHidden)
|
||||||
}
|
waitingForFirstReload = false
|
||||||
} else {
|
reloadHTML()
|
||||||
if let windowScrollY = windowScrollY {
|
|
||||||
webView.evaluateJavaScript("window.scrollTo(0, \(windowScrollY));")
|
// Waiting for the first navigation to complete isn't long enough to avoid the flash of white.
|
||||||
self.windowScrollY = nil
|
// A hard coded value is awful, but 5/100th of a second seems to be enough.
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
||||||
|
webView.isHidden = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let windowScrollY = windowScrollY {
|
||||||
|
_ = try? await webView.evaluateJavaScript("window.scrollTo(0, \(windowScrollY));")
|
||||||
|
self.windowScrollY = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WKUIDelegate
|
// WKUIDelegate
|
||||||
|
|
||||||
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
|
nonisolated func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
|
||||||
// This method is reached when WebKit handles a JavaScript based window.open() invocation, for example. One
|
// This method is reached when WebKit handles a JavaScript based window.open() invocation, for example. One
|
||||||
// example where this is used is in YouTube's embedded video player when a user clicks on the video's title
|
// example where this is used is in YouTube's embedded video player when a user clicks on the video's title
|
||||||
// or on the "Watch in YouTube" button. For our purposes we'll handle such window.open calls the same way we
|
// or on the "Watch in YouTube" button. For our purposes we'll handle such window.open calls the same way we
|
||||||
// handle clicks on a URL.
|
// handle clicks on a URL.
|
||||||
if let url = navigationAction.request.url {
|
if let url = navigationAction.request.url {
|
||||||
self.openInBrowser(url, flags: navigationAction.modifierFlags)
|
let flags = navigationAction.modifierFlags
|
||||||
|
|
||||||
|
Task { @MainActor in
|
||||||
|
self.openInBrowser(url, flags: flags)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -115,16 +115,17 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
|||||||
return sidebarViewController?.selectedObjects
|
return sidebarViewController?.selectedObjects
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle(_ response: UNNotificationResponse) {
|
func handle(articlePathInfo: ArticlePathInfo) {
|
||||||
let userInfo = response.notification.request.content.userInfo
|
sidebarViewController?.deepLinkRevealAndSelect(for: articlePathInfo)
|
||||||
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any] else { return }
|
currentTimelineViewController?.goToDeepLink(for: articlePathInfo)
|
||||||
sidebarViewController?.deepLinkRevealAndSelect(for: articlePathUserInfo)
|
|
||||||
currentTimelineViewController?.goToDeepLink(for: articlePathUserInfo)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle(_ activity: NSUserActivity) {
|
func handle(_ activity: NSUserActivity) {
|
||||||
guard let userInfo = activity.userInfo else { return }
|
|
||||||
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any] else { return }
|
guard let userInfo = activity.userInfo, let articlePathUserInfo = ArticlePathInfo(userInfo: userInfo) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
sidebarViewController?.deepLinkRevealAndSelect(for: articlePathUserInfo)
|
sidebarViewController?.deepLinkRevealAndSelect(for: articlePathUserInfo)
|
||||||
currentTimelineViewController?.goToDeepLink(for: articlePathUserInfo)
|
currentTimelineViewController?.goToDeepLink(for: articlePathUserInfo)
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ import AppKit
|
|||||||
import Account
|
import Account
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
struct NNW3ImportController {
|
@MainActor struct NNW3ImportController {
|
||||||
|
|
||||||
/// Import NNW3 subscriptions if they exist.
|
/// Import NNW3 subscriptions if they exist.
|
||||||
/// Return true if Subscriptions.plist was found and subscriptions were imported.
|
/// Return true if Subscriptions.plist was found and subscriptions were imported.
|
||||||
|
@ -11,7 +11,7 @@ import RSCore
|
|||||||
|
|
||||||
// image - title - unreadCount
|
// image - title - unreadCount
|
||||||
|
|
||||||
struct SidebarCellLayout {
|
@MainActor struct SidebarCellLayout {
|
||||||
|
|
||||||
let faviconRect: CGRect
|
let faviconRect: CGRect
|
||||||
let titleRect: CGRect
|
let titleRect: CGRect
|
||||||
|
@ -10,7 +10,7 @@ import AppKit
|
|||||||
import RSTree
|
import RSTree
|
||||||
import Account
|
import Account
|
||||||
|
|
||||||
enum SidebarDeleteItemsAlert {
|
@MainActor struct SidebarDeleteItemsAlert {
|
||||||
|
|
||||||
/// Builds a delete confirmation dialog for the supplied nodes
|
/// Builds a delete confirmation dialog for the supplied nodes
|
||||||
static func build(_ nodes: [Node]) -> NSAlert {
|
static func build(_ nodes: [Node]) -> NSAlert {
|
||||||
|
@ -12,7 +12,7 @@ import Articles
|
|||||||
import RSCore
|
import RSCore
|
||||||
import Account
|
import Account
|
||||||
|
|
||||||
@objc final class SidebarOutlineDataSource: NSObject, NSOutlineViewDataSource {
|
@objc @MainActor final class SidebarOutlineDataSource: NSObject, NSOutlineViewDataSource {
|
||||||
|
|
||||||
let treeController: TreeController
|
let treeController: TreeController
|
||||||
static let dragOperationNone = NSDragOperation(rawValue: 0)
|
static let dragOperationNone = NSDragOperation(rawValue: 0)
|
||||||
|
@ -17,13 +17,13 @@ extension Notification.Name {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protocol SidebarDelegate: AnyObject {
|
protocol SidebarDelegate: AnyObject {
|
||||||
func sidebarSelectionDidChange(_: SidebarViewController, selectedObjects: [AnyObject]?)
|
@MainActor func sidebarSelectionDidChange(_: SidebarViewController, selectedObjects: [AnyObject]?)
|
||||||
func unreadCount(for: AnyObject) -> Int
|
@MainActor func unreadCount(for: AnyObject) -> Int
|
||||||
func sidebarInvalidatedRestorationState(_: SidebarViewController)
|
@MainActor func sidebarInvalidatedRestorationState(_: SidebarViewController)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc class SidebarViewController: NSViewController, NSOutlineViewDelegate, NSMenuDelegate, UndoableCommandRunner {
|
@objc @MainActor class SidebarViewController: NSViewController, NSOutlineViewDelegate, NSMenuDelegate, UndoableCommandRunner {
|
||||||
|
|
||||||
@IBOutlet weak var outlineView: NSOutlineView!
|
@IBOutlet weak var outlineView: NSOutlineView!
|
||||||
|
|
||||||
weak var delegate: SidebarDelegate?
|
weak var delegate: SidebarDelegate?
|
||||||
@ -483,9 +483,9 @@ protocol SidebarDelegate: AnyObject {
|
|||||||
revealAndSelectRepresentedObject(feed as AnyObject)
|
revealAndSelectRepresentedObject(feed as AnyObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deepLinkRevealAndSelect(for userInfo: [AnyHashable : Any]) {
|
func deepLinkRevealAndSelect(for articlePathInfo: ArticlePathInfo) {
|
||||||
guard let accountNode = findAccountNode(userInfo),
|
guard let accountNode = findAccountNode(articlePathInfo),
|
||||||
let feedNode = findFeedNode(userInfo, beginningAt: accountNode),
|
let feedNode = findFeedNode(articlePathInfo, beginningAt: accountNode),
|
||||||
let feed = feedNode.representedObject as? SidebarItem else {
|
let feed = feedNode.representedObject as? SidebarItem else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -738,16 +738,17 @@ private extension SidebarViewController {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func findAccountNode(_ userInfo: [AnyHashable : Any]?) -> Node? {
|
func findAccountNode(_ articlePathInfo: ArticlePathInfo) -> Node? {
|
||||||
guard let accountID = userInfo?[ArticlePathKey.accountID] as? String else {
|
|
||||||
|
guard let accountID = articlePathInfo.accountID else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if let node = treeController.rootNode.descendantNode(where: { ($0.representedObject as? Account)?.accountID == accountID }) {
|
if let node = treeController.rootNode.descendantNode(where: { ($0.representedObject as? Account)?.accountID == accountID }) {
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let accountName = userInfo?[ArticlePathKey.accountName] as? String else {
|
guard let accountName = articlePathInfo.accountName else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -758,8 +759,8 @@ private extension SidebarViewController {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func findFeedNode(_ userInfo: [AnyHashable : Any]?, beginningAt startingNode: Node) -> Node? {
|
func findFeedNode(_ articlePathInfo: ArticlePathInfo, beginningAt startingNode: Node) -> Node? {
|
||||||
guard let feedID = userInfo?[ArticlePathKey.feedID] as? String else {
|
guard let feedID = articlePathInfo.feedID else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if let node = startingNode.descendantNode(where: { ($0.representedObject as? Feed)?.feedID == feedID }) {
|
if let node = startingNode.descendantNode(where: { ($0.representedObject as? Feed)?.feedID == feedID }) {
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
class UnreadCountView : NSView {
|
@MainActor final class UnreadCountView : NSView {
|
||||||
|
|
||||||
struct Appearance {
|
struct Appearance {
|
||||||
static let padding = NSEdgeInsets(top: 1.0, left: 7.0, bottom: 1.0, right: 7.0)
|
static let padding = NSEdgeInsets(top: 1.0, left: 7.0, bottom: 1.0, right: 7.0)
|
||||||
|
@ -11,12 +11,13 @@ import Articles
|
|||||||
import RSCore
|
import RSCore
|
||||||
|
|
||||||
extension Article: PasteboardWriterOwner {
|
extension Article: PasteboardWriterOwner {
|
||||||
public var pasteboardWriter: NSPasteboardWriting {
|
|
||||||
|
@MainActor public var pasteboardWriter: NSPasteboardWriting {
|
||||||
return ArticlePasteboardWriter(article: self)
|
return ArticlePasteboardWriter(article: self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc final class ArticlePasteboardWriter: NSObject, NSPasteboardWriting {
|
@objc @MainActor final class ArticlePasteboardWriter: NSObject, NSPasteboardWriting {
|
||||||
|
|
||||||
let article: Article
|
let article: Article
|
||||||
static let articleUTI = "com.ranchero.article"
|
static let articleUTI = "com.ranchero.article"
|
||||||
|
@ -26,7 +26,7 @@ struct TextFieldSizeInfo {
|
|||||||
let numberOfLinesUsed: Int // A two-line text field may only use one line, for instance. This would equal 1, then.
|
let numberOfLinesUsed: Int // A two-line text field may only use one line, for instance. This would equal 1, then.
|
||||||
}
|
}
|
||||||
|
|
||||||
final class MultilineTextFieldSizer {
|
@MainActor final class MultilineTextFieldSizer {
|
||||||
|
|
||||||
private let numberOfLines: Int
|
private let numberOfLines: Int
|
||||||
private let font: NSFont
|
private let font: NSFont
|
||||||
@ -35,7 +35,7 @@ final class MultilineTextFieldSizer {
|
|||||||
private let doubleLineHeightEstimate: Int
|
private let doubleLineHeightEstimate: Int
|
||||||
private var cache = [String: WidthHeightCache]() // Each string has a cache.
|
private var cache = [String: WidthHeightCache]() // Each string has a cache.
|
||||||
private var attributedCache = [NSAttributedString: WidthHeightCache]()
|
private var attributedCache = [NSAttributedString: WidthHeightCache]()
|
||||||
private static var sizers = [TextFieldSizerSpecifier: MultilineTextFieldSizer]()
|
@MainActor private static var sizers = [TextFieldSizerSpecifier: MultilineTextFieldSizer]()
|
||||||
|
|
||||||
private init(numberOfLines: Int, font: NSFont) {
|
private init(numberOfLines: Int, font: NSFont) {
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ import AppKit
|
|||||||
// Uses a cache.
|
// Uses a cache.
|
||||||
// Main thready only.
|
// Main thready only.
|
||||||
|
|
||||||
final class SingleLineTextFieldSizer {
|
@MainActor final class SingleLineTextFieldSizer {
|
||||||
|
|
||||||
let font: NSFont
|
let font: NSFont
|
||||||
private let textField: NSTextField
|
private let textField: NSTextField
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
import AppKit
|
import AppKit
|
||||||
import Articles
|
import Articles
|
||||||
|
|
||||||
struct TimelineCellData {
|
@MainActor struct TimelineCellData {
|
||||||
|
|
||||||
private static let noText = NSLocalizedString("(No Text)", comment: "No Text")
|
private static let noText = NSLocalizedString("(No Text)", comment: "No Text")
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
import AppKit
|
import AppKit
|
||||||
import RSCore
|
import RSCore
|
||||||
|
|
||||||
struct TimelineCellLayout {
|
@MainActor struct TimelineCellLayout {
|
||||||
|
|
||||||
let width: CGFloat
|
let width: CGFloat
|
||||||
let height: CGFloat
|
let height: CGFloat
|
||||||
|
@ -11,10 +11,10 @@ import Account
|
|||||||
import Articles
|
import Articles
|
||||||
|
|
||||||
protocol TimelineContainerViewControllerDelegate: AnyObject {
|
protocol TimelineContainerViewControllerDelegate: AnyObject {
|
||||||
func timelineSelectionDidChange(_: TimelineContainerViewController, articles: [Article]?, mode: TimelineSourceMode)
|
|
||||||
func timelineRequestedFeedSelection(_: TimelineContainerViewController, feed: Feed)
|
@MainActor func timelineSelectionDidChange(_: TimelineContainerViewController, articles: [Article]?, mode: TimelineSourceMode)
|
||||||
func timelineInvalidatedRestorationState(_: TimelineContainerViewController)
|
@MainActor func timelineRequestedFeedSelection(_: TimelineContainerViewController, feed: Feed)
|
||||||
|
@MainActor func timelineInvalidatedRestorationState(_: TimelineContainerViewController)
|
||||||
}
|
}
|
||||||
|
|
||||||
final class TimelineContainerViewController: NSViewController {
|
final class TimelineContainerViewController: NSViewController {
|
||||||
|
@ -13,9 +13,10 @@ import Account
|
|||||||
import os.log
|
import os.log
|
||||||
|
|
||||||
protocol TimelineDelegate: AnyObject {
|
protocol TimelineDelegate: AnyObject {
|
||||||
func timelineSelectionDidChange(_: TimelineViewController, selectedArticles: [Article]?)
|
|
||||||
func timelineRequestedFeedSelection(_: TimelineViewController, feed: Feed)
|
@MainActor func timelineSelectionDidChange(_: TimelineViewController, selectedArticles: [Article]?)
|
||||||
func timelineInvalidatedRestorationState(_: TimelineViewController)
|
@MainActor func timelineRequestedFeedSelection(_: TimelineViewController, feed: Feed)
|
||||||
|
@MainActor func timelineInvalidatedRestorationState(_: TimelineViewController)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TimelineShowFeedName {
|
enum TimelineShowFeedName {
|
||||||
@ -535,12 +536,15 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
|||||||
|
|
||||||
// MARK: - Navigation
|
// MARK: - Navigation
|
||||||
|
|
||||||
func goToDeepLink(for userInfo: [AnyHashable : Any]) {
|
func goToDeepLink(for articlePathInfo: ArticlePathInfo) {
|
||||||
guard let articleID = userInfo[ArticlePathKey.articleID] as? String else { return }
|
|
||||||
|
guard let articleID = articlePathInfo.articleID else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
if isReadFiltered ?? false {
|
if isReadFiltered ?? false {
|
||||||
if let accountName = userInfo[ArticlePathKey.accountName] as? String,
|
if let accountName = articlePathInfo.accountName,
|
||||||
let account = AccountManager.shared.existingActiveAccount(forDisplayName: accountName) {
|
let account = AccountManager.shared.existingActiveAccount(forDisplayName: accountName) {
|
||||||
exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: articleID)
|
exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: articleID)
|
||||||
await fetchAndReplaceArticles()
|
await fetchAndReplaceArticles()
|
||||||
|
@ -161,7 +161,7 @@ struct AddAccountsView: View {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var icloudAccount: some View {
|
@MainActor var icloudAccount: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text("iCloud")
|
Text("iCloud")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
@ -260,7 +260,7 @@ struct AddAccountsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func isCloudInUse() -> Bool {
|
@MainActor private func isCloudInUse() -> Bool {
|
||||||
AccountManager.shared.accounts.contains(where: { $0.type == .cloudKit })
|
AccountManager.shared.accounts.contains(where: { $0.type == .cloudKit })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@ extension AppDelegate : AppDelegateAppleEvents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class NetNewsWireCreateElementCommand : NSCreateCommand {
|
class NetNewsWireCreateElementCommand : NSCreateCommand {
|
||||||
override func performDefaultImplementation() -> Any? {
|
@MainActor override func performDefaultImplementation() -> Any? {
|
||||||
let classDescription = self.createClassDescription
|
let classDescription = self.createClassDescription
|
||||||
if (classDescription.className == "feed") {
|
if (classDescription.className == "feed") {
|
||||||
return ScriptableFeed.handleCreateElement(command:self)
|
return ScriptableFeed.handleCreateElement(command:self)
|
||||||
|
@ -111,7 +111,9 @@ class ScriptableArticle: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
|
|||||||
return article.status.boolStatus(forKey:.read)
|
return article.status.boolStatus(forKey:.read)
|
||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
markArticles([self.article], statusKey: .read, flag: newValue)
|
Task { @MainActor in
|
||||||
|
markArticles([self.article], statusKey: .read, flag: newValue)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,7 +123,9 @@ class ScriptableArticle: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
|
|||||||
return article.status.boolStatus(forKey:.starred)
|
return article.status.boolStatus(forKey:.starred)
|
||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
markArticles([self.article], statusKey: .starred, flag: newValue)
|
Task { @MainActor in
|
||||||
|
markArticles([self.article], statusKey: .starred, flag: newValue)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,7 +146,7 @@ class ScriptableArticle: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc(feed)
|
@objc(feed)
|
||||||
var feed: ScriptableFeed? {
|
@MainActor var feed: ScriptableFeed? {
|
||||||
guard let parentFeed = self.article.feed,
|
guard let parentFeed = self.article.feed,
|
||||||
let account = parentFeed.account
|
let account = parentFeed.account
|
||||||
else { return nil }
|
else { return nil }
|
||||||
|
@ -81,7 +81,7 @@ import Articles
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class func handleCreateElement(command:NSCreateCommand) -> Any? {
|
@MainActor class func handleCreateElement(command:NSCreateCommand) -> Any? {
|
||||||
guard command.isCreateCommand(forClass:"Feed") else { return nil }
|
guard command.isCreateCommand(forClass:"Feed") else { return nil }
|
||||||
guard let arguments = command.arguments else {return nil}
|
guard let arguments = command.arguments else {return nil}
|
||||||
let titleFromArgs = command.property(forKey:"name") as? String
|
let titleFromArgs = command.property(forKey:"name") as? String
|
||||||
|
@ -65,7 +65,7 @@ class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContai
|
|||||||
or
|
or
|
||||||
tell account X to make new folder at end with properties {name:"new folder name"}
|
tell account X to make new folder at end with properties {name:"new folder name"}
|
||||||
*/
|
*/
|
||||||
class func handleCreateElement(command:NSCreateCommand) -> Any? {
|
@MainActor class func handleCreateElement(command:NSCreateCommand) -> Any? {
|
||||||
guard command.isCreateCommand(forClass:"fold") else { return nil }
|
guard command.isCreateCommand(forClass:"fold") else { return nil }
|
||||||
let name = command.property(forKey:"name") as? String ?? ""
|
let name = command.property(forKey:"name") as? String ?? ""
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ extension NSScriptCommand {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func accountAndFolderForNewChild() -> (Account, Folder?) {
|
@MainActor func accountAndFolderForNewChild() -> (Account, Folder?) {
|
||||||
let appleEvent = self.appleEvent
|
let appleEvent = self.appleEvent
|
||||||
var account = AccountManager.shared.defaultAccount
|
var account = AccountManager.shared.defaultAccount
|
||||||
var folder:Folder? = nil
|
var folder:Folder? = nil
|
||||||
|
@ -20,8 +20,8 @@ import UniformTypeIdentifiers
|
|||||||
import CoreSpotlight
|
import CoreSpotlight
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
class ActivityManager {
|
@MainActor final class ActivityManager {
|
||||||
|
|
||||||
private var nextUnreadActivity: NSUserActivity?
|
private var nextUnreadActivity: NSUserActivity?
|
||||||
private var selectingActivity: NSUserActivity?
|
private var selectingActivity: NSUserActivity?
|
||||||
private var readingActivity: NSUserActivity?
|
private var readingActivity: NSUserActivity?
|
||||||
@ -264,8 +264,8 @@ private extension ActivityManager {
|
|||||||
return value?.components(separatedBy: " ").filter { $0.count > 2 } ?? []
|
return value?.components(separatedBy: " ").filter { $0.count > 2 } ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateSelectingActivityFeedSearchAttributes(with feed: Feed) {
|
@MainActor func updateSelectingActivityFeedSearchAttributes(with feed: Feed) {
|
||||||
|
|
||||||
let attributeSet = CSSearchableItemAttributeSet(contentType: UTType.item)
|
let attributeSet = CSSearchableItemAttributeSet(contentType: UTType.item)
|
||||||
attributeSet.title = feed.nameForDisplay
|
attributeSet.title = feed.nameForDisplay
|
||||||
attributeSet.keywords = makeKeywords(feed.nameForDisplay)
|
attributeSet.keywords = makeKeywords(feed.nameForDisplay)
|
||||||
|
@ -19,11 +19,12 @@ public enum ArticleExtractorState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protocol ArticleExtractorDelegate {
|
protocol ArticleExtractorDelegate {
|
||||||
func articleExtractionDidFail(with: Error)
|
|
||||||
func articleExtractionDidComplete(extractedArticle: ExtractedArticle)
|
@MainActor func articleExtractionDidFail(with: Error)
|
||||||
|
@MainActor func articleExtractionDidComplete(extractedArticle: ExtractedArticle)
|
||||||
}
|
}
|
||||||
|
|
||||||
class ArticleExtractor {
|
final class ArticleExtractor {
|
||||||
|
|
||||||
private var dataTask: URLSessionDataTask? = nil
|
private var dataTask: URLSessionDataTask? = nil
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ import RSCore
|
|||||||
import Articles
|
import Articles
|
||||||
import Account
|
import Account
|
||||||
|
|
||||||
struct ArticleRenderer {
|
@MainActor struct ArticleRenderer {
|
||||||
|
|
||||||
typealias Rendering = (style: String, html: String, title: String, baseURL: String)
|
typealias Rendering = (style: String, html: String, title: String, baseURL: String)
|
||||||
|
|
||||||
@ -30,10 +30,10 @@ struct ArticleRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static var imageIconScheme = "nnwImageIcon"
|
static let imageIconScheme = "nnwImageIcon"
|
||||||
|
|
||||||
static var blank = Page(name: "blank")
|
static let blank = Page(name: "blank")
|
||||||
static var page = Page(name: "page")
|
static let page = Page(name: "page")
|
||||||
|
|
||||||
private let article: Article?
|
private let article: Article?
|
||||||
private let extractedArticle: ExtractedArticle?
|
private let extractedArticle: ExtractedArticle?
|
||||||
@ -328,7 +328,7 @@ private extension ArticleRenderer {
|
|||||||
|
|
||||||
// MARK: - Article extension
|
// MARK: - Article extension
|
||||||
|
|
||||||
private extension Article {
|
@MainActor private extension Article {
|
||||||
|
|
||||||
var baseURL: URL? {
|
var baseURL: URL? {
|
||||||
var s = link
|
var s = link
|
||||||
|
@ -10,22 +10,20 @@ import Foundation
|
|||||||
|
|
||||||
struct ArticlePathInfo {
|
struct ArticlePathInfo {
|
||||||
|
|
||||||
let accountID: String
|
let accountID: String?
|
||||||
let articleID: String
|
let accountName: String?
|
||||||
|
let articleID: String?
|
||||||
|
let feedID: String?
|
||||||
|
|
||||||
init?(userInfo: [AnyHashable: Any]) {
|
init?(userInfo: [AnyHashable: Any]) {
|
||||||
|
|
||||||
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [String: String] else {
|
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [String: String] else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
guard let accountID = articlePathUserInfo[ArticlePathKey.accountID] else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
guard let articleID = articlePathUserInfo[ArticlePathKey.articleID] else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
self.accountID = accountID
|
self.accountID = articlePathUserInfo[ArticlePathKey.accountID]
|
||||||
self.articleID = articleID
|
self.accountName = articlePathUserInfo[ArticlePathKey.accountName]
|
||||||
|
self.articleID = articlePathUserInfo[ArticlePathKey.articleID]
|
||||||
|
self.feedID = articlePathUserInfo[ArticlePathKey.feedID]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import Articles
|
|||||||
|
|
||||||
// Mark articles read/unread, starred/unstarred, deleted/undeleted.
|
// Mark articles read/unread, starred/unstarred, deleted/undeleted.
|
||||||
|
|
||||||
final class MarkStatusCommand: UndoableCommand {
|
@MainActor final class MarkStatusCommand: UndoableCommand {
|
||||||
|
|
||||||
let undoActionName: String
|
let undoActionName: String
|
||||||
let redoActionName: String
|
let redoActionName: String
|
||||||
@ -50,12 +50,12 @@ final class MarkStatusCommand: UndoableCommand {
|
|||||||
self.init(initialArticles: initialArticles, statusKey: .starred, flag: markingStarred, undoManager: undoManager, completion: completion)
|
self.init(initialArticles: initialArticles, statusKey: .starred, flag: markingStarred, undoManager: undoManager, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func perform() {
|
@MainActor func perform() {
|
||||||
mark(statusKey, flag)
|
mark(statusKey, flag)
|
||||||
registerUndo()
|
registerUndo()
|
||||||
}
|
}
|
||||||
|
|
||||||
func undo() {
|
@MainActor func undo() {
|
||||||
mark(statusKey, !flag)
|
mark(statusKey, !flag)
|
||||||
registerRedo()
|
registerRedo()
|
||||||
}
|
}
|
||||||
@ -63,7 +63,7 @@ final class MarkStatusCommand: UndoableCommand {
|
|||||||
|
|
||||||
private extension MarkStatusCommand {
|
private extension MarkStatusCommand {
|
||||||
|
|
||||||
func mark(_ statusKey: ArticleStatus.Key, _ flag: Bool) {
|
@MainActor func mark(_ statusKey: ArticleStatus.Key, _ flag: Bool) {
|
||||||
markArticles(articles, statusKey: statusKey, flag: flag, completion: completion)
|
markArticles(articles, statusKey: statusKey, flag: flag, completion: completion)
|
||||||
completion = nil
|
completion = nil
|
||||||
}
|
}
|
||||||
@ -83,7 +83,7 @@ private extension MarkStatusCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func filteredArticles(_ articles: [Article], _ statusKey: ArticleStatus.Key, _ flag: Bool) -> [Article] {
|
@MainActor static func filteredArticles(_ articles: [Article], _ statusKey: ArticleStatus.Key, _ flag: Bool) -> [Article] {
|
||||||
|
|
||||||
return articles.filter{ article in
|
return articles.filter{ article in
|
||||||
guard article.status.boolStatus(forKey: statusKey) != flag else { return false }
|
guard article.status.boolStatus(forKey: statusKey) != flag else { return false }
|
||||||
|
@ -21,7 +21,7 @@ final class SendToMarsEditCommand: SendToCommand {
|
|||||||
appToUse() != nil
|
appToUse() != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendObject(_ object: Any?, selectedText: String?) {
|
@MainActor func sendObject(_ object: Any?, selectedText: String?) {
|
||||||
|
|
||||||
guard canSendObject(object, selectedText: selectedText) else {
|
guard canSendObject(object, selectedText: selectedText) else {
|
||||||
return
|
return
|
||||||
@ -39,7 +39,7 @@ final class SendToMarsEditCommand: SendToCommand {
|
|||||||
|
|
||||||
private extension SendToMarsEditCommand {
|
private extension SendToMarsEditCommand {
|
||||||
|
|
||||||
func send(_ article: Article, to app: UserApp) {
|
@MainActor func send(_ article: Article, to app: UserApp) {
|
||||||
|
|
||||||
// App has already been launched.
|
// App has already been launched.
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ final class SendToMicroBlogCommand: SendToCommand {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendObject(_ object: Any?, selectedText: String?) {
|
@MainActor func sendObject(_ object: Any?, selectedText: String?) {
|
||||||
|
|
||||||
guard canSendObject(object, selectedText: selectedText) else {
|
guard canSendObject(object, selectedText: selectedText) else {
|
||||||
return
|
return
|
||||||
@ -60,7 +60,7 @@ final class SendToMicroBlogCommand: SendToCommand {
|
|||||||
|
|
||||||
private extension Article {
|
private extension Article {
|
||||||
|
|
||||||
var attributionString: String {
|
@MainActor var attributionString: String {
|
||||||
|
|
||||||
// Feed name, or feed name + author name (if author is specified per-article).
|
// Feed name, or feed name + author name (if author is specified per-article).
|
||||||
// Includes trailing space.
|
// Includes trailing space.
|
||||||
|
@ -11,7 +11,7 @@ import Account
|
|||||||
|
|
||||||
struct AddFeedDefaultContainer {
|
struct AddFeedDefaultContainer {
|
||||||
|
|
||||||
static var defaultContainer: Container? {
|
@MainActor static var defaultContainer: Container? {
|
||||||
|
|
||||||
if let accountID = AppDefaults.shared.addFeedAccountID, let account = AccountManager.shared.activeAccounts.first(where: { $0.accountID == accountID }) {
|
if let accountID = AppDefaults.shared.addFeedAccountID, let account = AccountManager.shared.activeAccounts.first(where: { $0.accountID == accountID }) {
|
||||||
if let folderName = AppDefaults.shared.addFeedFolderName, let folder = account.existingFolder(withDisplayName: folderName) {
|
if let folderName = AppDefaults.shared.addFeedFolderName, let folder = account.existingFolder(withDisplayName: folderName) {
|
||||||
|
@ -10,7 +10,7 @@ import Foundation
|
|||||||
import Articles
|
import Articles
|
||||||
import RSParser
|
import RSParser
|
||||||
|
|
||||||
struct ArticleStringFormatter {
|
@MainActor struct ArticleStringFormatter {
|
||||||
|
|
||||||
private static var feedNameCache = [String: String]()
|
private static var feedNameCache = [String: String]()
|
||||||
private static var titleCache = [String: String]()
|
private static var titleCache = [String: String]()
|
||||||
|
@ -13,7 +13,7 @@ import Account
|
|||||||
|
|
||||||
// These handle multiple accounts.
|
// These handle multiple accounts.
|
||||||
|
|
||||||
func markArticles(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool, completion: (() -> Void)? = nil) {
|
@MainActor func markArticles(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool, completion: (() -> Void)? = nil) {
|
||||||
|
|
||||||
let d: [String: Set<Article>] = accountAndArticlesDictionary(articles)
|
let d: [String: Set<Article>] = accountAndArticlesDictionary(articles)
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ private func accountAndArticlesDictionary(_ articles: Set<Article>) -> [String:
|
|||||||
|
|
||||||
extension Article {
|
extension Article {
|
||||||
|
|
||||||
var feed: Feed? {
|
@MainActor var feed: Feed? {
|
||||||
return account?.existingFeed(withFeedID: feedID)
|
return account?.existingFeed(withFeedID: feedID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,7 +98,7 @@ extension Article {
|
|||||||
return datePublished ?? dateModified ?? status.dateArrived
|
return datePublished ?? dateModified ?? status.dateArrived
|
||||||
}
|
}
|
||||||
|
|
||||||
var isAvailableToMarkUnread: Bool {
|
@MainActor var isAvailableToMarkUnread: Bool {
|
||||||
guard let markUnreadWindow = account?.behaviors.compactMap( { behavior -> Int? in
|
guard let markUnreadWindow = account?.behaviors.compactMap( { behavior -> Int? in
|
||||||
switch behavior {
|
switch behavior {
|
||||||
case .disallowMarkAsUnreadAfterPeriod(let days):
|
case .disallowMarkAsUnreadAfterPeriod(let days):
|
||||||
@ -117,11 +117,11 @@ extension Article {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func iconImage() -> IconImage? {
|
@MainActor func iconImage() -> IconImage? {
|
||||||
return IconImageCache.shared.imageForArticle(self)
|
return IconImageCache.shared.imageForArticle(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
func iconImageUrl(feed: Feed) -> URL? {
|
@MainActor func iconImageUrl(feed: Feed) -> URL? {
|
||||||
if let image = iconImage() {
|
if let image = iconImage() {
|
||||||
let fm = FileManager.default
|
let fm = FileManager.default
|
||||||
var path = fm.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
var path = fm.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
||||||
@ -138,7 +138,7 @@ extension Article {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func byline() -> String {
|
@MainActor func byline() -> String {
|
||||||
guard let authors = authors ?? feed?.authors, !authors.isEmpty else {
|
guard let authors = authors ?? feed?.authors, !authors.isEmpty else {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@ -199,7 +199,7 @@ struct ArticlePathKey {
|
|||||||
|
|
||||||
extension Article {
|
extension Article {
|
||||||
|
|
||||||
public var pathUserInfo: [AnyHashable : Any] {
|
@MainActor public var pathUserInfo: [AnyHashable : Any] {
|
||||||
return [
|
return [
|
||||||
ArticlePathKey.accountID: accountID,
|
ArticlePathKey.accountID: accountID,
|
||||||
ArticlePathKey.accountName: account?.nameForDisplay ?? "",
|
ArticlePathKey.accountName: account?.nameForDisplay ?? "",
|
||||||
@ -214,7 +214,7 @@ extension Article {
|
|||||||
|
|
||||||
extension Article: SortableArticle {
|
extension Article: SortableArticle {
|
||||||
|
|
||||||
var sortableName: String {
|
@MainActor var sortableName: String {
|
||||||
return feed?.name ?? ""
|
return feed?.name ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ extension Account: SmallIconProvider {
|
|||||||
|
|
||||||
extension Feed: SmallIconProvider {
|
extension Feed: SmallIconProvider {
|
||||||
|
|
||||||
var smallIcon: IconImage? {
|
@MainActor var smallIcon: IconImage? {
|
||||||
if let iconImage = appDelegate.faviconDownloader.favicon(for: self) {
|
if let iconImage = appDelegate.faviconDownloader.favicon(for: self) {
|
||||||
return iconImage
|
return iconImage
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ import Foundation
|
|||||||
import RSCore
|
import RSCore
|
||||||
import Account
|
import Account
|
||||||
|
|
||||||
final class FaviconGenerator {
|
@MainActor final class FaviconGenerator {
|
||||||
|
|
||||||
private static var faviconGeneratorCache = [String: IconImage]() // feedURL: RSImage
|
private static var faviconGeneratorCache = [String: IconImage]() // feedURL: RSImage
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ import Foundation
|
|||||||
import Account
|
import Account
|
||||||
import Articles
|
import Articles
|
||||||
|
|
||||||
final class IconImageCache {
|
@MainActor final class IconImageCache {
|
||||||
|
|
||||||
static let shared = IconImageCache()
|
static let shared = IconImageCache()
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ import Foundation
|
|||||||
import Account
|
import Account
|
||||||
import RSCore
|
import RSCore
|
||||||
|
|
||||||
struct DefaultFeedsImporter {
|
@MainActor struct DefaultFeedsImporter {
|
||||||
|
|
||||||
static func importDefaultFeeds(account: Account) {
|
static func importDefaultFeeds(account: Account) {
|
||||||
let defaultFeedsURL = Bundle.main.url(forResource: "DefaultFeeds", withExtension: "opml")!
|
let defaultFeedsURL = Bundle.main.url(forResource: "DefaultFeeds", withExtension: "opml")!
|
||||||
|
@ -14,9 +14,9 @@ import Account
|
|||||||
|
|
||||||
final class ExtensionContainersFile {
|
final class ExtensionContainersFile {
|
||||||
|
|
||||||
private static var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "extensionContainersFile")
|
private static let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "extensionContainersFile")
|
||||||
|
|
||||||
private static var filePath: String = {
|
private static let filePath: String = {
|
||||||
let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String
|
let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String
|
||||||
let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup)
|
let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup)
|
||||||
return containerURL!.appendingPathComponent("extension_containers.plist").path
|
return containerURL!.appendingPathComponent("extension_containers.plist").path
|
||||||
|
@ -10,8 +10,8 @@ import Foundation
|
|||||||
import os.log
|
import os.log
|
||||||
import Account
|
import Account
|
||||||
|
|
||||||
final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter {
|
final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter, Sendable {
|
||||||
|
|
||||||
private static let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "extensionFeedAddRequestFile")
|
private static let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "extensionFeedAddRequestFile")
|
||||||
|
|
||||||
private static let filePath: String = {
|
private static let filePath: String = {
|
||||||
@ -30,7 +30,7 @@ final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter {
|
|||||||
return operationQueue
|
return operationQueue
|
||||||
}
|
}
|
||||||
|
|
||||||
override init() {
|
@MainActor override init() {
|
||||||
operationQueue = OperationQueue()
|
operationQueue = OperationQueue()
|
||||||
operationQueue.maxConcurrentOperationCount = 1
|
operationQueue.maxConcurrentOperationCount = 1
|
||||||
|
|
||||||
@ -41,14 +41,16 @@ final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func presentedItemDidChange() {
|
func presentedItemDidChange() {
|
||||||
DispatchQueue.main.async {
|
Task { @MainActor in
|
||||||
self.process()
|
self.process()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func resume() {
|
func resume() {
|
||||||
NSFileCoordinator.addFilePresenter(self)
|
NSFileCoordinator.addFilePresenter(self)
|
||||||
process()
|
Task { @MainActor in
|
||||||
|
process()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func suspend() {
|
func suspend() {
|
||||||
@ -95,8 +97,8 @@ final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter {
|
|||||||
|
|
||||||
private extension ExtensionFeedAddRequestFile {
|
private extension ExtensionFeedAddRequestFile {
|
||||||
|
|
||||||
func process() {
|
@MainActor func process() {
|
||||||
|
|
||||||
let decoder = PropertyListDecoder()
|
let decoder = PropertyListDecoder()
|
||||||
let encoder = PropertyListEncoder()
|
let encoder = PropertyListEncoder()
|
||||||
encoder.outputFormat = .binary
|
encoder.outputFormat = .binary
|
||||||
@ -130,7 +132,7 @@ private extension ExtensionFeedAddRequestFile {
|
|||||||
requests?.forEach { processRequest($0) }
|
requests?.forEach { processRequest($0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
func processRequest(_ request: ExtensionFeedAddRequest) {
|
@MainActor func processRequest(_ request: ExtensionFeedAddRequest) {
|
||||||
var destinationAccountID: String? = nil
|
var destinationAccountID: String? = nil
|
||||||
switch request.destinationContainerID {
|
switch request.destinationContainerID {
|
||||||
case .account(let accountID):
|
case .account(let accountID):
|
||||||
|
@ -62,7 +62,7 @@ final class SmartFeed: PseudoFeed {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func fetchUnreadCounts() {
|
@objc @MainActor func fetchUnreadCounts() {
|
||||||
let activeAccounts = AccountManager.shared.activeAccounts
|
let activeAccounts = AccountManager.shared.activeAccounts
|
||||||
|
|
||||||
// Remove any accounts that are no longer active or have been deleted
|
// Remove any accounts that are no longer active or have been deleted
|
||||||
@ -113,15 +113,18 @@ private extension SmartFeed {
|
|||||||
|
|
||||||
func fetchUnreadCount(for account: Account) {
|
func fetchUnreadCount(for account: Account) {
|
||||||
delegate.fetchUnreadCount(for: account) { singleUnreadCountResult in
|
delegate.fetchUnreadCount(for: account) { singleUnreadCountResult in
|
||||||
guard let accountUnreadCount = try? singleUnreadCountResult.get() else {
|
|
||||||
return
|
MainActor.assumeIsolated {
|
||||||
|
guard let accountUnreadCount = try? singleUnreadCountResult.get() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.unreadCounts[account.accountID] = accountUnreadCount
|
||||||
|
self.updateUnreadCount()
|
||||||
}
|
}
|
||||||
self.unreadCounts[account.accountID] = accountUnreadCount
|
|
||||||
self.updateUnreadCount()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUnreadCount() {
|
@MainActor func updateUnreadCount() {
|
||||||
unreadCount = AccountManager.shared.activeAccounts.reduce(0) { (result, account) -> Int in
|
unreadCount = AccountManager.shared.activeAccounts.reduce(0) { (result, account) -> Int in
|
||||||
if let oneUnreadCount = unreadCounts[account.accountID] {
|
if let oneUnreadCount = unreadCounts[account.accountID] {
|
||||||
return result + oneUnreadCount
|
return result + oneUnreadCount
|
||||||
|
@ -51,13 +51,13 @@ final class UnreadFeed: PseudoFeed {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
init() {
|
@MainActor init() {
|
||||||
|
|
||||||
self.unreadCount = appDelegate.unreadCount
|
self.unreadCount = appDelegate.unreadCount
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: appDelegate)
|
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: appDelegate)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func unreadCountDidChange(_ note: Notification) {
|
@objc @MainActor func unreadCountDidChange(_ note: Notification) {
|
||||||
|
|
||||||
assert(note.object is AppDelegate)
|
assert(note.object is AppDelegate)
|
||||||
unreadCount = appDelegate.unreadCount
|
unreadCount = appDelegate.unreadCount
|
||||||
|
@ -67,7 +67,7 @@ extension Array where Element == Article {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func anyArticleIsReadAndCanMarkUnread() -> Bool {
|
@MainActor func anyArticleIsReadAndCanMarkUnread() -> Bool {
|
||||||
return anyArticlePassesTest { $0.status.read && $0.isAvailableToMarkUnread }
|
return anyArticlePassesTest { $0.status.read && $0.isAvailableToMarkUnread }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,7 +95,7 @@ extension Array where Element == Article {
|
|||||||
var i = 0
|
var i = 0
|
||||||
for article in self {
|
for article in self {
|
||||||
let otherArticle = otherArticles[i]
|
let otherArticle = otherArticles[i]
|
||||||
if article.account != otherArticle.account || article.articleID != otherArticle.articleID {
|
if article.accountID != otherArticle.accountID || article.articleID != otherArticle.articleID {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
i += 1
|
i += 1
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Account
|
import Account
|
||||||
|
|
||||||
class AccountRefreshTimer {
|
@MainActor final class AccountRefreshTimer {
|
||||||
|
|
||||||
var shuttingDown = false
|
var shuttingDown = false
|
||||||
|
|
||||||
@ -64,7 +64,7 @@ class AccountRefreshTimer {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func timedRefresh(_ sender: Timer?) {
|
@objc @MainActor func timedRefresh(_ sender: Timer?) {
|
||||||
|
|
||||||
guard !shuttingDown else {
|
guard !shuttingDown else {
|
||||||
return
|
return
|
||||||
|
@ -9,8 +9,8 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Account
|
import Account
|
||||||
|
|
||||||
class ArticleStatusSyncTimer {
|
@MainActor final class ArticleStatusSyncTimer {
|
||||||
|
|
||||||
private static let intervalSeconds = Double(120)
|
private static let intervalSeconds = Double(120)
|
||||||
|
|
||||||
var shuttingDown = false
|
var shuttingDown = false
|
||||||
|
@ -24,7 +24,7 @@ final class FeedTreeControllerDelegate: TreeControllerDelegate {
|
|||||||
filterExceptions = Set<SidebarItemIdentifier>()
|
filterExceptions = Set<SidebarItemIdentifier>()
|
||||||
}
|
}
|
||||||
|
|
||||||
func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? {
|
@MainActor func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? {
|
||||||
if node.isRoot {
|
if node.isRoot {
|
||||||
return childNodesForRootNode(node)
|
return childNodesForRootNode(node)
|
||||||
}
|
}
|
||||||
@ -41,7 +41,7 @@ final class FeedTreeControllerDelegate: TreeControllerDelegate {
|
|||||||
|
|
||||||
private extension FeedTreeControllerDelegate {
|
private extension FeedTreeControllerDelegate {
|
||||||
|
|
||||||
func childNodesForRootNode(_ rootNode: Node) -> [Node]? {
|
@MainActor func childNodesForRootNode(_ rootNode: Node) -> [Node]? {
|
||||||
var topLevelNodes = [Node]()
|
var topLevelNodes = [Node]()
|
||||||
|
|
||||||
let smartFeedsNode = rootNode.existingOrNewChildNode(with: SmartFeedsController.shared)
|
let smartFeedsNode = rootNode.existingOrNewChildNode(with: SmartFeedsController.shared)
|
||||||
@ -132,7 +132,7 @@ private extension FeedTreeControllerDelegate {
|
|||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
func sortedAccountNodes(_ parent: Node) -> [Node] {
|
@MainActor func sortedAccountNodes(_ parent: Node) -> [Node] {
|
||||||
let nodes = AccountManager.shared.sortedActiveAccounts.compactMap { (account) -> Node? in
|
let nodes = AccountManager.shared.sortedActiveAccounts.compactMap { (account) -> Node? in
|
||||||
let accountNode = parent.existingOrNewChildNode(with: account)
|
let accountNode = parent.existingOrNewChildNode(with: account)
|
||||||
accountNode.canHaveChildNodes = true
|
accountNode.canHaveChildNodes = true
|
||||||
|
@ -14,7 +14,7 @@ import Account
|
|||||||
|
|
||||||
final class FolderTreeControllerDelegate: TreeControllerDelegate {
|
final class FolderTreeControllerDelegate: TreeControllerDelegate {
|
||||||
|
|
||||||
func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? {
|
@MainActor func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? {
|
||||||
|
|
||||||
return node.isRoot ? childNodesForRootNode(node) : childNodes(node)
|
return node.isRoot ? childNodesForRootNode(node) : childNodes(node)
|
||||||
}
|
}
|
||||||
@ -22,8 +22,8 @@ final class FolderTreeControllerDelegate: TreeControllerDelegate {
|
|||||||
|
|
||||||
private extension FolderTreeControllerDelegate {
|
private extension FolderTreeControllerDelegate {
|
||||||
|
|
||||||
func childNodesForRootNode(_ node: Node) -> [Node]? {
|
@MainActor func childNodesForRootNode(_ node: Node) -> [Node]? {
|
||||||
|
|
||||||
let accountNodes: [Node] = AccountManager.shared.sortedActiveAccounts.map { account in
|
let accountNodes: [Node] = AccountManager.shared.sortedActiveAccounts.map { account in
|
||||||
let accountNode = Node(representedObject: account, parent: node)
|
let accountNode = Node(representedObject: account, parent: node)
|
||||||
accountNode.canHaveChildNodes = true
|
accountNode.canHaveChildNodes = true
|
||||||
|
@ -25,9 +25,11 @@ final class UserNotificationManager: NSObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for article in articles {
|
Task { @MainActor in
|
||||||
if !article.status.read, let feed = article.feed, feed.isNotifyAboutNewArticles ?? false {
|
for article in articles {
|
||||||
sendNotification(feed: feed, article: article)
|
if !article.status.read, let feed = article.feed, feed.isNotifyAboutNewArticles ?? false {
|
||||||
|
sendNotification(feed: feed, article: article)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -53,7 +55,7 @@ final class UserNotificationManager: NSObject {
|
|||||||
|
|
||||||
private extension UserNotificationManager {
|
private extension UserNotificationManager {
|
||||||
|
|
||||||
func sendNotification(feed: Feed, article: Article) {
|
@MainActor func sendNotification(feed: Feed, article: Article) {
|
||||||
let content = UNMutableNotificationContent()
|
let content = UNMutableNotificationContent()
|
||||||
|
|
||||||
content.title = feed.nameForDisplay
|
content.title = feed.nameForDisplay
|
||||||
@ -79,7 +81,7 @@ private extension UserNotificationManager {
|
|||||||
/// - feed: `Feed`
|
/// - feed: `Feed`
|
||||||
/// - Returns: A `UNNotifcationAttachment` if an icon is available. Otherwise nil.
|
/// - Returns: A `UNNotifcationAttachment` if an icon is available. Otherwise nil.
|
||||||
/// - Warning: In certain scenarios, this will return the `faviconTemplateImage`.
|
/// - Warning: In certain scenarios, this will return the `faviconTemplateImage`.
|
||||||
func thumbnailAttachment(for article: Article, feed: Feed) -> UNNotificationAttachment? {
|
@MainActor func thumbnailAttachment(for article: Article, feed: Feed) -> UNNotificationAttachment? {
|
||||||
if let imageURL = article.iconImageUrl(feed: feed) {
|
if let imageURL = article.iconImageUrl(feed: feed) {
|
||||||
let thumbnail = try? UNNotificationAttachment(identifier: feed.feedID, url: imageURL, options: nil)
|
let thumbnail = try? UNNotificationAttachment(identifier: feed.feedID, url: imageURL, options: nil)
|
||||||
return thumbnail
|
return thumbnail
|
||||||
|
Loading…
x
Reference in New Issue
Block a user