use coredata to track pdf highlights

This commit is contained in:
Satindar Dhillon 2022-04-16 23:52:54 -07:00
parent bd8798512a
commit 8ea404a8d8
5 changed files with 110 additions and 47 deletions

View file

@ -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)
}
}

View file

@ -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>

View file

@ -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
)
}
}

View file

@ -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) {

View file

@ -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)")
}
}
}