mirror of
https://github.com/kiwix/kiwix-apple.git
synced 2025-09-15 07:06:21 -04:00
Single background context for DB
This commit is contained in:
parent
ef8d6b55c2
commit
611b7c39ea
@ -36,7 +36,7 @@ struct Kiwix: App {
|
||||
WindowGroup {
|
||||
RootView()
|
||||
.ignoresSafeArea()
|
||||
.environment(\.managedObjectContext, Database.viewContext)
|
||||
.environment(\.managedObjectContext, Database.shared.viewContext)
|
||||
.environmentObject(library)
|
||||
.environmentObject(navigation)
|
||||
.modifier(AlertHandler())
|
||||
@ -45,7 +45,7 @@ struct Kiwix: App {
|
||||
.modifier(SaveContentHandler())
|
||||
.onChange(of: scenePhase) { newValue in
|
||||
guard newValue == .inactive else { return }
|
||||
try? Database.viewContext.save()
|
||||
try? Database.shared.viewContext.save()
|
||||
}
|
||||
.onOpenURL { url in
|
||||
if url.isFileURL {
|
||||
|
@ -39,7 +39,7 @@ struct Kiwix: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
RootView()
|
||||
.environment(\.managedObjectContext, Database.shared.container.viewContext)
|
||||
.environment(\.managedObjectContext, Database.shared.viewContext)
|
||||
.environmentObject(libraryRefreshViewModel)
|
||||
}.commands {
|
||||
SidebarCommands()
|
||||
@ -149,7 +149,9 @@ struct RootView: View {
|
||||
case .categories:
|
||||
ZimFilesCategories(dismiss: nil).modifier(LibraryZimFileDetailSidePanel())
|
||||
case .downloads:
|
||||
ZimFilesDownloads(dismiss: nil).modifier(LibraryZimFileDetailSidePanel())
|
||||
ZimFilesDownloads(dismiss: nil)
|
||||
.environment(\.managedObjectContext, Database.shared.viewContext)
|
||||
.modifier(LibraryZimFileDetailSidePanel())
|
||||
case .new:
|
||||
ZimFilesNew(dismiss: nil).modifier(LibraryZimFileDetailSidePanel())
|
||||
default:
|
||||
@ -214,10 +216,8 @@ struct RootView: View {
|
||||
DownloadService.shared.restartHeartbeatIfNeeded()
|
||||
case let .custom(zimFileURL):
|
||||
LibraryOperations.open(url: zimFileURL) {
|
||||
Task {
|
||||
await ZimMigration.forCustomApps()
|
||||
navigation.currentItem = .reading
|
||||
}
|
||||
ZimMigration.forCustomApps()
|
||||
navigation.currentItem = .reading
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ class SidebarViewController: UICollectionViewController, NSFetchedResultsControl
|
||||
}()
|
||||
private let fetchedResultController = NSFetchedResultsController(
|
||||
fetchRequest: Tab.fetchRequest(sortDescriptors: [NSSortDescriptor(key: "created", ascending: false)]),
|
||||
managedObjectContext: Database.viewContext,
|
||||
managedObjectContext: Database.shared.viewContext,
|
||||
sectionNameKeyPath: nil,
|
||||
cacheName: nil
|
||||
)
|
||||
@ -187,7 +187,7 @@ class SidebarViewController: UICollectionViewController, NSFetchedResultsControl
|
||||
// MARK: - Collection View Configuration
|
||||
|
||||
private func configureCell(cell: UICollectionViewListCell, indexPath: IndexPath, item: NavigationItem) {
|
||||
if case let .tab(objectID) = item, let tab = try? Database.viewContext.existingObject(with: objectID) as? Tab {
|
||||
if case let .tab(objectID) = item, let tab = try? Database.shared.viewContext.existingObject(with: objectID) as? Tab {
|
||||
var config = cell.defaultContentConfiguration()
|
||||
config.text = tab.title ?? "common.tab.menu.new_tab".localized
|
||||
if let zimFile = tab.zimFile, let category = Category(rawValue: zimFile.category) {
|
||||
|
@ -111,7 +111,10 @@ final class SplitViewController: UISplitViewController {
|
||||
let controller = UIHostingController(rootView: ZimFilesCategories(dismiss: nil))
|
||||
setViewController(UINavigationController(rootViewController: controller), for: .secondary)
|
||||
case .downloads:
|
||||
let controller = UIHostingController(rootView: ZimFilesDownloads(dismiss: nil))
|
||||
let controller = UIHostingController(
|
||||
rootView: ZimFilesDownloads(dismiss: nil)
|
||||
.environment(\.managedObjectContext, Database.shared.viewContext)
|
||||
)
|
||||
setViewController(UINavigationController(rootViewController: controller), for: .secondary)
|
||||
case .new:
|
||||
let controller = UIHostingController(rootView: ZimFilesNew(dismiss: nil))
|
||||
|
@ -74,16 +74,18 @@ final class DownloadService: NSObject, URLSessionDelegate, URLSessionTaskDelegat
|
||||
let zimFileID = UUID(uuidString: taskDescription) else { return }
|
||||
self.progress.updateFor(uuid: zimFileID, totalBytes: task.countOfBytesReceived)
|
||||
}
|
||||
self.startHeartbeat()
|
||||
Task { @MainActor in
|
||||
self.startHeartbeat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Start heartbeat, which will update database every 0.25 second
|
||||
private func startHeartbeat() {
|
||||
DispatchQueue.main.async {
|
||||
@MainActor private func startHeartbeat() {
|
||||
// DispatchQueue.main.async {
|
||||
guard self.heartbeat == nil else { return }
|
||||
self.heartbeat = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { [weak self] _ in
|
||||
Database.shared.container.performBackgroundTask { [weak self] context in
|
||||
self.heartbeat = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] _ in
|
||||
Database.shared.performBackgroundTask { [weak self] context in
|
||||
context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
|
||||
if let progressValues = self?.progress.values() {
|
||||
for (zimFileID, downloadedBytes) in progressValues {
|
||||
@ -97,17 +99,15 @@ final class DownloadService: NSObject, URLSessionDelegate, URLSessionTaskDelegat
|
||||
}
|
||||
}
|
||||
os_log("Heartbeat started.", log: Log.DownloadService, type: .info)
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
/// Stop heartbeat, which stops periodical database update
|
||||
private func stopHeartbeat() {
|
||||
DispatchQueue.main.async {
|
||||
guard self.heartbeat != nil else { return }
|
||||
self.heartbeat?.invalidate()
|
||||
self.heartbeat = nil
|
||||
os_log("Heartbeat stopped.", log: Log.DownloadService, type: .info)
|
||||
}
|
||||
@MainActor private func stopHeartbeat() {
|
||||
guard self.heartbeat != nil else { return }
|
||||
self.heartbeat?.invalidate()
|
||||
self.heartbeat = nil
|
||||
os_log("Heartbeat stopped.", log: Log.DownloadService, type: .info)
|
||||
}
|
||||
|
||||
// MARK: - Download Actions
|
||||
@ -116,9 +116,9 @@ final class DownloadService: NSObject, URLSessionDelegate, URLSessionTaskDelegat
|
||||
/// - Parameters:
|
||||
/// - zimFile: the zim file to download
|
||||
/// - allowsCellularAccess: if using cellular data is allowed
|
||||
func start(zimFileID: UUID, allowsCellularAccess: Bool) {
|
||||
@MainActor func start(zimFileID: UUID, allowsCellularAccess: Bool) {
|
||||
requestNotificationAuthorization()
|
||||
Database.shared.container.performBackgroundTask { context in
|
||||
Database.shared.performBackgroundTask { context in
|
||||
context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
|
||||
let fetchRequest = ZimFile.fetchRequest(fileID: zimFileID)
|
||||
guard let zimFile = try? context.fetch(fetchRequest).first,
|
||||
@ -160,7 +160,7 @@ final class DownloadService: NSObject, URLSessionDelegate, URLSessionTaskDelegat
|
||||
session.getTasksWithCompletionHandler { _, _, downloadTasks in
|
||||
guard let task = downloadTasks.filter({ $0.taskDescription == zimFileID.uuidString }).first else { return }
|
||||
task.cancel { resumeData in
|
||||
Database.shared.container.performBackgroundTask { context in
|
||||
Database.shared.performBackgroundTask { context in
|
||||
context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
|
||||
let request = DownloadTask.fetchRequest(fileID: zimFileID)
|
||||
guard let downloadTask = try? context.fetch(request).first else { return }
|
||||
@ -175,7 +175,7 @@ final class DownloadService: NSObject, URLSessionDelegate, URLSessionTaskDelegat
|
||||
/// - Parameter zimFileID: identifier of the zim file
|
||||
func resume(zimFileID: UUID) {
|
||||
requestNotificationAuthorization()
|
||||
Database.shared.container.performBackgroundTask { context in
|
||||
Database.shared.performBackgroundTask { context in
|
||||
context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
|
||||
|
||||
let request = DownloadTask.fetchRequest(fileID: zimFileID)
|
||||
@ -190,14 +190,16 @@ final class DownloadService: NSObject, URLSessionDelegate, URLSessionTaskDelegat
|
||||
downloadTask.resumeData = nil
|
||||
try? context.save()
|
||||
|
||||
self.startHeartbeat()
|
||||
Task { @MainActor in
|
||||
self.startHeartbeat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Database
|
||||
|
||||
private func deleteDownloadTask(zimFileID: UUID) {
|
||||
Database.shared.container.performBackgroundTask { context in
|
||||
Database.shared.performBackgroundTask { context in
|
||||
context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
|
||||
do {
|
||||
let request = DownloadTask.fetchRequest(fileID: zimFileID)
|
||||
@ -225,7 +227,7 @@ final class DownloadService: NSObject, URLSessionDelegate, URLSessionTaskDelegat
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.getNotificationSettings { settings in
|
||||
guard settings.authorizationStatus != .denied else { return }
|
||||
Database.shared.container.performBackgroundTask { context in
|
||||
Database.shared.performBackgroundTask { context in
|
||||
// configure notification content
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = "download_service.complete.title".localized
|
||||
@ -248,7 +250,9 @@ final class DownloadService: NSObject, URLSessionDelegate, URLSessionTaskDelegat
|
||||
let zimFileID = UUID(uuidString: taskDescription) else { return }
|
||||
progress.resetFor(uuid: zimFileID)
|
||||
if progress.isEmpty() {
|
||||
stopHeartbeat()
|
||||
Task { @MainActor in
|
||||
stopHeartbeat()
|
||||
}
|
||||
}
|
||||
|
||||
// download finished successfully if there's no error
|
||||
@ -268,7 +272,7 @@ final class DownloadService: NSObject, URLSessionDelegate, URLSessionTaskDelegat
|
||||
Note: The result data equality check is used as a trick to distinguish user pausing the download task vs
|
||||
failure. When pausing, the same resume data would have already been saved when the delegate is called.
|
||||
*/
|
||||
Database.shared.container.performBackgroundTask { context in
|
||||
Database.shared.performBackgroundTask { context in
|
||||
context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
|
||||
let request = DownloadTask.fetchRequest(fileID: zimFileID)
|
||||
let resumeData = error.userInfo[NSURLSessionDownloadTaskResumeData] as? Data
|
||||
|
@ -19,21 +19,36 @@ import os
|
||||
final class Database {
|
||||
static let shared = Database()
|
||||
private var notificationToken: NSObjectProtocol?
|
||||
private var token: NSPersistentHistoryToken?
|
||||
private var tokenURL = NSPersistentContainer.defaultDirectoryURL().appendingPathComponent("token.data")
|
||||
private let sync = InSync(label: "database.token")
|
||||
private var _token: NSPersistentHistoryToken?
|
||||
private let container: NSPersistentContainer
|
||||
private let backgroundContext: NSManagedObjectContext
|
||||
private let backgroundQueue = DispatchQueue(label: "database.background.queue",
|
||||
qos: .utility,
|
||||
attributes: [.concurrent])
|
||||
|
||||
private init() {
|
||||
container = Self.createContainer()
|
||||
backgroundContext = container.newBackgroundContext()
|
||||
backgroundContext.persistentStoreCoordinator = container.persistentStoreCoordinator
|
||||
backgroundContext.automaticallyMergesChangesFromParent = false
|
||||
backgroundContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
|
||||
backgroundContext.undoManager = nil
|
||||
backgroundContext.shouldDeleteInaccessibleFaults = true
|
||||
|
||||
// due to objc++ interop, only the older notification value is working for downloads:
|
||||
// https://developer.apple.com/documentation/coredata/nspersistentstoreremotechangenotification?language=objc
|
||||
let storeChange: NSNotification.Name = .init(rawValue: "NSPersistentStoreRemoteChangeNotification")
|
||||
|
||||
notificationToken = NotificationCenter.default.addObserver(
|
||||
forName: storeChange, object: nil, queue: nil) { _ in
|
||||
try? self.mergeChanges()
|
||||
}
|
||||
token = {
|
||||
let intialToken: NSPersistentHistoryToken? = {
|
||||
guard let data = UserDefaults.standard.data(forKey: "PersistentHistoryToken") else { return nil }
|
||||
return try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSPersistentHistoryToken.self, from: data)
|
||||
}()
|
||||
updateToken(intialToken)
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -42,23 +57,33 @@ final class Database {
|
||||
}
|
||||
}
|
||||
|
||||
static var viewContext: NSManagedObjectContext {
|
||||
Database.shared.container.viewContext
|
||||
var viewContext: NSManagedObjectContext {
|
||||
container.viewContext
|
||||
}
|
||||
|
||||
static func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) {
|
||||
Database.shared.container.performBackgroundTask(block)
|
||||
func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) {
|
||||
backgroundQueue.sync { [self] in
|
||||
backgroundContext.perform { [self] in
|
||||
block(backgroundContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func performBackgroundTask<T>(_ block: @escaping (NSManagedObjectContext) throws -> T) async rethrows -> T {
|
||||
try await Database.shared.container.performBackgroundTask(block)
|
||||
private func token() -> NSPersistentHistoryToken? {
|
||||
sync.read {
|
||||
self._token
|
||||
}
|
||||
}
|
||||
|
||||
private func updateToken(_ value: NSPersistentHistoryToken?) {
|
||||
sync.execute {
|
||||
self._token = value
|
||||
}
|
||||
}
|
||||
|
||||
/// A persistent container to set up the Core Data stack.
|
||||
lazy var container: NSPersistentContainer = {
|
||||
/// - Tag: persistentContainer
|
||||
private static func createContainer() -> NSPersistentContainer {
|
||||
let container = NSPersistentContainer(name: "DataModel")
|
||||
|
||||
guard let description = container.persistentStoreDescriptions.first else {
|
||||
fatalError("Failed to retrieve a persistent store description.")
|
||||
}
|
||||
@ -84,18 +109,17 @@ final class Database {
|
||||
container.viewContext.undoManager = nil
|
||||
container.viewContext.shouldDeleteInaccessibleFaults = true
|
||||
return container
|
||||
}()
|
||||
}
|
||||
|
||||
/// Save image data to zim files.
|
||||
func saveImageData(url: URL, completion: @escaping (Data) -> Void) {
|
||||
URLSession.shared.dataTask(with: url) { data, response, _ in
|
||||
URLSession.shared.dataTask(with: url) { [self] data, response, _ in
|
||||
guard let response = response as? HTTPURLResponse,
|
||||
response.statusCode == 200,
|
||||
let mimeType = response.mimeType,
|
||||
mimeType.contains("image"),
|
||||
let data = data else { return }
|
||||
let context = self.container.newBackgroundContext()
|
||||
context.perform {
|
||||
performBackgroundTask { [data] context in
|
||||
let predicate = NSPredicate(format: "faviconURL == %@", url as CVarArg)
|
||||
let request = ZimFile.fetchRequest(predicate: predicate)
|
||||
guard let zimFile = try? context.fetch(request).first else { return }
|
||||
@ -108,26 +132,24 @@ final class Database {
|
||||
|
||||
/// Merge changes performed on batch requests to view context.
|
||||
private func mergeChanges() throws {
|
||||
let context = container.newBackgroundContext()
|
||||
context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
|
||||
context.undoManager = nil
|
||||
context.perform {
|
||||
performBackgroundTask{ [weak self] context in
|
||||
guard let self else { return }
|
||||
// fetch and merge changes
|
||||
let fetchRequest = NSPersistentHistoryChangeRequest.fetchHistory(after: self.token)
|
||||
let fetchRequest = NSPersistentHistoryChangeRequest.fetchHistory(after: self.token())
|
||||
guard let result = try? context.execute(fetchRequest) as? NSPersistentHistoryResult else {
|
||||
os_log("no persistent history found after token: \(self.token)")
|
||||
self.token = nil
|
||||
os_log("no persistent history found after token: \(self.token())")
|
||||
self.updateToken(nil)
|
||||
return
|
||||
}
|
||||
guard let transactions = result.result as? [NSPersistentHistoryTransaction] else {
|
||||
os_log("no transactions in persistent history found after token: \(self.token)")
|
||||
self.token = nil
|
||||
os_log("no transactions in persistent history found after token: \(self.token())")
|
||||
self.updateToken(nil)
|
||||
return
|
||||
}
|
||||
self.container.viewContext.performAndWait {
|
||||
transactions.forEach { transaction in
|
||||
self.container.viewContext.mergeChanges(fromContextDidSave: transaction.objectIDNotification())
|
||||
self.token = transaction.token
|
||||
self.updateToken(transaction.token)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,7 @@ struct LibraryOperations {
|
||||
}
|
||||
|
||||
// upsert zim file in the database
|
||||
Database.shared.container.performBackgroundTask { context in
|
||||
Database.shared.performBackgroundTask { context in
|
||||
context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
|
||||
let predicate = NSPredicate(format: "fileID == %@", metadata.fileID as CVarArg)
|
||||
let fetchRequest = ZimFile.fetchRequest(predicate: predicate)
|
||||
@ -65,7 +65,7 @@ struct LibraryOperations {
|
||||
/// Reopen zim files from url bookmark data.
|
||||
static func reopen(onComplete: (() -> Void)?) {
|
||||
var successCount = 0
|
||||
let context = Database.shared.container.viewContext
|
||||
let context = Database.shared.viewContext
|
||||
let request = ZimFile.fetchRequest(predicate: ZimFile.Predicate.isDownloaded)
|
||||
|
||||
guard let zimFiles = try? context.fetch(request) else {
|
||||
@ -149,7 +149,7 @@ struct LibraryOperations {
|
||||
/// - Parameter zimFile: the zim file to unlink
|
||||
static func unlink(zimFileID: UUID) {
|
||||
ZimFileService.shared.close(fileID: zimFileID)
|
||||
Database.shared.container.performBackgroundTask { context in
|
||||
Database.shared.performBackgroundTask { context in
|
||||
context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
|
||||
guard let zimFile = try? ZimFile.fetchRequest(fileID: zimFileID).execute().first else { return }
|
||||
zimFile.bookmarks.forEach { context.delete($0) }
|
||||
|
@ -33,9 +33,9 @@ enum ZimMigration {
|
||||
sortDescriptors: Self.sortDescriptors
|
||||
)
|
||||
|
||||
static func forCustomApps() async {
|
||||
static func forCustomApps() {
|
||||
guard FeatureFlags.hasLibrary == false else { return }
|
||||
await Database.shared.container.performBackgroundTask { context in
|
||||
Database.shared.performBackgroundTask { context in
|
||||
guard var zimFiles = try? requestLatestZimFile.execute(),
|
||||
zimFiles.count > 1,
|
||||
let latest = zimFiles.popLast() else {
|
||||
@ -77,17 +77,16 @@ enum ZimMigration {
|
||||
if context.hasChanges { try? context.save() }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private static func latestZimFileHost() async -> String {
|
||||
if let newHost = await Self.newHost { return newHost }
|
||||
if let newHost = Self.newHost { return newHost }
|
||||
// if it wasn't set before, set and return by the last ZimFile in DB:
|
||||
guard let zimFile = try? Database.viewContext.fetch(requestLatestZimFile).first else {
|
||||
guard let zimFile = try? Database.shared.viewContext.fetch(requestLatestZimFile).first else {
|
||||
fatalError("we should have at least 1 zim file for a custom app")
|
||||
}
|
||||
let newHost = zimFile.fileID.uuidString
|
||||
// save the new host for later
|
||||
await MainActor.run {
|
||||
Self.newHost = newHost
|
||||
}
|
||||
Self.newHost = newHost
|
||||
return newHost
|
||||
}
|
||||
}
|
||||
|
@ -104,7 +104,7 @@ final class BrowserViewModel: NSObject, ObservableObject,
|
||||
// Bookmark fetching:
|
||||
bookmarkFetchedResultsController = NSFetchedResultsController(
|
||||
fetchRequest: Bookmark.fetchRequest(), // initially empty
|
||||
managedObjectContext: Database.viewContext,
|
||||
managedObjectContext: Database.shared.viewContext,
|
||||
sectionNameKeyPath: nil,
|
||||
cacheName: nil
|
||||
)
|
||||
@ -183,7 +183,7 @@ final class BrowserViewModel: NSObject, ObservableObject,
|
||||
private func didUpdate(title: String, url: URL) {
|
||||
let zimFile: ZimFile? = {
|
||||
guard let zimFileID = UUID(uuidString: url.host ?? "") else { return nil }
|
||||
return try? Database.viewContext.fetch(ZimFile.fetchRequest(fileID: zimFileID)).first
|
||||
return try? Database.shared.viewContext.fetch(ZimFile.fetchRequest(fileID: zimFileID)).first
|
||||
}()
|
||||
|
||||
metaData = ZimFileService.shared.getContentMetaData(url: url)
|
||||
@ -204,14 +204,14 @@ final class BrowserViewModel: NSObject, ObservableObject,
|
||||
tabID = currentTabID
|
||||
|
||||
// update tab data
|
||||
if let tab = try? Database.viewContext.existingObject(with: currentTabID) as? Tab {
|
||||
if let tab = try? Database.shared.viewContext.existingObject(with: currentTabID) as? Tab {
|
||||
tab.title = articleTitle
|
||||
tab.zimFile = zimFile
|
||||
}
|
||||
}
|
||||
|
||||
func updateLastOpened() {
|
||||
guard let tabID, let tab = try? Database.viewContext.existingObject(with: tabID) as? Tab else { return }
|
||||
guard let tabID, let tab = try? Database.shared.viewContext.existingObject(with: tabID) as? Tab else { return }
|
||||
tab.lastOpened = Date()
|
||||
}
|
||||
|
||||
@ -221,11 +221,11 @@ final class BrowserViewModel: NSObject, ObservableObject,
|
||||
|
||||
func persistState() {
|
||||
guard let tabID,
|
||||
let tab = try? Database.viewContext.existingObject(with: tabID) as? Tab else {
|
||||
let tab = try? Database.shared.viewContext.existingObject(with: tabID) as? Tab else {
|
||||
return
|
||||
}
|
||||
tab.interactionState = webView.interactionState as? Data
|
||||
try? Database.viewContext.save()
|
||||
try? Database.shared.viewContext.save()
|
||||
}
|
||||
|
||||
// MARK: - Content Loading
|
||||
@ -251,7 +251,7 @@ final class BrowserViewModel: NSObject, ObservableObject,
|
||||
}
|
||||
|
||||
private func restoreBy(tabID: NSManagedObjectID) {
|
||||
if let tab = try? Database.viewContext.existingObject(with: tabID) as? Tab {
|
||||
if let tab = try? Database.shared.viewContext.existingObject(with: tabID) as? Tab {
|
||||
webView.interactionState = tab.interactionState
|
||||
Task {
|
||||
await MainActor.run {
|
||||
@ -501,7 +501,7 @@ final class BrowserViewModel: NSObject, ObservableObject,
|
||||
|
||||
// bookmark
|
||||
let bookmarkAction: UIAction = {
|
||||
let context = Database.viewContext
|
||||
let context = Database.shared.viewContext
|
||||
let predicate = NSPredicate(format: "articleURL == %@", url as CVarArg)
|
||||
let request = Bookmark.fetchRequest(predicate: predicate)
|
||||
|
||||
@ -596,7 +596,7 @@ final class BrowserViewModel: NSObject, ObservableObject,
|
||||
|
||||
private func createNewTabID() -> NSManagedObjectID {
|
||||
if let tabID { return tabID }
|
||||
let context = Database.viewContext
|
||||
let context = Database.shared.viewContext
|
||||
let tab = Tab(context: context)
|
||||
tab.created = Date()
|
||||
tab.lastOpened = Date()
|
||||
@ -615,7 +615,7 @@ final class BrowserViewModel: NSObject, ObservableObject,
|
||||
func createBookmark(url: URL? = nil) {
|
||||
guard let url = url ?? webView.url else { return }
|
||||
let title = webView.title
|
||||
Database.performBackgroundTask { context in
|
||||
Database.shared.performBackgroundTask { context in
|
||||
let bookmark = Bookmark(context: context)
|
||||
bookmark.articleURL = url
|
||||
bookmark.created = Date()
|
||||
@ -631,7 +631,7 @@ final class BrowserViewModel: NSObject, ObservableObject,
|
||||
|
||||
func deleteBookmark(url: URL? = nil) {
|
||||
guard let url = url ?? webView.url else { return }
|
||||
Database.performBackgroundTask { context in
|
||||
Database.shared.performBackgroundTask { context in
|
||||
let request = Bookmark.fetchRequest(predicate: NSPredicate(format: "articleURL == %@", url as CVarArg))
|
||||
guard let bookmark = try? context.fetch(request).first else { return }
|
||||
context.delete(bookmark)
|
||||
|
@ -49,18 +49,12 @@ final class LibraryViewModel: ObservableObject {
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private let urlSession: URLSession
|
||||
private let context: NSManagedObjectContext
|
||||
private var insertionCount = 0
|
||||
private var deletionCount = 0
|
||||
|
||||
@MainActor
|
||||
init(urlSession: URLSession? = nil) {
|
||||
self.urlSession = urlSession ?? URLSession.shared
|
||||
|
||||
context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
|
||||
context.persistentStoreCoordinator = Database.shared.container.persistentStoreCoordinator
|
||||
context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
|
||||
context.undoManager = nil
|
||||
process = LibraryProcess.shared
|
||||
state = process.state
|
||||
process.$state.sink { [weak self] newState in
|
||||
@ -204,11 +198,15 @@ final class LibraryViewModel: ObservableObject {
|
||||
}
|
||||
|
||||
private func process(parser: Parser) async throws {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
context.perform {
|
||||
try await withCheckedThrowingContinuation { [weak self] continuation -> Void in
|
||||
Database.shared.performBackgroundTask { [weak self] context in
|
||||
guard let self else {
|
||||
continuation.resume()
|
||||
return
|
||||
}
|
||||
do {
|
||||
// insert new zim files
|
||||
let existing = try self.context.fetch(ZimFile.fetchRequest()).map { $0.fileID }
|
||||
let existing = try context.fetch(ZimFile.fetchRequest()).map { $0.fileID }
|
||||
var zimFileIDs = parser.zimFileIDs.subtracting(existing)
|
||||
let insertRequest = NSBatchInsertRequest(
|
||||
entity: ZimFile.entity(),
|
||||
@ -224,7 +222,7 @@ final class LibraryViewModel: ObservableObject {
|
||||
}
|
||||
)
|
||||
insertRequest.resultType = .count
|
||||
if let result = try self.context.execute(insertRequest) as? NSBatchInsertResult {
|
||||
if let result = try context.execute(insertRequest) as? NSBatchInsertResult {
|
||||
self.insertionCount = result.result as? Int ?? 0
|
||||
}
|
||||
|
||||
@ -236,10 +234,9 @@ final class LibraryViewModel: ObservableObject {
|
||||
])
|
||||
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
|
||||
deleteRequest.resultType = .resultTypeCount
|
||||
if let result = try self.context.execute(deleteRequest) as? NSBatchDeleteResult {
|
||||
if let result = try context.execute(deleteRequest) as? NSBatchDeleteResult {
|
||||
self.deletionCount = result.result as? Int ?? 0
|
||||
}
|
||||
|
||||
continuation.resume()
|
||||
} catch {
|
||||
os_log("Error saving OPDS Data: %s", log: Log.OPDS, type: .error, error.localizedDescription)
|
||||
|
@ -36,7 +36,7 @@ final class NavigationViewModel: ObservableObject {
|
||||
}
|
||||
|
||||
func navigateToMostRecentTab() {
|
||||
let context = Database.viewContext
|
||||
let context = Database.shared.viewContext
|
||||
let fetchRequest = Tab.fetchRequest(sortDescriptors: [NSSortDescriptor(key: "lastOpened", ascending: false)])
|
||||
fetchRequest.fetchLimit = 1
|
||||
let tab = (try? context.fetch(fetchRequest).first) ?? self.makeTab(context: context)
|
||||
@ -50,7 +50,7 @@ final class NavigationViewModel: ObservableObject {
|
||||
|
||||
@discardableResult
|
||||
func createTab() -> NSManagedObjectID {
|
||||
let context = Database.viewContext
|
||||
let context = Database.shared.viewContext
|
||||
let tab = self.makeTab(context: context)
|
||||
#if !os(macOS)
|
||||
currentItem = NavigationItem.tab(objectID: tab.objectID)
|
||||
@ -61,10 +61,10 @@ final class NavigationViewModel: ObservableObject {
|
||||
@MainActor
|
||||
func tabIDFor(url: URL?) -> NSManagedObjectID {
|
||||
guard let url,
|
||||
let coordinator = Database.viewContext.persistentStoreCoordinator,
|
||||
let coordinator = Database.shared.viewContext.persistentStoreCoordinator,
|
||||
let tabID = coordinator.managedObjectID(forURIRepresentation: url),
|
||||
// make sure it's not went missing
|
||||
let tab = try? Database.viewContext.existingObject(with: tabID) as? Tab,
|
||||
let tab = try? Database.shared.viewContext.existingObject(with: tabID) as? Tab,
|
||||
tab.zimFile != nil
|
||||
else {
|
||||
return createTab()
|
||||
@ -75,7 +75,7 @@ final class NavigationViewModel: ObservableObject {
|
||||
/// Delete a single tab, and select another tab
|
||||
/// - Parameter tabID: ID of the tab to delete
|
||||
func deleteTab(tabID: NSManagedObjectID) {
|
||||
Database.performBackgroundTask { context in
|
||||
Database.shared.performBackgroundTask { context in
|
||||
let sortByCreation = [NSSortDescriptor(key: "created", ascending: false)]
|
||||
guard let tabs: [Tab] = try? context.fetch(Tab.fetchRequest(predicate: nil,
|
||||
sortDescriptors: sortByCreation)),
|
||||
@ -107,7 +107,7 @@ final class NavigationViewModel: ObservableObject {
|
||||
|
||||
/// Delete all tabs, and open a new tab
|
||||
func deleteAllTabs() {
|
||||
Database.performBackgroundTask { context in
|
||||
Database.shared.performBackgroundTask { context in
|
||||
// delete all existing tabs
|
||||
let tabs = try? context.fetch(Tab.fetchRequest())
|
||||
tabs?.forEach { context.delete($0) }
|
||||
|
@ -34,7 +34,7 @@ class SearchViewModel: NSObject, ObservableObject, NSFetchedResultsControllerDel
|
||||
let predicate = NSPredicate(format: "includedInSearch == true AND fileURLBookmark != nil")
|
||||
fetchedResultsController = NSFetchedResultsController(
|
||||
fetchRequest: ZimFile.fetchRequest(predicate: predicate),
|
||||
managedObjectContext: Database.shared.container.viewContext,
|
||||
managedObjectContext: Database.shared.viewContext,
|
||||
sectionNameKeyPath: nil,
|
||||
cacheName: nil
|
||||
)
|
||||
|
@ -57,6 +57,7 @@ struct Library: View {
|
||||
.navigationTitle(NavigationItem.categories.name)
|
||||
case .downloads:
|
||||
ZimFilesDownloads(dismiss: dismiss)
|
||||
.environment(\.managedObjectContext, Database.shared.viewContext)
|
||||
case .new:
|
||||
ZimFilesNew(dismiss: dismiss)
|
||||
}
|
||||
@ -79,7 +80,7 @@ struct Library_Previews: PreviewProvider {
|
||||
NavigationStack {
|
||||
Library(dismiss: nil)
|
||||
.environmentObject(LibraryViewModel())
|
||||
.environment(\.managedObjectContext, Database.viewContext)
|
||||
.environment(\.managedObjectContext, Database.shared.viewContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -179,7 +179,10 @@ struct ZimFileDetail: View {
|
||||
}
|
||||
}()),
|
||||
primaryButton: .default(Text("zim_file.action.download.button.anyway".localized)) {
|
||||
DownloadService.shared.start(zimFileID: zimFile.id, allowsCellularAccess: false)
|
||||
DownloadService.shared.start(
|
||||
zimFileID: zimFile.id,
|
||||
allowsCellularAccess: false
|
||||
)
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
|
@ -115,7 +115,7 @@ struct ZimFilesNew_Previews: PreviewProvider {
|
||||
NavigationStack {
|
||||
ZimFilesNew(dismiss: nil)
|
||||
.environmentObject(LibraryViewModel())
|
||||
.environment(\.managedObjectContext, Database.viewContext)
|
||||
.environment(\.managedObjectContext, Database.shared.viewContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -165,8 +165,7 @@ class Languages {
|
||||
fetchRequest.resultType = .dictionaryResultType
|
||||
|
||||
let languages: [Language] = await withCheckedContinuation { continuation in
|
||||
let context = Database.shared.container.newBackgroundContext()
|
||||
context.perform {
|
||||
Database.shared.performBackgroundTask { context in
|
||||
guard let results = try? context.fetch(fetchRequest) else {
|
||||
continuation.resume(returning: [])
|
||||
return
|
||||
|
Loading…
x
Reference in New Issue
Block a user