162 lines
5.7 KiB
Swift
162 lines
5.7 KiB
Swift
import Foundation
|
|
import SwiftUI
|
|
import Combine
|
|
|
|
/// 首页 ViewModel
|
|
@MainActor
|
|
class HomeViewModel: ObservableObject {
|
|
/// 分类列表(包含手动注入的"推荐"分类)。
|
|
@Published var sorts: [MovieSort.SortData] = []
|
|
/// 当前选中的分类。
|
|
@Published var selectedSort: MovieSort.SortData?
|
|
/// 首页推荐内容(对应"推荐"分类)。
|
|
@Published var homeVideos: [Movie.Video] = []
|
|
/// 普通分类的视频列表(分页加载)。
|
|
@Published var categoryVideos: [Movie.Video] = []
|
|
/// 页面加载状态(分类加载与分页共用)。
|
|
@Published var isLoading = false
|
|
/// 当前分类的分页页码。
|
|
@Published var currentPage = 1
|
|
/// 是否还有下一页。
|
|
@Published var hasMore = true
|
|
/// 错误提示文案。
|
|
@Published var errorMessage: String?
|
|
|
|
/// 源数据访问服务。
|
|
private let sourceService = SourceService.shared
|
|
/// 标记上次加载是否因网络错误失败(用于网络恢复自动重试)。
|
|
private var lastLoadFailedDueToNetwork = false
|
|
private var networkRestoredCancellable: AnyCancellable?
|
|
|
|
init() {
|
|
setupNetworkRestoredAutoRetry()
|
|
}
|
|
|
|
/// 加载分类列表
|
|
func loadSorts() async {
|
|
guard let source = ApiConfig.shared.homeSourceBean else { return }
|
|
isLoading = true
|
|
errorMessage = nil
|
|
|
|
do {
|
|
let result = try await sourceService.getSort(sourceBean: source)
|
|
|
|
// 插入本地"推荐"分类,保持 UI 与 Android 版本习惯一致。
|
|
var allSorts = [MovieSort.SortData.home()]
|
|
allSorts.append(contentsOf: result.sorts)
|
|
|
|
self.sorts = allSorts
|
|
self.homeVideos = result.homeVideos
|
|
lastLoadFailedDueToNetwork = false
|
|
|
|
if selectedSort == nil {
|
|
selectedSort = allSorts.first
|
|
}
|
|
} catch {
|
|
errorMessage = error.localizedDescription
|
|
lastLoadFailedDueToNetwork = error.isNetworkConnectionError
|
|
}
|
|
|
|
isLoading = false
|
|
}
|
|
|
|
/// 网络恢复时,若上次因网络错误导致首页为空,自动重新加载。
|
|
private func setupNetworkRestoredAutoRetry() {
|
|
networkRestoredCancellable = NetworkMonitor.shared.networkRestoredPublisher
|
|
.sink { [weak self] in
|
|
guard let self else { return }
|
|
Task { @MainActor [weak self] in
|
|
guard let self else { return }
|
|
guard self.lastLoadFailedDueToNetwork || (self.sorts.isEmpty && self.homeVideos.isEmpty) else { return }
|
|
await self.refresh()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 选择分类
|
|
func selectSort(_ sort: MovieSort.SortData) {
|
|
// 切分类时先重置分页状态,避免旧分类残留数据闪烁。
|
|
selectedSort = sort
|
|
errorMessage = nil
|
|
categoryVideos = []
|
|
currentPage = 1
|
|
hasMore = true
|
|
|
|
if sort.id == "home" {
|
|
return
|
|
} else {
|
|
Task {
|
|
await loadCategoryVideos(page: 1, sort: sort)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 加载分类视频列表
|
|
private func loadCategoryVideos(page: Int, sort: MovieSort.SortData) async {
|
|
guard sort.id != "home" else { return }
|
|
guard let source = ApiConfig.shared.homeSourceBean else { return }
|
|
// 防重复并发加载,避免分页错序。
|
|
guard !isLoading else { return }
|
|
|
|
isLoading = true
|
|
defer { isLoading = false }
|
|
|
|
do {
|
|
let videos = try await sourceService.getList(sourceBean: source, sortData: sort, page: page)
|
|
|
|
// 分类切换过程中,丢弃旧请求结果
|
|
guard selectedSort?.id == sort.id else { return }
|
|
|
|
if page == 1 {
|
|
categoryVideos = videos
|
|
} else {
|
|
categoryVideos.append(contentsOf: videos)
|
|
}
|
|
// 以"返回非空"作为是否继续分页的轻量判断。
|
|
currentPage = page
|
|
hasMore = !videos.isEmpty
|
|
} catch {
|
|
guard selectedSort?.id == sort.id else { return }
|
|
errorMessage = error.localizedDescription
|
|
}
|
|
}
|
|
|
|
/// 加载下一页
|
|
func loadMore() async {
|
|
guard let lastItem = categoryVideos.last else { return }
|
|
await loadMoreIfNeeded(currentItem: lastItem)
|
|
}
|
|
|
|
/// 当最后一个元素出现时触发加载下一页
|
|
func loadMoreIfNeeded(currentItem: Movie.Video) async {
|
|
guard selectedSort?.id != "home" else { return }
|
|
guard hasMore, !isLoading else { return }
|
|
guard categoryVideos.last?.id == currentItem.id else { return }
|
|
guard let sort = selectedSort else { return }
|
|
|
|
let nextPage = currentPage + 1
|
|
await loadCategoryVideos(page: nextPage, sort: sort)
|
|
}
|
|
|
|
/// 刷新
|
|
func refresh() async {
|
|
// 全量刷新时重置分页与错误态,再重新拉分类与当前分类内容。
|
|
currentPage = 1
|
|
hasMore = true
|
|
categoryVideos = []
|
|
errorMessage = nil
|
|
await loadSorts()
|
|
|
|
guard let sort = selectedSort else { return }
|
|
if sort.id == "home" { return }
|
|
|
|
if let matchedSort = sorts.first(where: { $0.id == sort.id }) {
|
|
selectedSort = matchedSort
|
|
await loadCategoryVideos(page: 1, sort: matchedSort)
|
|
} else if let firstCategory = sorts.first(where: { $0.id != "home" }) {
|
|
selectedSort = firstCategory
|
|
await loadCategoryVideos(page: 1, sort: firstCategory)
|
|
}
|
|
}
|
|
}
|