Files
libnovel/ios/LibNovelV2/ViewModels/BrowseViewModel.swift
Admin 7413313100
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
fix: update integration_test.go to match server.New signature (version, commit args)
2026-03-14 14:25:46 +05:00

147 lines
4.4 KiB
Swift

import Foundation
// MARK: - BrowseViewModel
// Powers both the Discover shelves (BrowseView) and the full paginated grid (BrowseCategoryView).
// Uses @Observable (iOS 17+).
@Observable
@MainActor
final class BrowseViewModel {
// MARK: - Discover shelves (BrowseView)
var trending: [BrowseNovel] = []
var newReleases: [BrowseNovel] = []
var recentlyUpdated: [BrowseNovel] = []
var ranking: [BrowseNovel] = []
// MARK: - Paginated grid (BrowseCategoryView)
var novels: [BrowseNovel] = []
var currentPage = 1
var hasNext = false
// Filter params (BrowseCategoryView sets these before calling loadFirstPage)
var sort: String = "popular"
var genre: String = "all"
var status: String = "all"
// MARK: - UI state
var isLoading = false
var isLoadingMore = false
var error: String?
// MARK: - Discover load (fetches multiple shelves in parallel)
func loadShelves() async {
isLoading = true
error = nil
do {
async let trendingTask = APIClient.shared.browse(page: 1, genre: "all", sort: "popular", status: "all")
async let newTask = APIClient.shared.browse(page: 1, genre: "all", sort: "new", status: "all")
async let updatedTask = APIClient.shared.browse(page: 1, genre: "all", sort: "update", status: "all")
async let rankingTask = APIClient.shared.ranking()
let (trendingResp, newResp, updatedResp, rankItems) = try await (
trendingTask, newTask, updatedTask, rankingTask
)
trending = Array(trendingResp.novels.prefix(12))
newReleases = Array(newResp.novels.prefix(12))
recentlyUpdated = Array(updatedResp.novels.prefix(12))
ranking = rankItems.prefix(12).map { item in
BrowseNovelFromRanking(item)
}
} catch {
if !(error is CancellationError) {
self.error = error.localizedDescription
}
}
isLoading = false
}
// MARK: - Paginated category load
func loadFirstPage() async {
guard !isLoading else { return }
novels = []
currentPage = 1
hasNext = false
isLoading = true
error = nil
do {
let resp = try await APIClient.shared.browse(
page: 1, genre: genre, sort: sort, status: status)
novels = resp.novels
currentPage = resp.page
hasNext = resp.hasNext
} catch {
if !(error is CancellationError) {
self.error = error.localizedDescription
}
}
isLoading = false
}
func loadNextPage() async {
guard hasNext, !isLoadingMore, !isLoading else { return }
isLoadingMore = true
let next = currentPage + 1
do {
let resp = try await APIClient.shared.browse(
page: next, genre: genre, sort: sort, status: status)
novels += resp.novels
currentPage = resp.page
hasNext = resp.hasNext
} catch {
// Silently ignore user can scroll again
}
isLoadingMore = false
}
// MARK: - Ranking load (for rank sort mode)
func loadRanking() async {
guard !isLoading else { return }
novels = []
hasNext = false
isLoading = true
error = nil
do {
let items = try await APIClient.shared.ranking()
novels = items.map { BrowseNovelFromRanking($0) }
} catch {
if !(error is CancellationError) {
self.error = error.localizedDescription
}
}
isLoading = false
}
}
// MARK: - RankingItem BrowseNovel adapter
private func BrowseNovelFromRanking(_ item: RankingItem) -> BrowseNovel {
// Synthesise a minimal JSON blob so we can decode via the standard init
let rankStr = "#\(item.rank)"
let dict: [String: Any] = [
"slug": item.slug,
"title": item.title,
"cover": item.cover,
"rank": rankStr,
"rating": "",
"chapters": "",
"url": item.sourceURL,
"author": item.author,
"status": item.status,
"genres": item.genres
]
let data = try! JSONSerialization.data(withJSONObject: dict)
return try! JSONDecoder.apiDecoder.decode(BrowseNovel.self, from: data)
}