249 lines
8.3 KiB
Swift
249 lines
8.3 KiB
Swift
import Models
|
|
import Services
|
|
import SwiftUI
|
|
import Views
|
|
import WebKit
|
|
import Utils
|
|
|
|
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 operationMessage: String?
|
|
@Published var showOperationToast: Bool = false
|
|
@Published var operationStatus: OperationStatus = .none
|
|
|
|
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.show(message: "Downloading Offline Audio", dismissAfter: 2000)
|
|
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 {
|
|
Snackbar.show(message: downloaded ? "Audio file downloaded" : "Error downloading audio", dismissAfter: 2000)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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.show(message: "Saving link", dismissAfter: 5000)
|
|
_ = try await dataService.createPageFromUrl(id: UUID().uuidString, url: url.absoluteString)
|
|
Snackbar.show(message: "Link saved", dismissAfter: 2000)
|
|
} catch {
|
|
Snackbar.show(message: "Error saving link", dismissAfter: 2000)
|
|
}
|
|
}
|
|
}
|
|
|
|
func saveLinkAndFetch(dataService: DataService, username: String, url: URL) {
|
|
Task {
|
|
do {
|
|
Snackbar.show(message: "Saving link", dismissAfter: 5000)
|
|
let requestId = UUID().uuidString
|
|
_ = try await dataService.createPageFromUrl(id: requestId, url: url.absoluteString)
|
|
Snackbar.show(message: "Link saved", dismissAfter: 2000)
|
|
|
|
await loadContent(dataService: dataService, username: username, itemID: requestId, retryCount: 0)
|
|
} catch {
|
|
Snackbar.show(message: "Error saving link", dismissAfter: 2000)
|
|
}
|
|
}
|
|
}
|
|
|
|
func trackReadEvent(item: Models.LibraryItem) {
|
|
let itemID = item.unwrappedID
|
|
let slug = item.unwrappedSlug
|
|
let originalArticleURL = item.unwrappedPageURLString
|
|
|
|
EventTracker.track(
|
|
.linkRead(
|
|
linkID: itemID,
|
|
slug: slug,
|
|
reader: "WEB",
|
|
originalArticleURL: originalArticleURL
|
|
)
|
|
)
|
|
}
|
|
}
|