Files
omnivore/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.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

238 lines
7.9 KiB
Swift

import Models
import Services
import SwiftUI
import Views
import WebKit
struct SafariWebLink: Identifiable {
let id: UUID
let url: URL
}
@MainActor final class WebReaderViewModel: ObservableObject {
@Published var articleContent: ArticleContent?
@Published var errorMessage: String?
@Published var allowRetry = false
@Published var isDownloadingAudio: Bool = false
@Published var audioDownloadTask: Task<Void, Error>?
@Published var showSnackbar: Bool = false
var snackbarOperation: SnackbarOperation?
func snackbar(message: String) {
snackbarOperation = SnackbarOperation(message: message, undoAction: nil)
showSnackbar = true
}
func hasOriginalUrl(_ item: Models.LibraryItem) -> Bool {
if let pageURLString = item.pageURLString, let host = URL(string: pageURLString)?.host {
if host == "omnivore.app" {
return false
}
return true
}
return false
}
func downloadAudio(audioController: AudioController, item: Models.LibraryItem) {
snackbar(message: "Downloading Offline Audio")
isDownloadingAudio = true
if let audioDownloadTask = audioDownloadTask {
audioDownloadTask.cancel()
}
let itemID = item.unwrappedID
audioDownloadTask = Task.detached(priority: .background) {
let canceled = Task.isCancelled
let downloaded = await audioController.downloadForOffline(itemID: itemID)
DispatchQueue.main.async {
self.isDownloadingAudio = false
if !canceled {
self.snackbar(message: downloaded ? "Audio file downloaded" : "Error downloading audio")
}
}
}
}
func loadContent(dataService: DataService, username: String, itemID: String, retryCount _: Int = 0) async {
errorMessage = nil
do {
articleContent = try await dataService.loadArticleContentWithRetries(itemID: itemID, username: username)
} catch {
if let fetchError = error as? ContentFetchError {
allowRetry = true
switch fetchError {
case .network:
errorMessage = "We were unable to retrieve your content. Please check network connectivity and try again."
default:
errorMessage = "We were unable to parse your content."
}
} else {
errorMessage = "We were unable to retrieve your content."
}
}
}
func createHighlight(
messageBody: [String: Any],
replyHandler: @escaping WKScriptMessageReplyHandler,
dataService: DataService
) {
let result = dataService.createHighlight(
shortId: messageBody["shortId"] as? String ?? "",
highlightID: messageBody["id"] as? String ?? "",
quote: messageBody["quote"] as? String ?? "",
patch: messageBody["patch"] as? String ?? "",
articleId: messageBody["articleId"] as? String ?? "",
positionPercent: messageBody["highlightPositionPercent"] as? Double,
positionAnchorIndex: messageBody["highlightPositionAnchorIndex"] as? Int,
annotation: messageBody["annotation"] as? String ?? ""
)
return replyHandler(["result": result], nil)
}
func deleteHighlight(
messageBody: [String: Any],
replyHandler: @escaping WKScriptMessageReplyHandler,
dataService: DataService
) {
if let highlightID = messageBody["highlightId"] as? String {
dataService.deleteHighlight(highlightID: highlightID)
replyHandler(["result": true], nil)
} else {
replyHandler(["result": false], nil)
}
}
func mergeHighlight(
messageBody: [String: Any],
replyHandler: @escaping WKScriptMessageReplyHandler,
dataService: DataService
) {
guard
let shortId = messageBody["shortId"] as? String,
let highlightID = messageBody["id"] as? String,
let quote = messageBody["quote"] as? String,
let patch = messageBody["patch"] as? String,
let articleId = messageBody["articleId"] as? String,
let overlapHighlightIdList = messageBody["overlapHighlightIdList"] as? [String],
let positionPercent = messageBody["highlightPositionPercent"] as? Double,
let positionAnchorIndex = messageBody["highlightPositionAnchorIndex"] as? Int
else {
replyHandler([], "createHighlight: Error encoding response")
return
}
let jsonHighlight = dataService.mergeHighlights(
shortId: shortId,
highlightID: highlightID,
quote: quote,
patch: patch,
articleId: articleId,
positionPercent: positionPercent,
positionAnchorIndex: positionAnchorIndex,
overlapHighlightIdList: overlapHighlightIdList
)
replyHandler(["result": jsonHighlight], nil)
}
func updateHighlight(
messageBody: [String: Any],
replyHandler: @escaping WKScriptMessageReplyHandler,
dataService: DataService
) {
let highlightID = messageBody["highlightId"] as? String
let annotation = messageBody["annotation"] as? String
if let highlightID = highlightID, let annotation = annotation {
dataService.updateHighlightAttributes(highlightID: highlightID, annotation: annotation)
replyHandler(["result": highlightID], nil)
} else {
replyHandler([], "updateHighlight: Error encoding response")
}
}
func updateReadingProgress(
messageBody: [String: Any],
replyHandler: @escaping WKScriptMessageReplyHandler,
dataService: DataService
) {
let itemID = messageBody["id"] as? String
let readingProgress = messageBody["readingProgressPercent"] as? Double
let anchorIndex = messageBody["readingProgressAnchorIndex"] as? Int
print("READING PROGRESS FROM JS: ", messageBody)
guard let itemID = itemID, let readingProgress = readingProgress, let anchorIndex = anchorIndex else {
replyHandler(["result": false], nil)
return
}
dataService.updateLinkReadingProgress(itemID: itemID, readingProgress: readingProgress, anchorIndex: anchorIndex, force: false)
replyHandler(["result": true], nil)
}
func webViewActionWithReplyHandler(
message: WKScriptMessage,
replyHandler: @escaping WKScriptMessageReplyHandler,
dataService: DataService
) {
guard let messageBody = message.body as? [String: Any] else { return }
guard let actionID = messageBody["actionID"] as? String else { return }
switch actionID {
case "deleteHighlight":
deleteHighlight(messageBody: messageBody, replyHandler: replyHandler, dataService: dataService)
case "createHighlight":
createHighlight(messageBody: messageBody, replyHandler: replyHandler, dataService: dataService)
case "mergeHighlight":
mergeHighlight(messageBody: messageBody, replyHandler: replyHandler, dataService: dataService)
case "updateHighlight":
updateHighlight(messageBody: messageBody, replyHandler: replyHandler, dataService: dataService)
case "articleReadingProgress":
updateReadingProgress(messageBody: messageBody, replyHandler: replyHandler, dataService: dataService)
default:
replyHandler(nil, "Unknown actionID: \(actionID)")
}
}
func setLabelsForHighlight(
highlightID: String,
labelIDs: [String],
dataService: DataService
) {
dataService.setLabelsForHighlight(highlightID: highlightID, labelIDs: labelIDs)
}
func saveLink(dataService: DataService, url: URL) {
Task {
do {
snackbar(message: "Saving link")
print("SAVING: ", url.absoluteString)
_ = try await dataService.createPageFromUrl(id: UUID().uuidString, url: url.absoluteString)
snackbar(message: "Link saved")
} catch {
snackbar(message: "Error saving link")
}
}
}
func saveLinkAndFetch(dataService: DataService, username: String, url: URL) {
Task {
do {
snackbar(message: "Saving link")
let requestId = UUID().uuidString
_ = try await dataService.createPageFromUrl(id: requestId, url: url.absoluteString)
snackbar(message: "Link saved")
await loadContent(dataService: dataService, username: username, itemID: requestId, retryCount: 0)
} catch {
snackbar(message: "Error saving link")
}
}
}
}