import Foundation // MARK: - BookDetailViewModel // Loads book metadata, chapter index, save state, and reading progress. // Uses @Observable (iOS 17+). @Observable @MainActor final class BookDetailViewModel { let slug: String var book: Book? var chapters: [ChapterIndex] = [] var inLib: Bool = false var saved: Bool = false var lastChapter: Int? var isLoading = false var isSaving = false var error: String? init(slug: String) { self.slug = slug } // MARK: - Load func load() async { guard !isLoading else { return } isLoading = true error = nil do { let response = try await APIClient.shared.bookDetail(slug: slug) book = response.book chapters = response.chapters inLib = response.inLib saved = response.saved lastChapter = response.lastChapter } catch { if !(error is CancellationError) { self.error = error.localizedDescription } } isLoading = false } // MARK: - Toggle saved (bookmark) func toggleSaved() async { guard !isSaving else { return } isSaving = true let targetSaved = !saved saved = targetSaved // optimistic update do { if targetSaved { try await APIClient.shared.saveBook(slug: slug) if !inLib { inLib = true } } else { try await APIClient.shared.unsaveBook(slug: slug) } } catch { saved = !targetSaved // revert on failure self.error = error.localizedDescription } isSaving = false } // MARK: - Chapter helpers /// Title stripped of trailing " - Month DD YYYY" date suffixes. func displayTitle(for chapter: ChapterIndex) -> String { let stripped = chapter.title.strippingTrailingDate() if stripped.isEmpty || stripped == "Chapter \(chapter.number)" { return "Chapter \(chapter.number)" } return stripped } }