mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
use coredata to track pdf highlights
This commit is contained in:
parent
bd8798512a
commit
8ea404a8d8
5 changed files with 110 additions and 47 deletions
|
|
@ -41,7 +41,7 @@ public final class PDFViewerViewModel: ObservableObject {
|
|||
for highlight in fetchedHighlights {
|
||||
resultSet[highlight.id] = highlight
|
||||
}
|
||||
for highlightId in services.dataService.fetchRemovedHighlightIds(pdfID: feedItem.id) {
|
||||
for highlightId in services.dataService.deletedHighlightsIDs {
|
||||
resultSet.removeValue(forKey: highlightId)
|
||||
}
|
||||
return Array(resultSet.values)
|
||||
|
|
@ -164,6 +164,6 @@ public final class PDFViewerViewModel: ObservableObject {
|
|||
}
|
||||
|
||||
private func removeLocalHighlights(highlightIds: [String]) {
|
||||
services.dataService.removeHighlights(pdfID: feedItem.id, highlightIds: highlightIds)
|
||||
services.dataService.removeHighlights(highlightIds: highlightIds)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,11 @@
|
|||
<entity name="PersistedArticleContent" representedClassName="PersistedArticleContent" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="htmlContent" attributeType="String"/>
|
||||
<attribute name="slug" attributeType="String"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="slug"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PersistedFeedItem" representedClassName="PersistedFeedItem" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="author" optional="YES" attributeType="String"/>
|
||||
|
|
@ -22,6 +27,11 @@
|
|||
<attribute name="slug" attributeType="String"/>
|
||||
<attribute name="title" attributeType="String"/>
|
||||
<relationship name="labels" toMany="YES" deletionRule="Nullify" destinationEntity="PersistedFeedItemLabel"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="id"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PersistedFeedItemLabel" representedClassName="PersistedFeedItemLabel" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="color" attributeType="String"/>
|
||||
|
|
@ -29,23 +39,35 @@
|
|||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="labelDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="id"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PersistedHighlight" representedClassName="PersistedHighlight" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="annotation" optional="YES" attributeType="String"/>
|
||||
<attribute name="associatedItemId" attributeType="String"/>
|
||||
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="createdByMe" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="markedForDeletion" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="patch" attributeType="String"/>
|
||||
<attribute name="prefix" optional="YES" attributeType="String"/>
|
||||
<attribute name="quote" attributeType="String"/>
|
||||
<attribute name="shortId" attributeType="String"/>
|
||||
<attribute name="suffix" optional="YES" attributeType="String"/>
|
||||
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="id"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="PersistedFeedItemLabel" positionX="-36" positionY="18" width="128" height="104"/>
|
||||
<element name="PersistedFeedItem" positionX="-18" positionY="63" width="128" height="284"/>
|
||||
<element name="PersistedArticleContent" positionX="9" positionY="108" width="128" height="59"/>
|
||||
<element name="PersistedHighlight" positionX="27" positionY="225" width="128" height="14"/>
|
||||
<element name="PersistedHighlight" positionX="27" positionY="225" width="128" height="209"/>
|
||||
</elements>
|
||||
</model>
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import CoreData
|
||||
import Foundation
|
||||
|
||||
public struct Highlight: Identifiable, Hashable, Codable {
|
||||
|
|
@ -35,4 +36,36 @@ public struct Highlight: Identifiable, Hashable, Codable {
|
|||
self.updatedAt = updatedAt
|
||||
self.createdByMe = createdByMe
|
||||
}
|
||||
|
||||
public func toManagedObject(context: NSManagedObjectContext, associatedItemID: String) -> PersistedHighlight {
|
||||
let persistedHighlight = PersistedHighlight(context: context)
|
||||
persistedHighlight.associatedItemId = associatedItemID
|
||||
persistedHighlight.markedForDeletion = false
|
||||
persistedHighlight.id = id
|
||||
persistedHighlight.shortId = shortId
|
||||
persistedHighlight.quote = quote
|
||||
persistedHighlight.prefix = prefix
|
||||
persistedHighlight.suffix = suffix
|
||||
persistedHighlight.patch = patch
|
||||
persistedHighlight.annotation = annotation
|
||||
persistedHighlight.createdAt = createdAt
|
||||
persistedHighlight.updatedAt = updatedAt
|
||||
persistedHighlight.createdByMe = createdByMe
|
||||
return persistedHighlight
|
||||
}
|
||||
|
||||
public static func make(from persistedHighlight: PersistedHighlight) -> Highlight {
|
||||
Highlight(
|
||||
id: persistedHighlight.id ?? "",
|
||||
shortId: persistedHighlight.shortId ?? "",
|
||||
quote: persistedHighlight.quote ?? "",
|
||||
prefix: persistedHighlight.prefix,
|
||||
suffix: persistedHighlight.suffix,
|
||||
patch: persistedHighlight.patch ?? "",
|
||||
annotation: persistedHighlight.annotation,
|
||||
createdByMe: persistedHighlight.createdByMe,
|
||||
createdAt: persistedHighlight.createdAt,
|
||||
updatedAt: persistedHighlight.updatedAt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,11 +11,9 @@ public final class DataService: ObservableObject {
|
|||
public internal(set) var currentViewer: Viewer?
|
||||
let networker: Networker
|
||||
|
||||
let highlightsCache = NSCache<AnyObject, CachedPDFHighlights>()
|
||||
let highlightsCacheQueue = DispatchQueue(label: "app.omnivore.highlights.cache.queue", attributes: .concurrent)
|
||||
|
||||
let persistentContainer: PersistentContainer
|
||||
var subscriptions = Set<AnyCancellable>()
|
||||
public var deletedHighlightsIDs = Set<String>()
|
||||
|
||||
public init(appEnvironment: AppEnvironment, networker: Networker) {
|
||||
self.appEnvironment = appEnvironment
|
||||
|
|
@ -30,7 +28,21 @@ public final class DataService: ObservableObject {
|
|||
}
|
||||
|
||||
public func clearHighlights() {
|
||||
highlightsCache.removeAllObjects()
|
||||
deletedHighlightsIDs.removeAll()
|
||||
|
||||
let fetchRequest: NSFetchRequest<Models.PersistedHighlight> = PersistedHighlight.fetchRequest()
|
||||
|
||||
let highlights = (try? persistentContainer.viewContext.fetch(fetchRequest)) ?? []
|
||||
|
||||
for highlight in highlights {
|
||||
persistentContainer.viewContext.delete(highlight)
|
||||
}
|
||||
|
||||
do {
|
||||
try persistentContainer.viewContext.save()
|
||||
} catch {
|
||||
print("failed to delete objects")
|
||||
}
|
||||
}
|
||||
|
||||
public func switchAppEnvironment(appEnvironment: AppEnvironment) {
|
||||
|
|
|
|||
|
|
@ -1,57 +1,53 @@
|
|||
import Combine
|
||||
import CoreData
|
||||
import Foundation
|
||||
import Models
|
||||
|
||||
final class CachedPDFHighlights {
|
||||
init(pdfID: String, highlights: [Highlight], removedHighlightIDs: [String]) {
|
||||
self.pdfID = pdfID
|
||||
self.highlights = highlights
|
||||
self.removedHighlightIDs = removedHighlightIDs
|
||||
}
|
||||
|
||||
let pdfID: String
|
||||
var highlights: [Highlight]
|
||||
var removedHighlightIDs: [String]
|
||||
}
|
||||
|
||||
public extension DataService {
|
||||
func cachedHighlights(pdfID: String) -> [Highlight] {
|
||||
fetchCachedHighlights(pdfID: pdfID as NSString)?.highlights ?? []
|
||||
}
|
||||
let fetchRequest: NSFetchRequest<Models.PersistedHighlight> = PersistedHighlight.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(
|
||||
format: "associatedItemId = %@ AND markedForDeletion = %@", pdfID, false
|
||||
)
|
||||
|
||||
func fetchRemovedHighlightIds(pdfID: String) -> [String] {
|
||||
fetchCachedHighlights(pdfID: pdfID as NSString)?.removedHighlightIDs ?? []
|
||||
let highlights = (try? persistentContainer.viewContext.fetch(fetchRequest)) ?? []
|
||||
return highlights.map { Highlight.make(from: $0) }
|
||||
}
|
||||
|
||||
func persistHighlight(pdfID: String, highlight: Highlight) {
|
||||
let cachedHighlights =
|
||||
fetchCachedHighlights(pdfID: pdfID as NSString)
|
||||
?? CachedPDFHighlights(pdfID: pdfID, highlights: [], removedHighlightIDs: [])
|
||||
_ = highlight.toManagedObject(
|
||||
context: persistentContainer.viewContext,
|
||||
associatedItemID: pdfID
|
||||
)
|
||||
|
||||
cachedHighlights.highlights.append(highlight)
|
||||
insertCachedHighlights(highlights: cachedHighlights, pdfID: pdfID as NSString)
|
||||
}
|
||||
|
||||
func removeHighlights(pdfID: String, highlightIds: [String]) {
|
||||
let cachedHighlights =
|
||||
fetchCachedHighlights(pdfID: pdfID as NSString)
|
||||
?? CachedPDFHighlights(pdfID: pdfID, highlights: [], removedHighlightIDs: [])
|
||||
|
||||
cachedHighlights.removedHighlightIDs.append(contentsOf: highlightIds)
|
||||
insertCachedHighlights(highlights: cachedHighlights, pdfID: pdfID as NSString)
|
||||
}
|
||||
|
||||
private func fetchCachedHighlights(pdfID: NSString) -> CachedPDFHighlights? {
|
||||
var cachedHighlights: CachedPDFHighlights?
|
||||
highlightsCacheQueue.sync {
|
||||
cachedHighlights = highlightsCache.object(forKey: pdfID as NSString)
|
||||
do {
|
||||
try persistentContainer.viewContext.save()
|
||||
print("PersistedHighlight saved succesfully")
|
||||
} catch {
|
||||
persistentContainer.viewContext.rollback()
|
||||
print("Failed to save PersistedHighlight: \(error)")
|
||||
}
|
||||
return cachedHighlights
|
||||
}
|
||||
|
||||
private func insertCachedHighlights(highlights: CachedPDFHighlights, pdfID: NSString) {
|
||||
highlightsCacheQueue.async(flags: .barrier) {
|
||||
self.highlightsCache.setObject(highlights, forKey: pdfID as AnyObject)
|
||||
func removeHighlights(highlightIds: [String]) {
|
||||
for highlightID in highlightIds {
|
||||
deletedHighlightsIDs.insert(highlightID)
|
||||
}
|
||||
|
||||
let fetchRequest: NSFetchRequest<Models.PersistedHighlight> = PersistedHighlight.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(format: "id IN %@", highlightIds)
|
||||
guard let highlights = try? persistentContainer.viewContext.fetch(fetchRequest) else { return }
|
||||
|
||||
for highlight in highlights {
|
||||
highlight.markedForDeletion = true
|
||||
}
|
||||
|
||||
do {
|
||||
try persistentContainer.viewContext.save()
|
||||
print("PersistedHighlight(s) updated succesfully")
|
||||
} catch {
|
||||
persistentContainer.viewContext.rollback()
|
||||
print("Failed to update PersistedHighlight(s): \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue