Compare commits
35 Commits
adb500fa29
...
87ec4fc4e0
Author | SHA1 | Date |
---|---|---|
Andy Williams | 87ec4fc4e0 | |
Brent Simmons | 178cba34ad | |
Brent Simmons | 30e961bfe4 | |
Brent Simmons | bf02d1d86a | |
Brent Simmons | 16cebcd60a | |
Brent Simmons | 7f042b5d07 | |
Brent Simmons | 19a39ac295 | |
Brent Simmons | 3f8724c9d1 | |
Brent Simmons | 1e80253018 | |
Brent Simmons | 83298770c2 | |
Brent Simmons | 7f545c5a23 | |
Brent Simmons | db9a8c7feb | |
Brent Simmons | 4ea66ee11e | |
Brent Simmons | ea0a827024 | |
Brent Simmons | fdbcf0d84e | |
Brent Simmons | 7d04021415 | |
Brent Simmons | 87fe78f598 | |
Brent Simmons | 9dcfc2b09c | |
Brent Simmons | 22d184c5f6 | |
Brent Simmons | 09cf212057 | |
Brent Simmons | 07091e0d3e | |
Brent Simmons | 0a2b4f7008 | |
Brent Simmons | 9403d81550 | |
Brent Simmons | 78a64c3146 | |
Brent Simmons | 18d0b0e1e7 | |
Brent Simmons | 2418076364 | |
Brent Simmons | a9d50f3a14 | |
Brent Simmons | 02d8005fa7 | |
Brent Simmons | 19fd3d96ab | |
Brent Simmons | 81cede769a | |
Brent Simmons | 6776862322 | |
Brent Simmons | 6c1ea427af | |
Brent Simmons | 5c31993b90 | |
Brent Simmons | 325f8061de | |
Andy Williams | 1f1bbc8b26 |
|
@ -995,7 +995,8 @@ public enum FetchType {
|
|||
// MARK: - AccountMetadataDelegate
|
||||
|
||||
extension Account: AccountMetadataDelegate {
|
||||
func valueDidChange(_ accountMetadata: AccountMetadata, key: AccountMetadata.CodingKeys) {
|
||||
|
||||
nonisolated func valueDidChange(_ accountMetadata: AccountMetadata, key: AccountMetadata.CodingKeys) {
|
||||
Task { @MainActor in
|
||||
metadataFile.markAsDirty()
|
||||
}
|
||||
|
@ -1006,11 +1007,13 @@ extension Account: AccountMetadataDelegate {
|
|||
|
||||
extension Account: FeedMetadataDelegate {
|
||||
|
||||
func valueDidChange(_ feedMetadata: FeedMetadata, key: FeedMetadata.CodingKeys) {
|
||||
nonisolated func valueDidChange(_ feedMetadata: FeedMetadata, key: FeedMetadata.CodingKeys) {
|
||||
|
||||
let feedID = feedMetadata.feedID
|
||||
|
||||
Task { @MainActor in
|
||||
feedMetadataFile.markAsDirty()
|
||||
guard let feed = existingFeed(withFeedID: feedMetadata.feedID) else {
|
||||
guard let feed = existingFeed(withFeedID: feedID) else {
|
||||
return
|
||||
}
|
||||
feed.postFeedSettingDidChangeNotification(key)
|
||||
|
|
|
@ -24,7 +24,6 @@ final class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate {
|
|||
weak var account: Account?
|
||||
var database: SyncDatabase
|
||||
weak var articlesZone: CloudKitArticlesZone?
|
||||
var compressionQueue = DispatchQueue(label: "Articles Zone Delegate Compression Queue")
|
||||
|
||||
init(account: Account, database: SyncDatabase, articlesZone: CloudKitArticlesZone) {
|
||||
self.account = account
|
||||
|
@ -42,13 +41,13 @@ final class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate {
|
|||
|
||||
await self.delete(recordKeys: deleted, pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs)
|
||||
|
||||
self.update(records: changed,
|
||||
try await self.update(records: changed,
|
||||
pendingReadStatusArticleIDs: pendingReadStatusArticleIDs,
|
||||
pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs,
|
||||
completion: completion)
|
||||
pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs)
|
||||
completion(.success(()))
|
||||
|
||||
} catch {
|
||||
os_log(.error, log: self.log, "Error occurred getting pending status records: %@", error.localizedDescription)
|
||||
os_log(.error, log: self.log, "Error in CloudKitArticlesZoneDelegate.cloudKitDidModify: %@", error.localizedDescription)
|
||||
completion(.failure(CloudKitZoneError.unknown))
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +56,7 @@ final class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate {
|
|||
|
||||
private extension CloudKitArticlesZoneDelegate {
|
||||
|
||||
func delete(recordKeys: [CloudKitRecordKey], pendingStarredStatusArticleIDs: Set<String>) async {
|
||||
@MainActor func delete(recordKeys: [CloudKitRecordKey], pendingStarredStatusArticleIDs: Set<String>) async {
|
||||
|
||||
let receivedRecordIDs = recordKeys.filter({ $0.recordType == CloudKitArticlesZone.CloudKitArticleStatus.recordType }).map({ $0.recordID })
|
||||
let receivedArticleIDs = Set(receivedRecordIDs.map({ stripPrefix($0.externalID) }))
|
||||
|
@ -71,7 +70,7 @@ private extension CloudKitArticlesZoneDelegate {
|
|||
try? await account?.delete(articleIDs: deletableArticleIDs)
|
||||
}
|
||||
|
||||
@MainActor func update(records: [CKRecord], pendingReadStatusArticleIDs: Set<String>, pendingStarredStatusArticleIDs: Set<String>, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
@MainActor private func update(records: [CKRecord], pendingReadStatusArticleIDs: Set<String>, pendingStarredStatusArticleIDs: Set<String>) async throws {
|
||||
|
||||
let receivedUnreadArticleIDs = Set(records.filter({ $0[CloudKitArticlesZone.CloudKitArticleStatus.Fields.read] == "0" }).map({ stripPrefix($0.externalID) }))
|
||||
let receivedReadArticleIDs = Set(records.filter({ $0[CloudKitArticlesZone.CloudKitArticleStatus.Fields.read] == "1" }).map({ stripPrefix($0.externalID) }))
|
||||
|
@ -82,87 +81,76 @@ private extension CloudKitArticlesZoneDelegate {
|
|||
let updateableReadArticleIDs = receivedReadArticleIDs.subtracting(pendingReadStatusArticleIDs)
|
||||
let updateableUnstarredArticleIDs = receivedUnstarredArticleIDs.subtracting(pendingStarredStatusArticleIDs)
|
||||
let updateableStarredArticleIDs = receivedStarredArticleIDs.subtracting(pendingStarredStatusArticleIDs)
|
||||
|
||||
Task { @MainActor in
|
||||
|
||||
var errorOccurred = false
|
||||
var errorOccurred = false
|
||||
|
||||
do {
|
||||
try await account?.markAsUnread(updateableUnreadArticleIDs)
|
||||
} catch {
|
||||
errorOccurred = true
|
||||
os_log(.error, log: self.log, "Error occurred while storing unread statuses: %@", error.localizedDescription)
|
||||
}
|
||||
|
||||
do {
|
||||
try await account?.markAsRead(updateableReadArticleIDs)
|
||||
} catch {
|
||||
errorOccurred = true
|
||||
os_log(.error, log: self.log, "Error occurred while storing read statuses: %@", error.localizedDescription)
|
||||
}
|
||||
|
||||
do {
|
||||
try await account?.markAsUnstarred(updateableUnstarredArticleIDs)
|
||||
} catch {
|
||||
errorOccurred = true
|
||||
os_log(.error, log: self.log, "Error occurred while storing unstarred statuses: %@", error.localizedDescription)
|
||||
}
|
||||
|
||||
do {
|
||||
try await account?.markAsStarred(updateableStarredArticleIDs)
|
||||
} catch {
|
||||
errorOccurred = true
|
||||
os_log(.error, log: self.log, "Error occurred while storing starred statuses: %@", error.localizedDescription)
|
||||
}
|
||||
|
||||
let parsedItems = await Self.makeParsedItems(records)
|
||||
let feedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ).mapValues { Set($0) }
|
||||
|
||||
for (feedID, parsedItems) in feedIDsAndItems {
|
||||
|
||||
do {
|
||||
try await account?.markAsUnread(updateableUnreadArticleIDs)
|
||||
} catch {
|
||||
errorOccurred = true
|
||||
os_log(.error, log: self.log, "Error occurred while storing unread statuses: %@", error.localizedDescription)
|
||||
}
|
||||
|
||||
do {
|
||||
try await account?.markAsRead(updateableReadArticleIDs)
|
||||
} catch {
|
||||
errorOccurred = true
|
||||
os_log(.error, log: self.log, "Error occurred while storing read statuses: %@", error.localizedDescription)
|
||||
}
|
||||
|
||||
do {
|
||||
try await account?.markAsUnstarred(updateableUnstarredArticleIDs)
|
||||
} catch {
|
||||
errorOccurred = true
|
||||
os_log(.error, log: self.log, "Error occurred while storing unstarred statuses: %@", error.localizedDescription)
|
||||
}
|
||||
|
||||
do {
|
||||
try await account?.markAsStarred(updateableStarredArticleIDs)
|
||||
} catch {
|
||||
errorOccurred = true
|
||||
os_log(.error, log: self.log, "Error occurred while storing starred statuses: %@", error.localizedDescription)
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
|
||||
compressionQueue.async {
|
||||
let parsedItems = records.compactMap { Self.makeParsedItem($0) }
|
||||
let feedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ).mapValues { Set($0) }
|
||||
|
||||
Task { @MainActor in
|
||||
for (feedID, parsedItems) in feedIDsAndItems {
|
||||
group.enter()
|
||||
|
||||
do {
|
||||
|
||||
let articleChanges = try await self.account?.update(feedID: feedID, with: parsedItems, deleteOlder: false)
|
||||
|
||||
guard let deletes = articleChanges?.deletedArticles, !deletes.isEmpty else {
|
||||
group.leave()
|
||||
return
|
||||
}
|
||||
|
||||
let syncStatuses = deletes.map { SyncStatus(articleID: $0.articleID, key: .deleted, flag: true) }
|
||||
try? await self.database.insertStatuses(syncStatuses)
|
||||
group.leave()
|
||||
|
||||
} catch {
|
||||
errorOccurred = true
|
||||
os_log(.error, log: self.log, "Error occurred while storing articles: %@", error.localizedDescription)
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
group.leave()
|
||||
let articleChanges = try await self.account?.update(feedID: feedID, with: parsedItems, deleteOlder: false)
|
||||
guard let deletes = articleChanges?.deletedArticles, !deletes.isEmpty else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
if errorOccurred {
|
||||
completion(.failure(CloudKitZoneError.unknown))
|
||||
} else {
|
||||
completion(.success(()))
|
||||
}
|
||||
let syncStatuses = deletes.map { SyncStatus(articleID: $0.articleID, key: .deleted, flag: true) }
|
||||
try? await self.database.insertStatuses(syncStatuses)
|
||||
|
||||
} catch {
|
||||
errorOccurred = true
|
||||
os_log(.error, log: self.log, "Error occurred while storing articles: %@", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
if errorOccurred {
|
||||
throw CloudKitZoneError.unknown
|
||||
}
|
||||
}
|
||||
|
||||
func stripPrefix(_ externalID: String) -> String {
|
||||
return String(externalID[externalID.index(externalID.startIndex, offsetBy: 2)..<externalID.endIndex])
|
||||
}
|
||||
|
||||
private static func makeParsedItems(_ articleRecords: [CKRecord]) async -> Set<ParsedItem> {
|
||||
|
||||
let task = Task.detached { () -> Set<ParsedItem> in
|
||||
let parsedItems = articleRecords.compactMap { makeParsedItem($0) }
|
||||
return Set(parsedItems)
|
||||
}
|
||||
|
||||
return await task.value
|
||||
}
|
||||
|
||||
static func makeParsedItem(_ articleRecord: CKRecord) -> ParsedItem? {
|
||||
guard articleRecord.recordType == CloudKitArticlesZone.CloudKitArticle.recordType else {
|
||||
return nil
|
||||
|
|
|
@ -445,32 +445,59 @@ private extension FeedbinAccountDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
private func delay(seconds: Double) async {
|
||||
|
||||
await withCheckedContinuation { continuation in
|
||||
self.performBlockAfter(seconds: seconds) {
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated private func performBlockAfter(seconds: Double, block: @escaping @Sendable @MainActor () -> ()) {
|
||||
|
||||
let delayTime = DispatchTime.now() + seconds
|
||||
DispatchQueue.main.asyncAfter(deadline: delayTime) {
|
||||
Task { @MainActor in
|
||||
block()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkImportResult(opmlImportResultID: Int, completion: @escaping @Sendable (Result<Void, Error>) -> Void) {
|
||||
|
||||
DispatchQueue.main.async {
|
||||
Task { @MainActor in
|
||||
|
||||
Timer.scheduledTimer(withTimeInterval: 15, repeats: true) { timer in
|
||||
var retry = 0
|
||||
let maxRetries = 6 // a guess at a good number
|
||||
|
||||
Task { @MainActor in
|
||||
@MainActor func checkResult() async {
|
||||
|
||||
os_log(.debug, log: self.log, "Checking status of OPML import...")
|
||||
if retry >= maxRetries {
|
||||
return
|
||||
}
|
||||
retry = retry + 1
|
||||
|
||||
do {
|
||||
let importResult = try await self.caller.retrieveOPMLImportResult(importID: opmlImportResultID)
|
||||
await delay(seconds: 15)
|
||||
os_log(.debug, log: self.log, "Checking status of OPML import...")
|
||||
|
||||
if let importResult, importResult.complete {
|
||||
os_log(.debug, log: self.log, "Checking status of OPML import successfully completed.")
|
||||
timer.invalidate()
|
||||
completion(.success(()))
|
||||
}
|
||||
do {
|
||||
let importResult = try await self.caller.retrieveOPMLImportResult(importID: opmlImportResultID)
|
||||
|
||||
} catch {
|
||||
os_log(.debug, log: self.log, "Import OPML check failed.")
|
||||
timer.invalidate()
|
||||
completion(.failure(error))
|
||||
if let importResult, importResult.complete {
|
||||
os_log(.debug, log: self.log, "Checking status of OPML import successfully completed.")
|
||||
completion(.success(()))
|
||||
} else {
|
||||
await checkResult()
|
||||
}
|
||||
|
||||
} catch {
|
||||
os_log(.debug, log: self.log, "Import OPML check failed.")
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
await checkResult()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,10 +15,7 @@ import AppKit
|
|||
public final class UserApp {
|
||||
|
||||
public let bundleID: String
|
||||
public var icon: NSImage? = nil
|
||||
public var existsOnDisk = false
|
||||
public var path: String? = nil
|
||||
public var runningApplication: NSRunningApplication? = nil
|
||||
|
||||
public var isRunning: Bool {
|
||||
|
||||
|
@ -29,6 +26,10 @@ public final class UserApp {
|
|||
return false
|
||||
}
|
||||
|
||||
private var icon: NSImage? = nil
|
||||
private var path: String? = nil
|
||||
private var runningApplication: NSRunningApplication? = nil
|
||||
|
||||
public init(bundleID: String) {
|
||||
|
||||
self.bundleID = bundleID
|
||||
|
|
|
@ -29,7 +29,7 @@ public protocol SendToCommand {
|
|||
/// The image for the command.
|
||||
///
|
||||
/// Often the icon of the target application.
|
||||
var image: RSImage? { get }
|
||||
@MainActor var image: RSImage? { get }
|
||||
|
||||
/// Determine whether an object can be sent to the target application.
|
||||
///
|
||||
|
@ -37,7 +37,7 @@ public protocol SendToCommand {
|
|||
/// - object: The object to test.
|
||||
/// - selectedText: The currently selected text.
|
||||
/// - Returns: `true` if the object can be sent, `false` otherwise.
|
||||
func canSendObject(_ object: Any?, selectedText: String?) -> Bool
|
||||
@MainActor func canSendObject(_ object: Any?, selectedText: String?) -> Bool
|
||||
|
||||
/// Send an object to the target application.
|
||||
///
|
||||
|
|
|
@ -20,7 +20,7 @@ public extension Notification.Name {
|
|||
|
||||
public protocol FeedIconDownloaderDelegate: Sendable {
|
||||
|
||||
var appIconImage: IconImage? { get }
|
||||
@MainActor var appIconImage: IconImage? { get }
|
||||
|
||||
func downloadMetadata(_ url: String) async throws -> RSHTMLMetadata?
|
||||
}
|
||||
|
|
|
@ -11,14 +11,14 @@ import WebKit
|
|||
import Articles
|
||||
|
||||
final class DetailIconSchemeHandler: NSObject, WKURLSchemeHandler {
|
||||
|
||||
|
||||
var currentArticle: Article?
|
||||
|
||||
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
|
||||
|
||||
Task { @MainActor in
|
||||
MainActor.assumeIsolated {
|
||||
|
||||
guard let responseURL = urlSchemeTask.request.url, let iconImage = self.currentArticle?.iconImage() else {
|
||||
guard let responseURL = urlSchemeTask.request.url, let iconImage = self.currentArticle?.iconImage() else {
|
||||
urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist))
|
||||
return
|
||||
}
|
||||
|
|
|
@ -780,7 +780,7 @@ extension MainWindowController: NSToolbarDelegate {
|
|||
|
||||
case .sidebarToggle:
|
||||
let title = NSLocalizedString("Toggle Sidebar", comment: "Toggle Sidebar")
|
||||
return buildToolbarButton(.toggleSidebar, title, AppAssets.sidebarToggleImage, "toggleTheSidebar:")
|
||||
return buildToolbarButton(.sidebarToggle, title, AppAssets.sidebarToggleImage, "toggleTheSidebar:")
|
||||
|
||||
case .refresh:
|
||||
let title = NSLocalizedString("Refresh", comment: "Refresh")
|
||||
|
|
|
@ -18,15 +18,18 @@ import Core
|
|||
}
|
||||
|
||||
func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, sharingServicesForItems items: [Any], proposedSharingServices proposedServices: [NSSharingService]) -> [NSSharingService] {
|
||||
let filteredServices = proposedServices.filter { $0.menuItemTitle != "NetNewsWire" }
|
||||
return filteredServices + SharingServicePickerDelegate.customSharingServices(for: items)
|
||||
|
||||
MainActor.assumeIsolated {
|
||||
let filteredServices = proposedServices.filter { $0.menuItemTitle != "NetNewsWire" }
|
||||
return filteredServices + SharingServicePickerDelegate.customSharingServices(for: items)
|
||||
}
|
||||
}
|
||||
|
||||
func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, delegateFor sharingService: NSSharingService) -> NSSharingServiceDelegate? {
|
||||
return sharingServiceDelegate
|
||||
}
|
||||
|
||||
static func customSharingServices(for items: [Any]) -> [NSSharingService] {
|
||||
@MainActor static func customSharingServices(for items: [Any]) -> [NSSharingService] {
|
||||
let customServices: [SendToCommand] = [SendToMarsEditCommand(), SendToMicroBlogCommand()]
|
||||
|
||||
return customServices.compactMap { (sendToCommand) -> NSSharingService? in
|
||||
|
|
|
@ -7,8 +7,9 @@
|
|||
//
|
||||
|
||||
import SafariServices
|
||||
import os
|
||||
|
||||
class SafariExtensionHandler: SFSafariExtensionHandler {
|
||||
final class SafariExtensionHandler: SFSafariExtensionHandler {
|
||||
|
||||
// Safari App Extensions don't support any reasonable means of detecting whether a
|
||||
// specific Safari page was loaded with the benefit of the extension's injected
|
||||
|
@ -19,8 +20,9 @@ class SafariExtensionHandler: SFSafariExtensionHandler {
|
|||
|
||||
// I tried to use a NSMapTable from String to the closure directly, but Swift
|
||||
// complains that the object has to be a class type.
|
||||
typealias ValidationHandler = (Bool, String) -> Void
|
||||
class ValidationWrapper {
|
||||
typealias ValidationHandler = @Sendable (Bool, String) -> Void
|
||||
|
||||
final class ValidationWrapper: Sendable {
|
||||
let validationHandler: ValidationHandler
|
||||
|
||||
init(validationHandler: @escaping ValidationHandler) {
|
||||
|
@ -29,7 +31,16 @@ class SafariExtensionHandler: SFSafariExtensionHandler {
|
|||
}
|
||||
|
||||
// Maps from UUID to a validation wrapper
|
||||
static var gPingPongMap = Dictionary<String, ValidationWrapper>()
|
||||
private static let _gPingPongMap: OSAllocatedUnfairLock<Dictionary<String, ValidationWrapper>> = OSAllocatedUnfairLock(initialState: [String: ValidationWrapper]())
|
||||
static var gPingPongMap: [String: ValidationWrapper] {
|
||||
get {
|
||||
_gPingPongMap.withLock { $0 }
|
||||
}
|
||||
set {
|
||||
_gPingPongMap.withLock { $0 = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
static let validationQueue = DispatchQueue(label: "Toolbar Validation")
|
||||
|
||||
// Bottleneck for calling through to a validation handler we have saved, and removing it from the list.
|
||||
|
@ -79,7 +90,7 @@ class SafariExtensionHandler: SFSafariExtensionHandler {
|
|||
}
|
||||
}
|
||||
|
||||
override func validateToolbarItem(in window: SFSafariWindow, validationHandler: @escaping ((Bool, String) -> Void)) {
|
||||
override func validateToolbarItem(in window: SFSafariWindow, validationHandler: @escaping (Bool, String) -> Void) {
|
||||
|
||||
let uniqueValidationID = NSUUID().uuidString
|
||||
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
import Foundation
|
||||
|
||||
protocol ScriptingObject {
|
||||
var objectSpecifier: NSScriptObjectSpecifier? { get }
|
||||
|
||||
@MainActor var objectSpecifier: NSScriptObjectSpecifier? { get }
|
||||
@MainActor var scriptingKey: String { get }
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ final class ShareViewController: NSViewController {
|
|||
@IBOutlet weak var nameTextField: NSTextField!
|
||||
@IBOutlet weak var folderPopUpButton: NSPopUpButton!
|
||||
|
||||
private var url: URL?
|
||||
nonisolated(unsafe) private var url: URL?
|
||||
private var extensionContainers: ExtensionContainers?
|
||||
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "ShareViewController")
|
||||
|
||||
|
|
|
@ -18,6 +18,6 @@ extension AppDelegate: FaviconDownloaderDelegate, FeedIconDownloaderDelegate {
|
|||
|
||||
func downloadMetadata(_ url: String) async throws -> RSHTMLMetadata? {
|
||||
|
||||
try await HTMLMetadataDownloader.downloadMetadata(for: url)
|
||||
await HTMLMetadataDownloader.downloadMetadata(for: url)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
import AppKit
|
||||
import Articles
|
||||
import Core
|
||||
import AppKitExtras
|
||||
@preconcurrency import AppKitExtras
|
||||
|
||||
final class SendToMarsEditCommand: SendToCommand {
|
||||
@MainActor final class SendToMarsEditCommand: SendToCommand {
|
||||
|
||||
let title = "MarsEdit"
|
||||
let image: RSImage? = AppAssets.marsEditIcon
|
||||
|
@ -44,9 +44,9 @@ final class SendToMarsEditCommand: SendToCommand {
|
|||
}
|
||||
}
|
||||
|
||||
private extension SendToMarsEditCommand {
|
||||
@MainActor private extension SendToMarsEditCommand {
|
||||
|
||||
@MainActor func send(_ article: Article, to app: UserApp) {
|
||||
func send(_ article: Article, to app: UserApp) {
|
||||
|
||||
// App has already been launched.
|
||||
|
||||
|
|
|
@ -9,11 +9,11 @@
|
|||
import AppKit
|
||||
import Articles
|
||||
import Core
|
||||
import AppKitExtras
|
||||
@preconcurrency import AppKitExtras
|
||||
|
||||
// Not undoable.
|
||||
|
||||
final class SendToMicroBlogCommand: SendToCommand {
|
||||
@MainActor final class SendToMicroBlogCommand: SendToCommand {
|
||||
|
||||
let title = "Micro.blog"
|
||||
let image: RSImage? = AppAssets.microblogIcon
|
||||
|
@ -30,7 +30,7 @@ final class SendToMicroBlogCommand: SendToCommand {
|
|||
return true
|
||||
}
|
||||
|
||||
@MainActor func sendObject(_ object: Any?, selectedText: String?) {
|
||||
func sendObject(_ object: Any?, selectedText: String?) {
|
||||
|
||||
guard canSendObject(object, selectedText: selectedText) else {
|
||||
return
|
||||
|
|
|
@ -12,9 +12,9 @@ import Web
|
|||
|
||||
struct CacheCleaner {
|
||||
|
||||
static let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CacheCleaner")
|
||||
nonisolated(unsafe) static let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CacheCleaner")
|
||||
|
||||
static func purgeIfNecessary() {
|
||||
@MainActor static func purgeIfNecessary() {
|
||||
|
||||
guard let flushDate = AppDefaults.shared.lastImageCacheFlushDate else {
|
||||
AppDefaults.shared.lastImageCacheFlushDate = Date()
|
||||
|
|
|
@ -12,7 +12,7 @@ import Account
|
|||
|
||||
final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter, Sendable {
|
||||
|
||||
private static let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "extensionFeedAddRequestFile")
|
||||
nonisolated(unsafe) private static let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "extensionFeedAddRequestFile")
|
||||
|
||||
private static let filePath: String = {
|
||||
let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String
|
||||
|
|
|
@ -10,7 +10,7 @@ import Foundation
|
|||
|
||||
struct ShareDefaultContainer {
|
||||
|
||||
static func defaultContainer(containers: ExtensionContainers) -> ExtensionContainer? {
|
||||
@MainActor static func defaultContainer(containers: ExtensionContainers) -> ExtensionContainer? {
|
||||
|
||||
if let accountID = AppDefaults.shared.addFeedAccountID, let account = containers.accounts.first(where: { $0.accountID == accountID }) {
|
||||
if let folderName = AppDefaults.shared.addFeedFolderName, let folder = account.folders.first(where: { $0.name == folderName }) {
|
||||
|
@ -26,7 +26,7 @@ struct ShareDefaultContainer {
|
|||
|
||||
}
|
||||
|
||||
static func saveDefaultContainer(_ container: ExtensionContainer) {
|
||||
@MainActor static func saveDefaultContainer(_ container: ExtensionContainer) {
|
||||
AppDefaults.shared.addFeedAccountID = container.accountID
|
||||
if let folder = container as? ExtensionFolder {
|
||||
AppDefaults.shared.addFeedFolderName = folder.name
|
||||
|
|
|
@ -50,7 +50,7 @@ extension Array where Element == Article {
|
|||
})
|
||||
}
|
||||
|
||||
func sortedByDate(_ sortDirection: ComparisonResult, groupByFeed: Bool = false) -> ArticleArray {
|
||||
@MainActor func sortedByDate(_ sortDirection: ComparisonResult, groupByFeed: Bool = false) -> ArticleArray {
|
||||
return ArticleSorter.sortedByDate(articles: self, sortDirection: sortDirection, groupByFeed: groupByFeed)
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import Foundation
|
|||
|
||||
protocol SortableArticle {
|
||||
|
||||
var sortableName: String { get }
|
||||
@MainActor var sortableName: String { get }
|
||||
var sortableDate: Date { get }
|
||||
var sortableArticleID: String { get }
|
||||
var sortableFeedID: String { get }
|
||||
|
@ -19,7 +19,7 @@ protocol SortableArticle {
|
|||
|
||||
struct ArticleSorter {
|
||||
|
||||
static func sortedByDate<T: SortableArticle>(articles: [T],
|
||||
@MainActor static func sortedByDate<T: SortableArticle>(articles: [T],
|
||||
sortDirection: ComparisonResult,
|
||||
groupByFeed: Bool) -> [T] {
|
||||
if groupByFeed {
|
||||
|
@ -31,7 +31,7 @@ struct ArticleSorter {
|
|||
|
||||
// MARK: -
|
||||
|
||||
private static func sortedByFeedName<T: SortableArticle>(articles: [T],
|
||||
@MainActor private static func sortedByFeedName<T: SortableArticle>(articles: [T],
|
||||
sortByDateDirection: ComparisonResult) -> [T] {
|
||||
// Group articles by "feed-feedID" - feed ID is used to differentiate between
|
||||
// two feeds that have the same name
|
||||
|
|
|
@ -24,7 +24,7 @@ public final class WidgetDataEncoder {
|
|||
private lazy var containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup)
|
||||
private lazy var dataURL = containerURL?.appendingPathComponent("widget-data.json")
|
||||
|
||||
static let shared = WidgetDataEncoder()
|
||||
@MainActor static let shared = WidgetDataEncoder()
|
||||
private init () {}
|
||||
|
||||
func encodeWidgetData() throws {
|
||||
|
|
|
@ -11,10 +11,11 @@ import Account
|
|||
import Core
|
||||
|
||||
protocol AddFeedFolderViewControllerDelegate {
|
||||
func didSelect(container: Container)
|
||||
|
||||
@MainActor func didSelect(container: Container)
|
||||
}
|
||||
|
||||
class AddFeedFolderViewController: UITableViewController {
|
||||
final class AddFeedFolderViewController: UITableViewController {
|
||||
|
||||
var delegate: AddFeedFolderViewControllerDelegate?
|
||||
var addFeedType = AddFeedType.web
|
||||
|
|
|
@ -17,7 +17,7 @@ enum AddFeedType {
|
|||
case web
|
||||
}
|
||||
|
||||
class AddFeedViewController: UITableViewController {
|
||||
final class AddFeedViewController: UITableViewController {
|
||||
|
||||
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
|
||||
@IBOutlet weak var addButton: UIBarButtonItem!
|
||||
|
@ -166,6 +166,7 @@ class AddFeedViewController: UITableViewController {
|
|||
// MARK: AddFeedFolderViewControllerDelegate
|
||||
|
||||
extension AddFeedViewController: AddFeedFolderViewControllerDelegate {
|
||||
|
||||
func didSelect(container: Container) {
|
||||
self.container = container
|
||||
updateFolderLabel()
|
||||
|
|
|
@ -31,7 +31,7 @@ final class AppDefaults {
|
|||
|
||||
static let defaultThemeName = "Defaults"
|
||||
|
||||
static let shared = AppDefaults()
|
||||
nonisolated(unsafe) static let shared = AppDefaults()
|
||||
private init() {}
|
||||
|
||||
static let store: UserDefaults = {
|
||||
|
|
|
@ -37,7 +37,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
}
|
||||
}
|
||||
|
||||
var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application")
|
||||
nonisolated(unsafe) let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application")
|
||||
|
||||
var userNotificationManager: UserNotificationManager!
|
||||
var faviconDownloader: FaviconDownloader!
|
||||
|
@ -204,11 +204,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
}
|
||||
}
|
||||
|
||||
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([.list, .banner, .badge, .sound])
|
||||
}
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
defer { completionHandler() }
|
||||
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
|
@ -390,7 +391,9 @@ private extension AppDelegate {
|
|||
do {
|
||||
try BGTaskScheduler.shared.submit(request)
|
||||
} catch {
|
||||
os_log(.error, log: self.log, "Could not schedule app refresh: %@", error.localizedDescription)
|
||||
Task { @MainActor in
|
||||
os_log(.error, log: self.log, "Could not schedule app refresh: %@", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
import WebKit
|
||||
import Images
|
||||
@preconcurrency import Images
|
||||
|
||||
protocol ArticleIconSchemeHandlerDelegate: AnyObject {
|
||||
|
||||
|
|
|
@ -8,11 +8,11 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
@objc protocol SearchBarDelegate: NSObjectProtocol {
|
||||
@objc optional func nextWasPressed(_ searchBar: ArticleSearchBar)
|
||||
@objc optional func previousWasPressed(_ searchBar: ArticleSearchBar)
|
||||
@objc optional func doneWasPressed(_ searchBar: ArticleSearchBar)
|
||||
@objc optional func searchBar(_ searchBar: ArticleSearchBar, textDidChange: String)
|
||||
protocol SearchBarDelegate: AnyObject {
|
||||
@MainActor func nextWasPressed(_ searchBar: ArticleSearchBar)
|
||||
@MainActor func previousWasPressed(_ searchBar: ArticleSearchBar)
|
||||
@MainActor func doneWasPressed(_ searchBar: ArticleSearchBar)
|
||||
@MainActor func searchBar(_ searchBar: ArticleSearchBar, textDidChange: String)
|
||||
}
|
||||
|
||||
|
||||
|
@ -146,7 +146,7 @@ private extension ArticleSearchBar {
|
|||
private extension ArticleSearchBar {
|
||||
|
||||
@objc func textDidChange(_ notification: Notification) {
|
||||
delegate?.searchBar?(self, textDidChange: searchField.text ?? "")
|
||||
delegate?.searchBar(self, textDidChange: searchField.text ?? "")
|
||||
|
||||
if searchField.text?.isEmpty ?? true {
|
||||
searchField.rightViewMode = .never
|
||||
|
@ -156,21 +156,21 @@ private extension ArticleSearchBar {
|
|||
}
|
||||
|
||||
@objc func nextPressed() {
|
||||
delegate?.nextWasPressed?(self)
|
||||
delegate?.nextWasPressed(self)
|
||||
}
|
||||
|
||||
@objc func previousPressed() {
|
||||
delegate?.previousWasPressed?(self)
|
||||
delegate?.previousWasPressed(self)
|
||||
}
|
||||
|
||||
@objc func donePressed(_ _: Any? = nil) {
|
||||
delegate?.doneWasPressed?(self)
|
||||
delegate?.doneWasPressed(self)
|
||||
}
|
||||
}
|
||||
|
||||
extension ArticleSearchBar: UITextFieldDelegate {
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
delegate?.nextWasPressed?(self)
|
||||
delegate?.nextWasPressed(self)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,8 +9,9 @@
|
|||
import UIKit
|
||||
|
||||
@objc public protocol ImageScrollViewDelegate: UIScrollViewDelegate {
|
||||
func imageScrollViewDidGestureSwipeUp(imageScrollView: ImageScrollView)
|
||||
func imageScrollViewDidGestureSwipeDown(imageScrollView: ImageScrollView)
|
||||
|
||||
@MainActor func imageScrollViewDidGestureSwipeUp(imageScrollView: ImageScrollView)
|
||||
@MainActor func imageScrollViewDidGestureSwipeDown(imageScrollView: ImageScrollView)
|
||||
}
|
||||
|
||||
open class ImageScrollView: UIScrollView {
|
||||
|
|
|
@ -42,8 +42,9 @@ class OpenInBrowserActivity: UIActivity {
|
|||
return
|
||||
}
|
||||
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
Task { @MainActor in
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
}
|
||||
activityDidFinish(true)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -17,10 +17,11 @@ import ArticleExtractor
|
|||
import Images
|
||||
|
||||
protocol WebViewControllerDelegate: AnyObject {
|
||||
func webViewController(_: WebViewController, articleExtractorButtonStateDidUpdate: ArticleExtractorButtonState)
|
||||
|
||||
@MainActor func webViewController(_: WebViewController, articleExtractorButtonStateDidUpdate: ArticleExtractorButtonState)
|
||||
}
|
||||
|
||||
class WebViewController: UIViewController {
|
||||
final class WebViewController: UIViewController {
|
||||
|
||||
private struct MessageName {
|
||||
static let imageWasClicked = "imageWasClicked"
|
||||
|
|
|
@ -11,9 +11,10 @@ import os.log
|
|||
|
||||
struct ErrorHandler {
|
||||
|
||||
private static var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application")
|
||||
nonisolated(unsafe) private static let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application")
|
||||
|
||||
public static func present(_ viewController: UIViewController) -> (Error) -> () {
|
||||
public static func present(_ viewController: UIViewController) -> @MainActor (Error) -> () {
|
||||
|
||||
return { @MainActor [weak viewController] error in
|
||||
if UIApplication.shared.applicationState == .active {
|
||||
viewController?.presentError(error)
|
||||
|
|
|
@ -12,7 +12,8 @@ import Tree
|
|||
import Images
|
||||
|
||||
protocol FeedTableViewCellDelegate: AnyObject {
|
||||
func feedTableViewCellDisclosureDidToggle(_ sender: FeedTableViewCell, expanding: Bool)
|
||||
|
||||
@MainActor func feedTableViewCellDisclosureDidToggle(_ sender: FeedTableViewCell, expanding: Bool)
|
||||
}
|
||||
|
||||
class FeedTableViewCell : VibrantTableViewCell {
|
||||
|
|
|
@ -10,7 +10,8 @@ import UIKit
|
|||
import UIKitExtras
|
||||
|
||||
protocol FeedTableViewSectionHeaderDelegate {
|
||||
func FeedTableViewSectionHeaderDisclosureDidToggle(_ sender: FeedTableViewSectionHeader)
|
||||
|
||||
@MainActor func feedTableViewSectionHeaderDisclosureDidToggle(_ sender: FeedTableViewSectionHeader)
|
||||
}
|
||||
|
||||
class FeedTableViewSectionHeader: UITableViewHeaderFooterView {
|
||||
|
@ -139,7 +140,7 @@ class FeedTableViewSectionHeader: UITableViewHeaderFooterView {
|
|||
private extension FeedTableViewSectionHeader {
|
||||
|
||||
@objc func toggleDisclosure() {
|
||||
delegate?.FeedTableViewSectionHeaderDisclosureDidToggle(self)
|
||||
delegate?.feedTableViewSectionHeaderDisclosureDidToggle(self)
|
||||
}
|
||||
|
||||
func commonInit() {
|
||||
|
|
|
@ -669,7 +669,7 @@ extension SidebarViewController: UIContextMenuInteractionDelegate {
|
|||
|
||||
extension SidebarViewController: FeedTableViewSectionHeaderDelegate {
|
||||
|
||||
func FeedTableViewSectionHeaderDisclosureDidToggle(_ sender: FeedTableViewSectionHeader) {
|
||||
func feedTableViewSectionHeaderDisclosureDidToggle(_ sender: FeedTableViewSectionHeader) {
|
||||
toggle(sender)
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,8 @@ import UIKit
|
|||
import Core
|
||||
|
||||
protocol AddAccountDismissDelegate: UIViewController {
|
||||
func dismiss()
|
||||
|
||||
@MainActor func dismiss()
|
||||
}
|
||||
|
||||
class AddAccountViewController: UITableViewController, AddAccountDismissDelegate {
|
||||
|
|
|
@ -10,7 +10,8 @@ import UIKit
|
|||
import Account
|
||||
|
||||
protocol ShareFolderPickerControllerDelegate: AnyObject {
|
||||
func shareFolderPickerDidSelect(_ container: ExtensionContainer)
|
||||
|
||||
@MainActor func shareFolderPickerDidSelect(_ container: ExtensionContainer)
|
||||
}
|
||||
|
||||
class ShareFolderPickerController: UITableViewController {
|
||||
|
|
|
@ -12,7 +12,7 @@ import UIKit
|
|||
// Uses a cache.
|
||||
// Main thready only.
|
||||
|
||||
final class SingleLineUILabelSizer {
|
||||
@MainActor final class SingleLineUILabelSizer {
|
||||
|
||||
let font: UIFont
|
||||
private var cache = [String: CGSize]()
|
||||
|
@ -36,7 +36,7 @@ final class SingleLineUILabelSizer {
|
|||
|
||||
}
|
||||
|
||||
static private var sizers = [UIFont: SingleLineUILabelSizer]()
|
||||
@MainActor static private var sizers = [UIFont: SingleLineUILabelSizer]()
|
||||
|
||||
static func sizer(for font: UIFont) -> SingleLineUILabelSizer {
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ struct TimelineAccessibilityCellLayout: TimelineCellLayout {
|
|||
|
||||
private extension TimelineAccessibilityCellLayout {
|
||||
|
||||
static func rectForDate(_ cellData: TimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect {
|
||||
@MainActor static func rectForDate(_ cellData: TimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect {
|
||||
|
||||
var r = CGRect.zero
|
||||
|
||||
|
|
|
@ -94,7 +94,7 @@ extension TimelineCellLayout {
|
|||
|
||||
}
|
||||
|
||||
static func rectForFeedName(_ cellData: TimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect {
|
||||
@MainActor static func rectForFeedName(_ cellData: TimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect {
|
||||
|
||||
var r = CGRect.zero
|
||||
r.origin = point
|
||||
|
|
|
@ -109,7 +109,7 @@ struct TimelineDefaultCellLayout: TimelineCellLayout {
|
|||
|
||||
extension TimelineDefaultCellLayout {
|
||||
|
||||
static func rectForDate(_ cellData: TimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect {
|
||||
@MainActor static func rectForDate(_ cellData: TimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect {
|
||||
|
||||
var r = CGRect.zero
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
class TimelineTitleView: UIView {
|
||||
final class TimelineTitleView: UIView {
|
||||
|
||||
@IBOutlet weak var iconView: IconView!
|
||||
@IBOutlet weak var label: UILabel!
|
||||
|
|
|
@ -44,7 +44,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 17.0
|
|||
//SDKROOT = macosx
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Off
|
||||
SWIFT_VERSION = 5.10
|
||||
SWIFT_STRICT_CONCURRENCY = complete
|
||||
SWIFT_STRICT_CONCURRENCY = targeted
|
||||
ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS = NO
|
||||
|
||||
// https://forums.swift.org/t/swift-packages-in-multiple-targets-results-in-this-will-result-in-duplication-of-library-code-errors/34892/33
|
||||
|
|
|
@ -8,3 +8,4 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
|
|||
COMBINE_HIDPI_IMAGES = YES
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0
|
||||
SDKROOT = macosx;
|
||||
SWIFT_STRICT_CONCURRENCY = complete
|
||||
|
|
Loading…
Reference in New Issue