Files
omnivore/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift
Jackson Harper e63b4f9b2c Abstract out fetching from view model so we can better handle multiple fetch folders
Rename LinkedItem to LibraryItem

More on following

Add new fetcher

Tab bar
2023-12-07 17:15:52 +08:00

351 lines
9.7 KiB
Swift

import CoreData
import Foundation
import Utils
public struct LinkedItemQueryResult {
public let itemIDs: [NSManagedObjectID]
public let cursor: String?
public init(itemIDs: [NSManagedObjectID], cursor: String?) {
self.itemIDs = itemIDs
self.cursor = cursor
}
}
public struct LinkedItemSyncResult {
public let updatedItemIDs: [String]
public let cursor: String?
public let hasMore: Bool
public let mostRecentUpdatedAt: Date?
public let isEmpty: Bool
public init(updatedItemIDs: [String], cursor: String?, hasMore: Bool, mostRecentUpdatedAt: Date?, isEmpty: Bool) {
self.updatedItemIDs = updatedItemIDs
self.cursor = cursor
self.hasMore = hasMore
self.mostRecentUpdatedAt = mostRecentUpdatedAt
self.isEmpty = isEmpty
}
}
public struct LinkedItemAudioProperties {
public let itemID: String
public let objectID: NSManagedObjectID
public let title: String
public let byline: String?
public let imageURL: URL?
public let language: String?
public let startIndex: Int
public let startOffset: Double
}
// Internal model used for parsing a push notification object only
public struct JSONArticle: Decodable {
public let id: String
public let title: String
public let createdAt: Date
public let updatedAt: Date
public let savedAt: Date
public let readAt: Date?
public let folder: String
public let image: String
public let readingProgressPercent: Double
public let readingProgressAnchorIndex: Int
public let slug: String
public let contentReader: String
public let url: String
public let isArchived: Bool
public let language: String?
public let wordsCount: Int?
public let downloadURL: String
}
public extension LibraryItem {
var unwrappedID: String { id ?? "" }
var unwrappedSlug: String { slug ?? "" }
var unwrappedTitle: String { title ?? "" }
var unwrappedDownloadURLString: String { downloadURL ?? "" }
var unwrappedPageURLString: String { pageURLString ?? "" }
var unwrappedSavedAt: Date { savedAt ?? Date() }
var unwrappedCreatedAt: Date { createdAt ?? Date() }
var deepLink: URL? {
if let id = id {
return URL(string: "omnivore://read/\(id)")
}
return nil
}
var hasLabels: Bool {
(labels?.count ?? 0) > 0
}
var noteHighlight: Highlight? {
if let highlights = highlights?.compactMap({ $0 as? Highlight }) {
let result = highlights
.filter { $0.type == "NOTE" }
.sorted(by: { $0.updatedAt ?? Date() < $1.updatedAt ?? Date() })
.first
return result
}
return nil
}
var noteText: String? {
noteHighlight?.annotation
}
var isUnread: Bool {
readingProgress <= 0
}
var isPartiallyRead: Bool {
Int(readingProgress) > 0
}
var isRead: Bool {
readingProgress >= 0.98
}
var isPDF: Bool {
if let contentReader = contentReader {
return contentReader == "PDF"
}
return (pageURLString ?? "").hasSuffix("pdf")
}
func hideHost(_ host: String) -> Bool {
switch host {
case "storage.googleapis.com":
return true
case "omnivore.app":
return true
default:
return false
}
}
var publisherDisplayName: String? {
if let siteName = siteName {
return siteName
}
if let host = URL(string: publisherURLString ?? pageURLString ?? "")?.host, !hideHost(host) {
return host
}
return nil
}
var imageURL: URL? {
imageURLString.flatMap { URL(string: $0) }
}
var pdfURL: URL? {
guard isPDF else { return nil }
return URL(string: pageURLString ?? "")
}
var sortedLabels: [LinkedItemLabel] {
sortedLabels(labels: labels.asArray(of: LinkedItemLabel.self))
}
func sortedLabels(labels: [LinkedItemLabel]?) -> [LinkedItemLabel] {
guard let labels = labels else {
return []
}
var colors = [String: [LinkedItemLabel]]()
for label in labels {
if let color = label.color {
var list = colors[color] ?? []
list.append(label)
colors[color] = list.sorted(by: { ($0.name ?? "").localizedCompare($1.name ?? "") == .orderedAscending })
}
}
let sortedColors = Array(colors.keys).sorted(by: { leftLabel, rightLabel -> Bool in
let aname = colors[leftLabel]?.first?.name ?? leftLabel
let bname = colors[rightLabel]?.first?.name ?? rightLabel
return aname.localizedCompare(bname) == .orderedAscending
})
var result = [LinkedItemLabel]()
for key in sortedColors {
if let items = colors[key] {
let sorted = items.sorted(by: { ($0.name ?? "").localizedCompare($1.name ?? "") == .orderedAscending })
result.append(contentsOf: sorted)
}
}
return result
}
var sortedHighlights: [Highlight] {
highlights.asArray(of: Highlight.self).sorted {
($0.createdAt ?? Date()) < ($1.createdAt ?? Date())
}
}
var labelsJSONString: String {
let labels = self.labels.asArray(of: LinkedItemLabel.self).map { label in
[
"id": NSString(string: label.id ?? ""),
"color": NSString(string: label.color ?? ""),
"name": NSString(string: label.name ?? ""),
"description": NSString(string: label.labelDescription ?? "")
]
}
guard let JSON = (try? JSONSerialization.data(withJSONObject: labels, options: .prettyPrinted)) else { return "[]" }
return String(data: JSON, encoding: .utf8) ?? "[]"
}
var recommendationsJSONString: String {
let recommendations = self.recommendations.asArray(of: Recommendation.self).map { recommendation in
let recommendedAt = recommendation.recommendedAt == nil ? nil : recommendation.recommendedAt?.ISO8601Format()
return [
"id": NSString(string: recommendation.groupID ?? ""),
"name": NSString(string: recommendation.name ?? ""),
"note": recommendation.note == nil ? nil : NSString(string: recommendation.note ?? ""),
"user": recommendation.user == nil ? nil : NSDictionary(dictionary: [
"userID": NSString(string: recommendation.user?.userID ?? ""),
"name": NSString(string: recommendation.user?.name ?? ""),
"username": NSString(string: recommendation.user?.username ?? ""),
"profileImageURL": recommendation.user?.profileImageURL as NSString? as Any
]),
"recommendedAt": recommendedAt == nil ? nil : NSString(string: recommendedAt!)
]
}
guard let JSON = try? JSONSerialization.data(withJSONObject: recommendations, options: .prettyPrinted) else {
return "[]"
}
return String(data: JSON, encoding: .utf8) ?? "[]"
}
var formattedByline: String {
var byline = ""
if let author = author {
byline += author
}
if author != nil, publisherDisplayName != nil {
byline += ""
}
if let publisherDisplayName = publisherDisplayName {
byline += publisherDisplayName
}
return byline
}
var audioProperties: LinkedItemAudioProperties {
LinkedItemAudioProperties(
itemID: unwrappedID,
objectID: objectID,
title: unwrappedTitle,
byline: formattedByline,
imageURL: imageURL,
language: language,
startIndex: Int(listenPositionIndex),
startOffset: listenPositionOffset
)
}
static func lookup(byID itemID: String, inContext context: NSManagedObjectContext) -> LibraryItem? {
let fetchRequest: NSFetchRequest<Models.LibraryItem> = LibraryItem.fetchRequest()
fetchRequest.predicate = NSPredicate(
format: "id == %@", itemID
)
var item: LibraryItem?
context.performAndWait {
item = (try? context.fetch(fetchRequest))?.first
}
return item
}
// swiftlint:disable:next cyclomatic_complexity
func update(
inContext context: NSManagedObjectContext,
newReadingProgress: Double? = nil,
newAnchorIndex: Int? = nil,
newIsArchivedValue: Bool? = nil,
newTitle: String? = nil,
newDescription: String? = nil,
newAuthor: String? = nil,
listenPositionIndex: Int? = nil,
listenPositionOffset: Double? = nil,
listenPositionTime: Double? = nil
) {
context.perform {
if let newReadingProgress = newReadingProgress {
self.readingProgress = newReadingProgress
}
if let newAnchorIndex = newAnchorIndex {
self.readingProgressAnchor = Int64(newAnchorIndex)
}
if let newIsArchivedValue = newIsArchivedValue {
self.isArchived = newIsArchivedValue
}
if let newTitle = newTitle {
self.title = newTitle
}
if let newDescription = newDescription {
self.descriptionText = newDescription
}
if let newAuthor = newAuthor {
self.author = newAuthor
}
if let listenPositionIndex = listenPositionIndex {
self.listenPositionIndex = Int64(listenPositionIndex)
}
if let listenPositionOffset = listenPositionOffset {
self.listenPositionOffset = listenPositionOffset
}
if let listenPositionTime = listenPositionTime {
self.listenPositionTime = listenPositionTime
}
guard context.hasChanges else { return }
self.updatedAt = Date()
do {
try context.save()
logger.debug("LinkedItem updated succesfully")
} catch {
context.rollback()
logger.debug("Failed to update LinkedItem: \(error.localizedDescription)")
}
}
}
func remove(inContext context: NSManagedObjectContext) {
context.perform {
context.delete(self)
do {
try context.save()
logger.debug("LinkedItem removed")
} catch {
context.rollback()
logger.debug("Failed to remove LinkedItem: \(error.localizedDescription)")
}
}
}
static func deleteItems(ids: [String], context: NSManagedObjectContext) {
for itemID in ids {
if let linkedItem = lookup(byID: itemID, inContext: context) {
linkedItem.remove(inContext: context)
}
}
}
}