mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Merge branch 'main' of https://github.com/vincode-io/NetNewsWire
This commit is contained in:
@@ -12,7 +12,7 @@ let package = Package(
|
||||
targets: ["Account"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "2.0.0")),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")),
|
||||
|
||||
@@ -428,23 +428,16 @@ public enum FetchType {
|
||||
await delegate.receiveRemoteNotification(for: self, userInfo: userInfo)
|
||||
}
|
||||
|
||||
public func refreshAll(completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.refreshAll(for: self, completion: completion)
|
||||
public func refreshAll() async throws {
|
||||
try await delegate.refreshAll(for: self)
|
||||
}
|
||||
|
||||
public func sendArticleStatus(completion: ((Result<Void, Error>) -> Void)? = nil) {
|
||||
delegate.sendArticleStatus(for: self) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion?(.success(()))
|
||||
case .failure(let error):
|
||||
completion?(.failure(error))
|
||||
}
|
||||
}
|
||||
public func sendArticleStatus() async throws {
|
||||
try await delegate.sendArticleStatus(for: self)
|
||||
}
|
||||
|
||||
public func syncArticleStatus(completion: ((Result<Void, Error>) -> Void)? = nil) {
|
||||
delegate.syncArticleStatus(for: self, completion: completion)
|
||||
public func syncArticleStatus() async throws {
|
||||
try await delegate.syncArticleStatus(for: self)
|
||||
}
|
||||
|
||||
public func importOPML(_ opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
@@ -453,18 +446,23 @@ public enum FetchType {
|
||||
return
|
||||
}
|
||||
|
||||
delegate.importOPML(for: self, opmlFile: opmlFile) { [weak self] result in
|
||||
switch result {
|
||||
case .success:
|
||||
guard let self = self else { return }
|
||||
// Reset the last fetch date to get the article history for the added feeds.
|
||||
self.metadata.lastArticleFetchStartTime = nil
|
||||
self.delegate.refreshAll(for: self, completion: completion)
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
delegate.importOPML(for: self, opmlFile: opmlFile) { result in
|
||||
Task { @MainActor in
|
||||
switch result {
|
||||
case .success:
|
||||
// Reset the last fetch date to get the article history for the added feeds.
|
||||
self.metadata.lastArticleFetchStartTime = nil
|
||||
do {
|
||||
try await self.delegate.refreshAll(for: self)
|
||||
completion(result)
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
case .failure:
|
||||
completion(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public func suspendNetwork() {
|
||||
|
||||
@@ -25,10 +25,10 @@ import Secrets
|
||||
|
||||
func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any]) async
|
||||
|
||||
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func syncArticleStatus(for account: Account, completion: ((Result<Void, Error>) -> Void)?)
|
||||
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void))
|
||||
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void))
|
||||
func refreshAll(for account: Account) async throws
|
||||
func syncArticleStatus(for account: Account) async throws
|
||||
func sendArticleStatus(for account: Account) async throws
|
||||
func refreshArticleStatus(for account: Account) async throws
|
||||
|
||||
func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ public enum FeedbinAccountDelegateError: String, Error {
|
||||
@MainActor final class FeedbinAccountDelegate: AccountDelegate, Logging {
|
||||
|
||||
private let database: SyncDatabase
|
||||
|
||||
|
||||
private let caller: FeedbinAPICaller
|
||||
|
||||
let behaviors: AccountBehaviors = [.disallowFeedCopyInRootFolder]
|
||||
@@ -75,58 +75,73 @@ public enum FeedbinAccountDelegateError: String, Error {
|
||||
return
|
||||
}
|
||||
|
||||
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
func refreshAll(for account: Account) async throws {
|
||||
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(5)
|
||||
|
||||
refreshAccount(account) { result in
|
||||
switch result {
|
||||
case .success():
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
refreshAccount(account) { result in
|
||||
switch result {
|
||||
case .success():
|
||||
|
||||
self.refreshArticlesAndStatuses(account) { result in
|
||||
switch result {
|
||||
case .success():
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
self.refreshProgress.clear()
|
||||
let wrappedError = WrappedAccountError(accountID: account.accountID, accountNameForDisplay: account.nameForDisplay, underlyingError: error)
|
||||
completion(.failure(wrappedError))
|
||||
self.refreshArticlesAndStatuses(account) { result in
|
||||
switch result {
|
||||
case .success():
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
Task { @MainActor in
|
||||
self.refreshProgress.clear()
|
||||
let wrappedError = WrappedAccountError(accountID: account.accountID, accountNameForDisplay: account.nameForDisplay, underlyingError: error)
|
||||
continuation.resume(throwing: wrappedError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
self.refreshProgress.clear()
|
||||
let wrappedError = WrappedAccountError(accountID: account.accountID, accountNameForDisplay: account.nameForDisplay, underlyingError: error)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func syncArticleStatus(for account: Account, completion: ((Result<Void, Error>) -> Void)? = nil) {
|
||||
sendArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion?(.success(()))
|
||||
case .failure(let error):
|
||||
completion?(.failure(error))
|
||||
case .failure(let error):
|
||||
Task { @MainActor in
|
||||
self.refreshProgress.clear()
|
||||
let wrappedError = WrappedAccountError(accountID: account.accountID, accountNameForDisplay: account.nameForDisplay, underlyingError: error)
|
||||
continuation.resume(throwing: wrappedError)
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
completion?(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func syncArticleStatus(for account: Account) async throws {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
sendArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
func sendArticleStatus(for account: Account) async throws {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
self.sendArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
|
||||
Task { @MainActor in
|
||||
logger.debug("Sending article statuses")
|
||||
@@ -188,7 +203,21 @@ public enum FeedbinAccountDelegateError: String, Error {
|
||||
}
|
||||
}
|
||||
|
||||
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
func refreshArticleStatus(for account: Account) async throws {
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
|
||||
logger.debug("Refreshing article statuses...")
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ public enum LocalAccountDelegateError: String, Error {
|
||||
final class LocalAccountDelegate: AccountDelegate, Logging {
|
||||
|
||||
weak var account: Account?
|
||||
|
||||
|
||||
private lazy var refresher: LocalAccountRefresher? = {
|
||||
let refresher = LocalAccountRefresher()
|
||||
refresher.delegate = self
|
||||
@@ -44,43 +44,46 @@ final class LocalAccountDelegate: AccountDelegate, Logging {
|
||||
return
|
||||
}
|
||||
|
||||
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func refreshAll(for account: Account) async throws {
|
||||
guard refreshProgress.isComplete else {
|
||||
completion(.success(()))
|
||||
return
|
||||
}
|
||||
|
||||
let feeds = account.flattenedFeeds()
|
||||
let feedURLs = Set(feeds.map{ $0.url })
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(feedURLs.count)
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
Task { @MainActor in
|
||||
let feeds = account.flattenedFeeds()
|
||||
let feedURLs = Set(feeds.map{ $0.url })
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(feedURLs.count)
|
||||
|
||||
let group = DispatchGroup()
|
||||
let group = DispatchGroup()
|
||||
|
||||
group.enter()
|
||||
refresher?.refreshFeedURLs(feedURLs) {
|
||||
group.leave()
|
||||
}
|
||||
group.enter()
|
||||
refresher?.refreshFeedURLs(feedURLs) {
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
self.refreshProgress.clear()
|
||||
account.metadata.lastArticleFetchEndTime = Date()
|
||||
completion(.success(()))
|
||||
}
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
self.refreshProgress.clear()
|
||||
account.metadata.lastArticleFetchEndTime = Date()
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func syncArticleStatus(for account: Account, completion: ((Result<Void, Error>) -> Void)? = nil) {
|
||||
completion?(.success(()))
|
||||
func syncArticleStatus(for account: Account) async throws {
|
||||
return
|
||||
}
|
||||
|
||||
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
completion(.success(()))
|
||||
func sendArticleStatus(for account: Account) async throws {
|
||||
return
|
||||
}
|
||||
|
||||
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
completion(.success(()))
|
||||
func refreshArticleStatus(for account: Account) async throws {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
var fileData: Data?
|
||||
|
||||
|
||||
@@ -469,9 +469,9 @@ extension NewsBlurAccountDelegate {
|
||||
|
||||
// Download the initial articles
|
||||
downloadFeed(account: account, feed: feed, page: 1) { result in
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
Task { @MainActor in
|
||||
do {
|
||||
try await self.refreshArticleStatus(for: account)
|
||||
self.refreshMissingStories(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
@@ -485,8 +485,7 @@ extension NewsBlurAccountDelegate {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging {
|
||||
return
|
||||
}
|
||||
|
||||
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
private func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
self.refreshProgress.addToNumberOfTasksAndRemaining(4)
|
||||
|
||||
refreshFeeds(for: account) { result in
|
||||
@@ -118,25 +118,54 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging {
|
||||
}
|
||||
}
|
||||
|
||||
func syncArticleStatus(for account: Account, completion: ((Result<Void, Error>) -> Void)? = nil) {
|
||||
sendArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion?(.success(()))
|
||||
case .failure(let error):
|
||||
completion?(.failure(error))
|
||||
}
|
||||
func refreshAll(for account: Account) async throws {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
self.refreshAll(for: account) { result in
|
||||
switch result {
|
||||
case .success():
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func syncArticleStatus(for account: Account) async throws {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
sendArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
case .failure(let error):
|
||||
completion?(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendArticleStatus(for account: Account, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
func sendArticleStatus(for account: Account) async throws {
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
self.sendArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func sendArticleStatus(for account: Account, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
Task { @MainActor in
|
||||
logger.debug("Sending story statuses")
|
||||
|
||||
@@ -208,7 +237,21 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging {
|
||||
}
|
||||
}
|
||||
|
||||
func refreshArticleStatus(for account: Account, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
func refreshArticleStatus(for account: Account) async throws {
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshArticleStatus(for account: Account, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
logger.debug("Refreshing story statuses...")
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
@@ -39,7 +39,7 @@ public enum ReaderAPIAccountDelegateError: LocalizedError {
|
||||
@MainActor final class ReaderAPIAccountDelegate: AccountDelegate, Logging {
|
||||
|
||||
private let variant: ReaderAPIVariant
|
||||
|
||||
|
||||
private let database: SyncDatabase
|
||||
|
||||
private let caller: ReaderAPICaller
|
||||
@@ -166,35 +166,61 @@ public enum ReaderAPIAccountDelegateError: LocalizedError {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func syncArticleStatus(for account: Account, completion: ((Result<Void, Error>) -> Void)? = nil) {
|
||||
guard variant != .inoreader else {
|
||||
completion?(.success(()))
|
||||
return
|
||||
}
|
||||
|
||||
sendArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion?(.success(()))
|
||||
case .failure(let error):
|
||||
completion?(.failure(error))
|
||||
}
|
||||
func refreshAll(for account: Account) async throws {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
self.refreshAll(for: account) { result in
|
||||
switch result {
|
||||
case .success():
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
case .failure(let error):
|
||||
completion?(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
func syncArticleStatus(for account: Account) async throws {
|
||||
guard variant != .inoreader else {
|
||||
return
|
||||
}
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
sendArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendArticleStatus(for account: Account) async throws {
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
self.sendArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
logger.debug("Sending article statuses")
|
||||
|
||||
@MainActor func processStatuses(_ syncStatuses: [SyncStatus]) {
|
||||
@@ -241,7 +267,21 @@ public enum ReaderAPIAccountDelegateError: LocalizedError {
|
||||
}
|
||||
}
|
||||
|
||||
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
func refreshArticleStatus(for account: Account) async throws {
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
logger.debug("Refreshing article statuses...")
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
@@ -245,37 +245,34 @@ import RSDatabase
|
||||
}
|
||||
}
|
||||
|
||||
public func refreshAll(errorHandler: @escaping @MainActor (Error) -> Void, completion: (() -> Void)? = nil) {
|
||||
public func refreshAll(errorHandler: @escaping @MainActor (Error) -> Void) async {
|
||||
|
||||
guard let reachability = try? Reachability(hostname: "apple.com"), reachability.connection != .unavailable else { return }
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
for account in activeAccounts {
|
||||
group.enter()
|
||||
account.refreshAll() { result in
|
||||
group.leave()
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
Task { @MainActor in
|
||||
errorHandler(error)
|
||||
}
|
||||
await withTaskGroup(of: Void.self) { group in
|
||||
for account in activeAccounts {
|
||||
group.addTask {
|
||||
do {
|
||||
try await account.refreshAll()
|
||||
} catch {
|
||||
Task { @MainActor in
|
||||
errorHandler(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
completion?()
|
||||
await group.waitForAll()
|
||||
}
|
||||
}
|
||||
|
||||
public func sendArticleStatusAll(completion: (() -> Void)? = nil) {
|
||||
let group = DispatchGroup()
|
||||
|
||||
|
||||
for account in activeAccounts {
|
||||
group.enter()
|
||||
account.sendArticleStatus() { _ in
|
||||
|
||||
Task { @MainActor in
|
||||
try? await account.sendArticleStatus()
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
@@ -285,18 +282,17 @@ import RSDatabase
|
||||
}
|
||||
}
|
||||
|
||||
public func syncArticleStatusAll(completion: (() -> Void)? = nil) {
|
||||
let group = DispatchGroup()
|
||||
|
||||
for account in activeAccounts {
|
||||
group.enter()
|
||||
account.syncArticleStatus() { _ in
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.global(qos: .background)) {
|
||||
completion?()
|
||||
public func syncArticleStatusAll() async {
|
||||
|
||||
await withTaskGroup(of: Void.self) { group in
|
||||
for account in activeAccounts {
|
||||
group.addTask {
|
||||
try? await account.syncArticleStatus()
|
||||
}
|
||||
}
|
||||
|
||||
await group.waitForAll()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,56 +78,54 @@ final class CloudKitAccountDelegate: AccountDelegate, Logging {
|
||||
}
|
||||
}
|
||||
|
||||
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func refreshAll(for account: Account) async throws {
|
||||
guard refreshProgress.isComplete else {
|
||||
completion(.success(()))
|
||||
return
|
||||
}
|
||||
|
||||
let reachability = SCNetworkReachabilityCreateWithName(nil, "apple.com")
|
||||
var flags = SCNetworkReachabilityFlags()
|
||||
guard SCNetworkReachabilityGetFlags(reachability!, &flags), flags.contains(.reachable) else {
|
||||
completion(.success(()))
|
||||
return
|
||||
}
|
||||
|
||||
standardRefreshAll(for: account, completion: completion)
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
standardRefreshAll(for: account) { result in
|
||||
switch result {
|
||||
case .success():
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func syncArticleStatus(for account: Account, completion: ((Result<Void, Error>) -> Void)? = nil) {
|
||||
sendArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion?(.success(()))
|
||||
case .failure(let error):
|
||||
completion?(.failure(error))
|
||||
}
|
||||
func syncArticleStatus(for account: Account) async throws {
|
||||
try await sendArticleStatus(for: account)
|
||||
}
|
||||
|
||||
func sendArticleStatus(for account: Account) async throws {
|
||||
try await sendArticleStatus(for: account, showProgress: false)
|
||||
}
|
||||
|
||||
func refreshArticleStatus(for account: Account) async throws {
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
|
||||
let op = CloudKitReceiveStatusOperation(articlesZone: self.articlesZone)
|
||||
op.completionBlock = { mainThreadOperaion in
|
||||
if mainThreadOperaion.isCanceled {
|
||||
continuation.resume(throwing: CloudKitAccountDelegateError.unknown)
|
||||
} else {
|
||||
continuation.resume()
|
||||
}
|
||||
case .failure(let error):
|
||||
completion?(.failure(error))
|
||||
}
|
||||
|
||||
mainThreadOperationQueue.add(op)
|
||||
}
|
||||
}
|
||||
|
||||
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
sendArticleStatus(for: account, showProgress: false, completion: completion)
|
||||
}
|
||||
|
||||
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
let op = CloudKitReceiveStatusOperation(articlesZone: articlesZone)
|
||||
op.completionBlock = { mainThreadOperaion in
|
||||
if mainThreadOperaion.isCanceled {
|
||||
completion(.failure(CloudKitAccountDelegateError.unknown))
|
||||
} else {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
mainThreadOperationQueue.add(op)
|
||||
}
|
||||
|
||||
func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard refreshProgress.isComplete else {
|
||||
completion(.success(()))
|
||||
@@ -444,7 +442,7 @@ final class CloudKitAccountDelegate: AccountDelegate, Logging {
|
||||
|
||||
try? await self.database.insertStatuses(syncStatuses)
|
||||
if let count = try? await self.database.selectPendingCount(), count > 100 {
|
||||
self.sendArticleStatus(for: account, showProgress: false) { _ in }
|
||||
try? await self.sendArticleStatus(for: account, showProgress: false)
|
||||
}
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
@@ -507,52 +505,92 @@ final class CloudKitAccountDelegate: AccountDelegate, Logging {
|
||||
}
|
||||
|
||||
private extension CloudKitAccountDelegate {
|
||||
|
||||
func initialRefreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
func fail(_ error: Error) {
|
||||
self.processAccountError(account, error)
|
||||
self.refreshProgress.clear()
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
refreshProgress.isIndeterminate = true
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(3)
|
||||
accountZone.fetchChangesInZone() { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
let feeds = account.flattenedFeeds()
|
||||
self.refreshProgress.addToNumberOfTasksAndRemaining(feeds.count)
|
||||
func fetchChangesInZone() async throws {
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
self.refreshProgress.isIndeterminate = false
|
||||
switch result {
|
||||
case .success:
|
||||
|
||||
self.combinedRefresh(account, feeds) { result in
|
||||
self.refreshProgress.clear()
|
||||
switch result {
|
||||
case .success:
|
||||
account.metadata.lastArticleFetchEndTime = Date()
|
||||
case .failure(let error):
|
||||
fail(error)
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
fail(error)
|
||||
}
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
self.accountZone.fetchChangesInZone { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
case .failure(let error):
|
||||
fail(error)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@MainActor func initialRefreshAll(for account : Account) async throws {
|
||||
|
||||
refreshProgress.isIndeterminate = true
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(3)
|
||||
|
||||
do {
|
||||
try await fetchChangesInZone()
|
||||
refreshProgress.completeTask()
|
||||
|
||||
let feeds = account.flattenedFeeds()
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(feeds.count)
|
||||
|
||||
try await refreshArticleStatus(for: account)
|
||||
refreshProgress.completeTask()
|
||||
refreshProgress.isIndeterminate = false
|
||||
|
||||
await combinedRefresh(account, feeds)
|
||||
refreshProgress.clear()
|
||||
account.metadata.lastArticleFetchEndTime = Date()
|
||||
} catch {
|
||||
processAccountError(account, error)
|
||||
refreshProgress.clear()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// func initialRefreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
//
|
||||
// func fail(_ error: Error) {
|
||||
// self.processAccountError(account, error)
|
||||
// self.refreshProgress.clear()
|
||||
// completion(.failure(error))
|
||||
// }
|
||||
//
|
||||
// refreshProgress.isIndeterminate = true
|
||||
// refreshProgress.addToNumberOfTasksAndRemaining(3)
|
||||
// accountZone.fetchChangesInZone() { result in
|
||||
// self.refreshProgress.completeTask()
|
||||
//
|
||||
// let feeds = account.flattenedFeeds()
|
||||
// self.refreshProgress.addToNumberOfTasksAndRemaining(feeds.count)
|
||||
//
|
||||
// switch result {
|
||||
// case .success:
|
||||
// self.refreshArticleStatus(for: account) { result in
|
||||
// self.refreshProgress.completeTask()
|
||||
// self.refreshProgress.isIndeterminate = false
|
||||
// switch result {
|
||||
// case .success:
|
||||
//
|
||||
// self.combinedRefresh(account, feeds) { result in
|
||||
// self.refreshProgress.clear()
|
||||
// switch result {
|
||||
// case .success:
|
||||
// account.metadata.lastArticleFetchEndTime = Date()
|
||||
// case .failure(let error):
|
||||
// fail(error)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// case .failure(let error):
|
||||
// fail(error)
|
||||
// }
|
||||
// }
|
||||
// case .failure(let error):
|
||||
// fail(error)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// }
|
||||
|
||||
func standardRefreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
let intialFeedsCount = account.flattenedFeeds().count
|
||||
@@ -573,6 +611,36 @@ private extension CloudKitAccountDelegate {
|
||||
let feeds = account.flattenedFeeds()
|
||||
self.refreshProgress.addToNumberOfTasksAndRemaining(feeds.count - intialFeedsCount)
|
||||
|
||||
Task { @MainActor in
|
||||
do {
|
||||
try await self.refreshArticleStatus(for: account)
|
||||
self.refreshProgress.completeTask()
|
||||
self.refreshProgress.isIndeterminate = false
|
||||
|
||||
self.combinedRefresh(account, feeds) { result in
|
||||
do {
|
||||
try await self.sendArticleStatus(for: account, showProgress: true)
|
||||
if case .failure(let error) = result {
|
||||
fail(error)
|
||||
} else {
|
||||
account.metadata.lastArticleFetchEndTime = Date()
|
||||
completion(.success(()))
|
||||
}
|
||||
} catch {
|
||||
fail(error)
|
||||
}
|
||||
}
|
||||
|
||||
} catch {
|
||||
self.refreshProgress.completeTask()
|
||||
self.refreshProgress.isIndeterminate = false
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
@@ -601,20 +669,31 @@ private extension CloudKitAccountDelegate {
|
||||
|
||||
}
|
||||
|
||||
func combinedRefresh(_ account: Account, _ feeds: Set<Feed>, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func combinedRefresh(_ account: Account, _ feeds: Set<Feed>) async {
|
||||
|
||||
let feedURLs = Set(feeds.map{ $0.url })
|
||||
let group = DispatchGroup()
|
||||
|
||||
try await withCheckedContinuation { continuation in
|
||||
refresher.refreshFeedURLs(feedURLs) {
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.enter()
|
||||
refresher.refreshFeedURLs(feedURLs) {
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
// func combinedRefresh(_ account: Account, _ feeds: Set<Feed>, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
//
|
||||
// let feedURLs = Set(feeds.map{ $0.url })
|
||||
// let group = DispatchGroup()
|
||||
//
|
||||
// group.enter()
|
||||
// refresher.refreshFeedURLs(feedURLs) {
|
||||
// group.leave()
|
||||
// }
|
||||
//
|
||||
// group.notify(queue: DispatchQueue.main) {
|
||||
// completion(.success(()))
|
||||
// }
|
||||
// }
|
||||
|
||||
func createRSSFeed(for account: Account, url: URL, editedName: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
|
||||
@@ -729,12 +808,13 @@ private extension CloudKitAccountDelegate {
|
||||
case .success(let articles):
|
||||
self.storeArticleChanges(new: articles, updated: Set<Article>(), deleted: Set<Article>()) {
|
||||
self.refreshProgress.completeTask()
|
||||
self.sendArticleStatus(for: account, showProgress: true) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshArticleStatus(for: account) { _ in }
|
||||
case .failure(let error):
|
||||
self.logger.error("CloudKit Feed send articles error: \(error.localizedDescription, privacy: .public)")
|
||||
|
||||
Task { @MainActor in
|
||||
do {
|
||||
try await self.sendArticleStatus(for: account, showProgress: true)
|
||||
try await self.refreshArticleStatus(for: account)
|
||||
} catch {
|
||||
self.logger.error("CloudKit Feed send articles error: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -795,20 +875,24 @@ private extension CloudKitAccountDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func sendArticleStatus(for account: Account, showProgress: Bool, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
let op = CloudKitSendStatusOperation(account: account,
|
||||
articlesZone: articlesZone,
|
||||
refreshProgress: refreshProgress,
|
||||
showProgress: showProgress,
|
||||
database: database)
|
||||
op.completionBlock = { mainThreadOperaion in
|
||||
if mainThreadOperaion.isCanceled {
|
||||
completion(.failure(CloudKitAccountDelegateError.unknown))
|
||||
} else {
|
||||
completion(.success(()))
|
||||
func sendArticleStatus(for account: Account, showProgress: Bool) async throws {
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
let op = CloudKitSendStatusOperation(account: account,
|
||||
articlesZone: self.articlesZone,
|
||||
refreshProgress: self.refreshProgress,
|
||||
showProgress: showProgress,
|
||||
database: self.database)
|
||||
op.completionBlock = { mainThreadOperation in
|
||||
if mainThreadOperation.isCanceled {
|
||||
continuation.resume(throwing: CloudKitAccountDelegateError.unknown)
|
||||
} else {
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
|
||||
mainThreadOperationQueue.add(op)
|
||||
}
|
||||
mainThreadOperationQueue.add(op)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -253,6 +253,29 @@ final class CloudKitAccountZone: CloudKitZone {
|
||||
}
|
||||
}
|
||||
|
||||
func findOrCreateAccount() async throws -> String {
|
||||
|
||||
guard let database else {
|
||||
return
|
||||
}
|
||||
|
||||
let predicate = NSPredicate(format: "isAccount = \"1\"")
|
||||
let ckQuery = CKQuery(recordType: CloudKitContainer.recordType, predicate: predicate)
|
||||
|
||||
do {
|
||||
let records = try await database.perform(ckQuery, inZoneWith: zoneID)
|
||||
if records.count > 0 {
|
||||
return records[0].externalID
|
||||
} else {
|
||||
createContainer(name: "Account", isAccount: true, completion: completion)
|
||||
}
|
||||
} catch {
|
||||
switch CloudKitZoneResult.resolve(error) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func findOrCreateAccount(completion: @escaping (Result<String, Error>) -> Void) {
|
||||
let predicate = NSPredicate(format: "isAccount = \"1\"")
|
||||
let ckQuery = CKQuery(recordType: CloudKitContainer.recordType, predicate: predicate)
|
||||
@@ -343,6 +366,21 @@ private extension CloudKitAccountZone {
|
||||
return record
|
||||
}
|
||||
|
||||
func createContainer(name: String, isAccount: Bool) async throws -> String {
|
||||
let record = CKRecord(recordType: CloudKitContainer.recordType, recordID: generateRecordID())
|
||||
record[CloudKitContainer.Fields.name] = name
|
||||
record[CloudKitContainer.Fields.isAccount] = isAccount ? "1" : "0"
|
||||
|
||||
save(record) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(record.externalID))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createContainer(name: String, isAccount: Bool, completion: @escaping (Result<String, Error>) -> Void) {
|
||||
let record = CKRecord(recordType: CloudKitContainer.recordType, recordID: generateRecordID())
|
||||
record[CloudKitContainer.Fields.name] = name
|
||||
|
||||
@@ -31,32 +31,19 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
||||
self.articlesZone = articlesZone
|
||||
}
|
||||
|
||||
@MainActor func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
for deletedRecordKey in deleted {
|
||||
switch deletedRecordKey.recordType {
|
||||
case CloudKitAccountZone.CloudKitFeed.recordType:
|
||||
removeFeed(deletedRecordKey.recordID.externalID)
|
||||
case CloudKitAccountZone.CloudKitContainer.recordType:
|
||||
removeContainer(deletedRecordKey.recordID.externalID)
|
||||
default:
|
||||
assertionFailure("Unknown record type: \(deletedRecordKey.recordType)")
|
||||
@MainActor func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey]) async throws {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
self.cloudKitWasChanged(updated: updated, deleted: deleted) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for changedRecord in updated {
|
||||
switch changedRecord.recordType {
|
||||
case CloudKitAccountZone.CloudKitFeed.recordType:
|
||||
addOrUpdateFeed(changedRecord)
|
||||
case CloudKitAccountZone.CloudKitContainer.recordType:
|
||||
addOrUpdateContainer(changedRecord)
|
||||
default:
|
||||
assertionFailure("Unknown record type: \(changedRecord.recordType)")
|
||||
}
|
||||
}
|
||||
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
|
||||
@MainActor func addOrUpdateFeed(_ record: CKRecord) {
|
||||
guard let account = account,
|
||||
let urlString = record[CloudKitAccountZone.CloudKitFeed.Fields.url] as? String,
|
||||
@@ -140,7 +127,33 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
||||
|
||||
private extension CloudKitAcountZoneDelegate {
|
||||
|
||||
@MainActor func updateFeed(_ feed: Feed, name: String?, editedName: String?, homePageURL: String?, containerExternalIDs: [String]) {
|
||||
@MainActor func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
for deletedRecordKey in deleted {
|
||||
switch deletedRecordKey.recordType {
|
||||
case CloudKitAccountZone.CloudKitFeed.recordType:
|
||||
removeFeed(deletedRecordKey.recordID.externalID)
|
||||
case CloudKitAccountZone.CloudKitContainer.recordType:
|
||||
removeContainer(deletedRecordKey.recordID.externalID)
|
||||
default:
|
||||
assertionFailure("Unknown record type: \(deletedRecordKey.recordType)")
|
||||
}
|
||||
}
|
||||
|
||||
for changedRecord in updated {
|
||||
switch changedRecord.recordType {
|
||||
case CloudKitAccountZone.CloudKitFeed.recordType:
|
||||
addOrUpdateFeed(changedRecord)
|
||||
case CloudKitAccountZone.CloudKitContainer.recordType:
|
||||
addOrUpdateContainer(changedRecord)
|
||||
default:
|
||||
assertionFailure("Unknown record type: \(changedRecord.recordType)")
|
||||
}
|
||||
}
|
||||
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
@MainActor func updateFeed(_ feed: Feed, name: String?, editedName: String?, homePageURL: String?, containerExternalIDs: [String]) {
|
||||
guard let account = account else { return }
|
||||
|
||||
feed.name = name
|
||||
|
||||
@@ -84,7 +84,30 @@ final class CloudKitArticlesZone: CloudKitZone {
|
||||
self.save(compressedRecords, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@MainActor func saveNewArticles(_ articles: Set<Article>) async throws {
|
||||
guard !articles.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
let records: [CKRecord] = {
|
||||
var recordsAccumulator = [CKRecord]()
|
||||
|
||||
let saveArticles = articles.filter { $0.status.read == false || $0.status.starred == true }
|
||||
for saveArticle in saveArticles {
|
||||
recordsAccumulator.append(makeStatusRecord(saveArticle))
|
||||
recordsAccumulator.append(makeArticleRecord(saveArticle))
|
||||
}
|
||||
return recordsAccumulator
|
||||
}()
|
||||
|
||||
compressionQueue.async {
|
||||
let compressedRecords = self.compressArticleRecords(records)
|
||||
self.save(compressedRecords, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func deleteArticles(_ feedExternalID: String, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
let predicate = NSPredicate(format: "webFeedExternalID = %@", feedExternalID)
|
||||
let ckQuery = CKQuery(recordType: CloudKitArticleStatus.recordType, predicate: predicate)
|
||||
|
||||
@@ -28,6 +28,22 @@ class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate, Logging {
|
||||
self.articlesZone = articlesZone
|
||||
}
|
||||
|
||||
func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey]) async throws {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
cloudKitWasChanged(updated: updated, deleted: deleted) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension CloudKitArticlesZoneDelegate {
|
||||
|
||||
func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
Task { @MainActor in
|
||||
@@ -54,9 +70,6 @@ class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate, Logging {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension CloudKitArticlesZoneDelegate {
|
||||
|
||||
func delete(recordKeys: [CloudKitRecordKey], pendingStarredStatusArticleIDs: Set<String>, completion: @escaping (Error?) -> Void) {
|
||||
let receivedRecordIDs = recordKeys.filter({ $0.recordType == CloudKitArticlesZone.CloudKitArticleStatus.recordType }).map({ $0.recordID })
|
||||
|
||||
@@ -32,16 +32,15 @@ class CloudKitReceiveStatusOperation: MainThreadOperation, Logging {
|
||||
|
||||
logger.debug("Refreshing article statuses...")
|
||||
|
||||
articlesZone.fetchChangesInZone() { result in
|
||||
self.logger.debug("Done refreshing article statuses.")
|
||||
switch result {
|
||||
case .success:
|
||||
Task { @MainActor in
|
||||
do {
|
||||
try await articlesZone.fetchChangesInZone()
|
||||
self.logger.debug("Done refreshing article statuses.")
|
||||
self.operationDelegate?.operationDidComplete(self)
|
||||
case .failure(let error):
|
||||
self.logger.error("Receive status error: \(error.localizedDescription, privacy: .public)")
|
||||
} catch {
|
||||
self.logger.error("Receive status error: \(error.localizedDescription, privacy: .public)")
|
||||
self.operationDelegate?.cancelOperation(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -36,13 +36,11 @@ class CloudKitRemoteNotificationOperation: MainThreadOperation, Logging {
|
||||
|
||||
logger.debug("Processing remote notification...")
|
||||
|
||||
accountZone.receiveRemoteNotification(userInfo: userInfo) {
|
||||
articlesZone.receiveRemoteNotification(userInfo: self.userInfo) {
|
||||
self.logger.debug("Done processing remote notification.")
|
||||
self.operationDelegate?.operationDidComplete(self)
|
||||
}
|
||||
Task { @MainActor in
|
||||
await accountZone.receiveRemoteNotification(userInfo: self.userInfo)
|
||||
await articlesZone.receiveRemoteNotification(userInfo: self.userInfo)
|
||||
self.logger.debug("Done processing remote notification.")
|
||||
self.operationDelegate?.operationDidComplete(self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -107,19 +107,17 @@ final class FeedlyAccountDelegate: AccountDelegate, Logging {
|
||||
return
|
||||
}
|
||||
|
||||
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func refreshAll(for account: Account) async throws {
|
||||
assert(Thread.isMainThread)
|
||||
|
||||
guard currentSyncAllOperation == nil else {
|
||||
self.logger.debug("Ignoring refreshAll: Feedly sync already in progress.")
|
||||
completion(.success(()))
|
||||
return
|
||||
}
|
||||
|
||||
guard let credentials = credentials else {
|
||||
self.logger.debug("Ignoring refreshAll: Feedly account has no credentials.")
|
||||
completion(.failure(FeedlyAccountDelegateError.notLoggedIn))
|
||||
return
|
||||
throw FeedlyAccountDelegateError.notLoggedIn
|
||||
}
|
||||
|
||||
let syncAllOperation = FeedlySyncAllOperation(account: account, feedlyUserID: credentials.username, caller: caller, database: database, lastSuccessfulFetchStartDate: accountMetadata?.lastArticleFetchStartTime, downloadProgress: refreshProgress)
|
||||
@@ -127,51 +125,62 @@ final class FeedlyAccountDelegate: AccountDelegate, Logging {
|
||||
syncAllOperation.downloadProgress = refreshProgress
|
||||
|
||||
let date = Date()
|
||||
syncAllOperation.syncCompletionHandler = { [weak self] result in
|
||||
if case .success = result {
|
||||
self?.accountMetadata?.lastArticleFetchStartTime = date
|
||||
self?.accountMetadata?.lastArticleFetchEndTime = Date()
|
||||
}
|
||||
|
||||
self?.logger.debug("Sync took \(-date.timeIntervalSinceNow, privacy: .public) seconds.")
|
||||
completion(result)
|
||||
}
|
||||
|
||||
currentSyncAllOperation = syncAllOperation
|
||||
|
||||
operationQueue.add(syncAllOperation)
|
||||
}
|
||||
|
||||
func syncArticleStatus(for account: Account, completion: ((Result<Void, Error>) -> Void)? = nil) {
|
||||
sendArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion?(.success(()))
|
||||
case .failure(let error):
|
||||
self.logger.error("Failed to refresh article status for account \(String(describing: account.type), privacy: .public): \(error.localizedDescription, privacy: .public)")
|
||||
completion?(.failure(error))
|
||||
}
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
syncAllOperation.syncCompletionHandler = { result in
|
||||
self.logger.debug("Sync took \(-date.timeIntervalSinceNow, privacy: .public) seconds.")
|
||||
|
||||
switch result {
|
||||
case .success():
|
||||
self.accountMetadata?.lastArticleFetchStartTime = date
|
||||
self.accountMetadata?.lastArticleFetchEndTime = Date()
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
case .failure(let error):
|
||||
self.logger.error("Failed to send article status for account \(String(describing: account.type), privacy: .public): \(error.localizedDescription, privacy: .public)")
|
||||
completion?(.failure(error))
|
||||
}
|
||||
|
||||
currentSyncAllOperation = syncAllOperation
|
||||
|
||||
operationQueue.add(syncAllOperation)
|
||||
}
|
||||
}
|
||||
|
||||
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
func syncArticleStatus(for account: Account) async throws {
|
||||
|
||||
do {
|
||||
try await sendArticleStatus(for: account)
|
||||
try await refreshArticleStatus(for: account)
|
||||
} catch {
|
||||
self.logger.error("Failed to send article status for account \(String(describing: account.type), privacy: .public): \(error.localizedDescription, privacy: .public)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
func sendArticleStatus(for account: Account) async throws {
|
||||
// Ensure remote articles have the same status as they do locally.
|
||||
let send = FeedlySendArticleStatusesOperation(database: database, service: caller)
|
||||
send.completionBlock = { operation in
|
||||
// TODO: not call with success if operation was canceled? Not sure.
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(()))
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
let send = FeedlySendArticleStatusesOperation(database: database, service: caller)
|
||||
send.completionBlock = { operation in
|
||||
continuation.resume()
|
||||
}
|
||||
|
||||
operationQueue.add(send)
|
||||
}
|
||||
}
|
||||
|
||||
func refreshArticleStatus(for account: Account) async throws {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
operationQueue.add(send)
|
||||
}
|
||||
|
||||
/// Attempts to ensure local articles have the same status as they do remotely.
|
||||
@@ -180,7 +189,7 @@ final class FeedlyAccountDelegate: AccountDelegate, Logging {
|
||||
///
|
||||
/// - Parameter account: The account whose articles have a remote status.
|
||||
/// - Parameter completion: Call on the main queue.
|
||||
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
private func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
guard let credentials = credentials else {
|
||||
return completion(.success(()))
|
||||
}
|
||||
@@ -550,7 +559,7 @@ final class FeedlyAccountDelegate: AccountDelegate, Logging {
|
||||
try await self.database.insertStatuses(syncStatuses)
|
||||
let count = try await self.database.selectPendingCount()
|
||||
if count > 100 {
|
||||
self.sendArticleStatus(for: account) { _ in }
|
||||
try? await self.sendArticleStatus(for: account)
|
||||
}
|
||||
completion(.success(()))
|
||||
} catch {
|
||||
|
||||
@@ -6,7 +6,21 @@
|
||||
<description>Most recent NetNewsWire changes with links to updates.</description>
|
||||
<language>en</language>
|
||||
|
||||
<item>
|
||||
<item>
|
||||
<title>NetNewsWire 6.1.5b1</title>
|
||||
<description><![CDATA[
|
||||
<p>This builds adds a new setting — you can turn on/off JavaScript for the article pane. It’s on by default, which matches previous behavior.</p>
|
||||
<p>Note that some content — videos and embedded social media posts, for instance — will often require JavaScript to be <i>on</i> in order to work properly.</p>
|
||||
<p>However, for those who want or need greater security and privacy, we’ve made this setting available.</p>
|
||||
|
||||
<p>This build also fixes a case where images might not load in the article pane.</p>
|
||||
]]></description>
|
||||
<pubDate>Fri, 01 Dec 2023 17:30:00 -0700</pubDate>
|
||||
<enclosure url="https://github.com/Ranchero-Software/NetNewsWire/releases/download/mac-6.1.5b1/NetNewsWire6.1.5b1.zip" sparkle:version="6121" sparkle:shortVersionString="6.1.5b1" length="10098688" type="application/zip" />
|
||||
<sparkle:minimumSystemVersion>11.0.0</sparkle:minimumSystemVersion>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>NetNewsWire 6.1.4b1</title>
|
||||
<description><![CDATA[
|
||||
<p><b>This build removes Reddit API integration! Don’t install it if you’re not ready for that to happen!</b></p>
|
||||
|
||||
@@ -11,7 +11,7 @@ let package = Package(
|
||||
targets: ["Articles"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import PackageDescription
|
||||
|
||||
var dependencies: [Package.Dependency] = [
|
||||
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "2.0.0")),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")),
|
||||
]
|
||||
|
||||
@@ -13,7 +13,7 @@ let package = Package(
|
||||
targets: ["FeedFinder"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")),
|
||||
.package(path: "../AccountError"),
|
||||
|
||||
@@ -581,7 +581,9 @@ var appDelegate: AppDelegate!
|
||||
}
|
||||
|
||||
@IBAction func refreshAll(_ sender: Any?) {
|
||||
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present)
|
||||
Task {
|
||||
await AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present)
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func showAddFeedWindow(_ sender: Any?) {
|
||||
|
||||
@@ -100,6 +100,18 @@ protocol DetailWebViewControllerDelegate: AnyObject {
|
||||
userContentController.add(self, name: MessageName.windowDidScroll)
|
||||
userContentController.add(self, name: MessageName.mouseDidEnter)
|
||||
userContentController.add(self, name: MessageName.mouseDidExit)
|
||||
|
||||
let baseURL = ArticleRenderer.page.baseURL
|
||||
let appScriptsWorld = WKContentWorld.world(name: "NetNewsWire")
|
||||
for fileName in ["main.js", "main_mac.js", "newsfoot.js"] {
|
||||
userContentController.addUserScript(
|
||||
.init(source: try! String(contentsOf: baseURL.appending(path: fileName,
|
||||
directoryHint: .notDirectory)),
|
||||
injectionTime: .atDocumentStart,
|
||||
forMainFrameOnly: true,
|
||||
in: appScriptsWorld))
|
||||
}
|
||||
|
||||
configuration.userContentController = userContentController
|
||||
|
||||
webView = DetailWebView(frame: NSRect.zero, configuration: configuration)
|
||||
@@ -323,7 +335,7 @@ private extension DetailWebViewController {
|
||||
]
|
||||
|
||||
let html = try! MacroProcessor.renderedText(withTemplate: ArticleRenderer.page.html, substitutions: substitutions)
|
||||
webView.loadHTMLString(html, baseURL: ArticleRenderer.page.baseURL)
|
||||
webView.loadHTMLString(html, baseURL: URL(string: rendering.baseURL))
|
||||
}
|
||||
|
||||
func scrollInfo() async -> ScrollInfo? {
|
||||
|
||||
@@ -4,14 +4,6 @@
|
||||
<style>
|
||||
[[style]]
|
||||
</style>
|
||||
<script src="main.js"></script>
|
||||
<script src="main_mac.js"></script>
|
||||
<script src="newsfoot.js" async="async"></script>
|
||||
<script type="text/javascript">
|
||||
document.addEventListener("DOMContentLoaded", function(event) {
|
||||
processPage();
|
||||
})
|
||||
</script>
|
||||
<base href="[[baseURL]]">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -14,19 +14,31 @@ import Foundation
|
||||
|
||||
// Since the Toolbar intercepts right clicks we need to stop it from doing that here
|
||||
// so that the ArticleExtractorButton can receive right click events.
|
||||
if event.isRightClick,
|
||||
let frameView = contentView?.superview,
|
||||
let view = frameView.hitTest(frameView.convert(event.locationInWindow, from: nil)),
|
||||
type(of: view).description() == "NSToolbarView" {
|
||||
if #available(macOS 14.0, *) {
|
||||
if event.isRightClick,
|
||||
let frameView = contentView?.superview,
|
||||
let view = frameView.hitTest(frameView.convert(event.locationInWindow, from: nil)),
|
||||
let articleExtractorButton = view as? ArticleExtractorButton {
|
||||
|
||||
for subview in view.subviews {
|
||||
for subsubview in subview.subviews {
|
||||
let candidateView = subsubview.hitTest(subsubview.convert(event.locationInWindow, from: nil))
|
||||
if candidateView is ArticleExtractorButton {
|
||||
candidateView?.rightMouseDown(with: event)
|
||||
return
|
||||
articleExtractorButton.rightMouseDown(with: event)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if event.isRightClick,
|
||||
let frameView = contentView?.superview,
|
||||
let view = frameView.hitTest(frameView.convert(event.locationInWindow, from: nil)),
|
||||
type(of: view).description() == "NSToolbarView" {
|
||||
|
||||
for subview in view.subviews {
|
||||
for subsubview in subview.subviews {
|
||||
let candidateView = subsubview.hitTest(subsubview.convert(event.locationInWindow, from: nil))
|
||||
if candidateView is ArticleExtractorButton {
|
||||
candidateView?.rightMouseDown(with: event)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -94,13 +94,10 @@ import Secrets
|
||||
try self.account?.removeCredentials(type: .basic)
|
||||
try self.account?.storeCredentials(validatedCredentials)
|
||||
|
||||
self.account?.refreshAll() { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
do {
|
||||
try await self.account?.refreshAll()
|
||||
} catch {
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
|
||||
self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK)
|
||||
|
||||
@@ -92,13 +92,10 @@ import Secrets
|
||||
try self.account?.storeCredentials(credentials)
|
||||
try self.account?.storeCredentials(validatedCredentials)
|
||||
|
||||
self.account?.refreshAll() { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
do {
|
||||
try await self.account?.refreshAll()
|
||||
} catch {
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
|
||||
self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK)
|
||||
|
||||
@@ -272,12 +272,11 @@ extension AccountsPreferencesViewController: OAuthAccountAuthorizationOperationD
|
||||
// because the user probably wants to see the result of authorizing NetNewsWire to act on their behalf.
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
|
||||
account.refreshAll { [weak self] result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
self?.presentError(error)
|
||||
Task {
|
||||
do {
|
||||
try await account.refreshAll()
|
||||
} catch {
|
||||
self.presentError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,13 +151,10 @@ import ReaderAPI
|
||||
try self.account?.storeCredentials(credentials)
|
||||
try self.account?.storeCredentials(validatedCredentials)
|
||||
|
||||
self.account?.refreshAll() { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
do {
|
||||
try await self.account?.refreshAll()
|
||||
} catch {
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
|
||||
self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK)
|
||||
|
||||
@@ -5296,7 +5296,7 @@
|
||||
repositoryURL = "https://github.com/Ranchero-Software/RSCore.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 2.0.1;
|
||||
minimumVersion = 3.0.0;
|
||||
};
|
||||
};
|
||||
510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */ = {
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Ranchero-Software/RSCore.git",
|
||||
"state" : {
|
||||
"revision" : "dcaa40ceb2c8acd182fbcd69d1f8e56df97e38b1",
|
||||
"version" : "2.0.1"
|
||||
"revision" : "cee6d96e036cc4ad08ad5f79364f87f9c291c43c",
|
||||
"version" : "3.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ It supports [RSS](https://cyber.harvard.edu/rss/rss.html), [Atom](https://datatr
|
||||
|
||||
More info: [https://netnewswire.com/](https://netnewswire.com/)
|
||||
|
||||
Also see the [Technotes](Technotes/) and the [Roadmap](Technotes/Roadmap.md).
|
||||
Also see the [Technotes](Technotes/) and the [Roadmap/Milestones](https://github.com/Ranchero-Software/NetNewsWire/milestones).
|
||||
|
||||
Note: NetNewsWire’s Help menu has a bunch of these links, so you don’t have to remember to come back to this page.
|
||||
|
||||
|
||||
@@ -168,3 +168,8 @@ function processPage() {
|
||||
removeWpSmiley()
|
||||
postRenderProcessing();
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function(event) {
|
||||
window.scrollTo(0, [[windowScrollY]]);
|
||||
processPage();
|
||||
})
|
||||
|
||||
@@ -263,6 +263,10 @@ figure {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
figure > * + * {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
font-size: 14px;
|
||||
line-height: 1.3em;
|
||||
|
||||
@@ -73,6 +73,8 @@ import Account
|
||||
lastTimedRefresh = Date()
|
||||
update()
|
||||
|
||||
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log, completion: nil)
|
||||
Task {
|
||||
await AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,8 +68,8 @@ class ArticleStatusSyncTimer {
|
||||
lastTimedRefresh = Date()
|
||||
update()
|
||||
|
||||
AccountManager.shared.syncArticleStatusAll()
|
||||
|
||||
Task {
|
||||
await AccountManager.shared.syncArticleStatusAll()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ let package = Package(
|
||||
dependencies: [
|
||||
.package(path: "../../Secrets"),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2"))
|
||||
],
|
||||
targets: [
|
||||
|
||||
@@ -13,7 +13,7 @@ let package = Package(
|
||||
targets: ["LocalAccount"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")),
|
||||
],
|
||||
|
||||
@@ -15,7 +15,7 @@ let package = Package(
|
||||
dependencies: [
|
||||
.package(path: "../../Secrets"),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2"))
|
||||
],
|
||||
targets: [
|
||||
|
||||
@@ -16,7 +16,7 @@ let package = Package(
|
||||
.package(path: "../../Secrets"),
|
||||
.package(path: "../../AccountError"),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2"))
|
||||
],
|
||||
targets: [
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import PackageDescription
|
||||
|
||||
var dependencies: [Package.Dependency] = [
|
||||
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")),
|
||||
.package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "2.0.0")),
|
||||
]
|
||||
|
||||
|
||||
@@ -529,6 +529,20 @@ private extension WebViewController {
|
||||
}
|
||||
configuration.setURLSchemeHandler(articleIconSchemeHandler, forURLScheme: ArticleRenderer.imageIconScheme)
|
||||
|
||||
let userContentController = WKUserContentController()
|
||||
let baseURL = ArticleRenderer.page.baseURL
|
||||
let appScriptsWorld = WKContentWorld.world(name: "NetNewsWire")
|
||||
for fileName in ["main.js", "main_ios.js", "newsfoot.js"] {
|
||||
userContentController.addUserScript(
|
||||
.init(source: try! String(contentsOf: baseURL.appending(path: fileName,
|
||||
directoryHint: .notDirectory)),
|
||||
injectionTime: .atDocumentStart,
|
||||
forMainFrameOnly: true,
|
||||
in: appScriptsWorld))
|
||||
}
|
||||
|
||||
configuration.userContentController = userContentController
|
||||
|
||||
let webView = WKWebView(frame: self.view.bounds, configuration: configuration)
|
||||
webView.isOpaque = false;
|
||||
webView.backgroundColor = .clear;
|
||||
@@ -591,8 +605,8 @@ private extension WebViewController {
|
||||
]
|
||||
|
||||
let html = try! MacroProcessor.renderedText(withTemplate: ArticleRenderer.page.html, substitutions: substitutions)
|
||||
webView.loadHTMLString(html, baseURL: ArticleRenderer.page.baseURL)
|
||||
|
||||
webView.loadHTMLString(html, baseURL: URL(string: rendering.baseURL))
|
||||
|
||||
}
|
||||
|
||||
func finalScrollPosition(scrollingUp: Bool) -> CGFloat {
|
||||
|
||||
@@ -5,15 +5,6 @@
|
||||
<style>
|
||||
[[style]]
|
||||
</style>
|
||||
<script src="main.js"></script>
|
||||
<script src="main_ios.js"></script>
|
||||
<script src="newsfoot.js" async="async"></script>
|
||||
<script type="text/javascript">
|
||||
document.addEventListener("DOMContentLoaded", function(event) {
|
||||
window.scrollTo(0, [[windowScrollY]]);
|
||||
processPage();
|
||||
})
|
||||
</script>
|
||||
<base href="[[baseURL]]">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user