import Foundation // MARK: - LibraryViewModel // Loads library items and exposes filtered/sorted views for LibraryView. // Uses @Observable (iOS 17+). enum LibrarySortOrder: String, CaseIterable { case recent = "Recent" case title = "Title" case author = "Author" case progress = "Progress" } enum LibraryReadingFilter: String, CaseIterable { case all = "All" case inProgress = "In Progress" case completed = "Completed" } @Observable @MainActor final class LibraryViewModel { // Raw data var items: [LibraryItem] = [] var progressMap: [String: Int] = [:] // slug -> last chapter read // Filter & sort state var sortOrder: LibrarySortOrder = .recent var readingFilter: LibraryReadingFilter = .all var selectedGenre: String = "All" // UI state var isLoading = false var error: String? // MARK: - Derived var allGenres: [String] { var seen = Set() var result: [String] = ["All"] for item in items { for genre in item.book.genres where !seen.contains(genre) { seen.insert(genre) result.append(genre) } } return result } var filteredItems: [LibraryItem] { var list = items // Genre filter if selectedGenre != "All" { list = list.filter { $0.book.genres.contains(selectedGenre) } } // Reading filter switch readingFilter { case .all: break case .inProgress: list = list.filter { item in let ch = progressMap[item.book.slug] ?? item.lastChapter ?? 0 return ch > 0 && ch < item.book.totalChapters } case .completed: list = list.filter { item in let ch = progressMap[item.book.slug] ?? item.lastChapter ?? 0 return item.book.totalChapters > 0 && ch >= item.book.totalChapters } } // Sort switch sortOrder { case .recent: // server already returns newest-saved first; preserve order break case .title: list.sort { $0.book.title.localizedCaseInsensitiveCompare($1.book.title) == .orderedAscending } case .author: list.sort { $0.book.author.localizedCaseInsensitiveCompare($1.book.author) == .orderedAscending } case .progress: list.sort { a, b in let pa = progressFraction(for: a) let pb = progressFraction(for: b) return pa > pb } } return list } // MARK: - Progress helpers func lastChapter(for item: LibraryItem) -> Int { progressMap[item.book.slug] ?? item.lastChapter ?? 0 } func progressFraction(for item: LibraryItem) -> Double { let total = item.book.totalChapters guard total > 0 else { return 0 } return Double(lastChapter(for: item)) / Double(total) } func progressPercent(for item: LibraryItem) -> String { let fraction = progressFraction(for: item) let pct = fraction * 100 if pct < 10 { return String(format: "%.1f%%", pct) } else { return String(format: "%.0f%%", pct) } } func isCompleted(for item: LibraryItem) -> Bool { let total = item.book.totalChapters guard total > 0 else { return false } return lastChapter(for: item) >= total } // MARK: - Load func load() async { isLoading = true error = nil do { async let libraryTask = APIClient.shared.library() async let progressTask = APIClient.shared.progress() let (library, progressEntries) = try await (libraryTask, progressTask) items = library progressMap = Dictionary(uniqueKeysWithValues: progressEntries.map { ($0.slug, $0.chapter) }) } catch { if !(error is CancellationError) { self.error = error.localizedDescription } } isLoading = false } // MARK: - Mutations func removeFromLibrary(slug: String) async { // Optimistic remove items.removeAll { $0.book.slug == slug } do { try await APIClient.shared.unsaveBook(slug: slug) } catch { // Silently fail — user can pull-to-refresh to restore } } func markFinished(item: LibraryItem) async { let total = item.book.totalChapters guard total > 0 else { return } progressMap[item.book.slug] = total do { try await APIClient.shared.setProgress(slug: item.book.slug, chapter: total) } catch { // Silently fail } } }