import Foundation import Combine @Observable class BookmarkDetailViewModel { private let getBookmarkUseCase: PGetBookmarkUseCase private let getBookmarkArticleUseCase: PGetBookmarkArticleUseCase private let loadSettingsUseCase: PLoadSettingsUseCase private let updateBookmarkUseCase: PUpdateBookmarkUseCase private var addTextToSpeechQueueUseCase: PAddTextToSpeechQueueUseCase? private let getCachedArticleUseCase: PGetCachedArticleUseCase private let createAnnotationUseCase: PCreateAnnotationUseCase var bookmarkDetail: BookmarkDetail = BookmarkDetail.empty var articleContent: String = "" var articleParagraphs: [String] = [] var bookmark: Bookmark? = nil var isLoading = false var isLoadingArticle = true var errorMessage: String? var settings: Settings? var readProgress: Int = 0 var selectedAnnotationId: String? var hasAnnotations: Bool = false private var factory: UseCaseFactory? private var cancellables = Set() private let readProgressSubject = PassthroughSubject<(id: String, progress: Double, anchor: String?), Never>() init(_ factory: UseCaseFactory = DefaultUseCaseFactory.shared) { self.getBookmarkUseCase = factory.makeGetBookmarkUseCase() self.getBookmarkArticleUseCase = factory.makeGetBookmarkArticleUseCase() self.loadSettingsUseCase = factory.makeLoadSettingsUseCase() self.updateBookmarkUseCase = factory.makeUpdateBookmarkUseCase() self.getCachedArticleUseCase = factory.makeGetCachedArticleUseCase() self.createAnnotationUseCase = factory.makeCreateAnnotationUseCase() self.factory = factory readProgressSubject .debounce(for: .seconds(1), scheduler: DispatchQueue.main) .sink { [weak self] (id, progress, anchor) in let progressInt = Int(progress * 100) Task { await self?.updateReadProgress(id: id, progress: progressInt, anchor: anchor) } } .store(in: &cancellables) } @MainActor func loadBookmarkDetail(id: String) async { isLoading = true errorMessage = nil do { settings = try await loadSettingsUseCase.execute() bookmarkDetail = try await getBookmarkUseCase.execute(id: id) // Always take the higher value between server and local progress let serverProgress = bookmarkDetail.readProgress ?? 0 readProgress = max(readProgress, serverProgress) if settings?.enableTTS == true { self.addTextToSpeechQueueUseCase = factory?.makeAddTextToSpeechQueueUseCase() } } catch { errorMessage = "Error loading bookmark" } isLoading = false } @MainActor func loadArticleContent(id: String) async { isLoadingArticle = true // First, try to load from cache if let cachedHTML = getCachedArticleUseCase.execute(id: id) { articleContent = cachedHTML processArticleContent() isLoadingArticle = false Logger.viewModel.info("📱 Loaded article \(id) from cache (\(cachedHTML.utf8.count) bytes)") // Debug: Check for Base64 images let base64Count = countOccurrences(in: cachedHTML, of: "data:image/") let httpCount = countOccurrences(in: cachedHTML, of: "src=\"http") Logger.viewModel.info(" Images in cached HTML: \(base64Count) Base64, \(httpCount) HTTP") return } // If not cached, fetch from server Logger.viewModel.info("📡 Fetching article \(id) from server (not in cache)") do { articleContent = try await getBookmarkArticleUseCase.execute(id: id) processArticleContent() Logger.viewModel.info("✅ Fetched article from server (\(articleContent.utf8.count) bytes)") } catch { errorMessage = "Error loading article" Logger.viewModel.error("❌ Failed to load article: \(error.localizedDescription)") } isLoadingArticle = false } private func countOccurrences(in text: String, of substring: String) -> Int { return text.components(separatedBy: substring).count - 1 } private func processArticleContent() { let paragraphs = articleContent .components(separatedBy: .newlines) .filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } articleParagraphs = paragraphs // Check if article contains annotations hasAnnotations = articleContent.contains(" readProgress { do { try await updateBookmarkUseCase.updateReadProgress(bookmarkId: id, progress: progress, anchor: anchor) } catch { // ignore error in this case } } } func debouncedUpdateReadProgress(id: String, progress: Double, anchor: String?) { readProgressSubject.send((id, progress, anchor)) } @MainActor func createAnnotation(bookmarkId: String, color: String, text: String, startOffset: Int, endOffset: Int, startSelector: String, endSelector: String) async { do { let annotation = try await createAnnotationUseCase.execute( bookmarkId: bookmarkId, color: color, startOffset: startOffset, endOffset: endOffset, startSelector: startSelector, endSelector: endSelector ) Logger.viewModel.info("✅ Annotation created: \(annotation.id)") } catch { Logger.viewModel.error("❌ Failed to create annotation: \(error.localizedDescription)") errorMessage = "Error creating annotation" } } }