fix: update integration_test.go to match server.New signature (version, commit args)
All checks were successful
CI / Scraper / Lint (push) Successful in 10s
CI / Scraper / Test (push) Successful in 14s
Release / Scraper / Test (push) Successful in 18s
CI / Scraper / Lint (pull_request) Successful in 18s
Release / UI / Build (push) Successful in 23s
CI / Scraper / Test (pull_request) Successful in 15s
CI / UI / Build (pull_request) Successful in 32s
Release / Scraper / Docker (push) Successful in 55s
CI / Scraper / Docker Push (pull_request) Has been skipped
CI / UI / Docker Push (pull_request) Has been skipped
CI / Scraper / Docker Push (push) Successful in 1m5s
Release / UI / Docker (push) Successful in 1m12s
iOS CI / Build (push) Successful in 4m18s
iOS CI / Build (pull_request) Successful in 4m25s
iOS CI / Test (push) Successful in 8m11s
iOS CI / Test (pull_request) Successful in 8m21s
All checks were successful
CI / Scraper / Lint (push) Successful in 10s
CI / Scraper / Test (push) Successful in 14s
Release / Scraper / Test (push) Successful in 18s
CI / Scraper / Lint (pull_request) Successful in 18s
Release / UI / Build (push) Successful in 23s
CI / Scraper / Test (pull_request) Successful in 15s
CI / UI / Build (pull_request) Successful in 32s
Release / Scraper / Docker (push) Successful in 55s
CI / Scraper / Docker Push (pull_request) Has been skipped
CI / UI / Docker Push (pull_request) Has been skipped
CI / Scraper / Docker Push (push) Successful in 1m5s
Release / UI / Docker (push) Successful in 1m12s
iOS CI / Build (push) Successful in 4m18s
iOS CI / Build (pull_request) Successful in 4m25s
iOS CI / Test (push) Successful in 8m11s
iOS CI / Test (pull_request) Successful in 8m21s
This commit is contained in:
164
ios/LibNovelV2/ViewModels/LibraryViewModel.swift
Normal file
164
ios/LibNovelV2/ViewModels/LibraryViewModel.swift
Normal file
@@ -0,0 +1,164 @@
|
||||
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<String>()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user