330 lines
10 KiB
Swift
330 lines
10 KiB
Swift
//
|
|
// TravelAlbumDetailViewModel.swift
|
|
// suixinkan
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/// 旅拍相册详情 ViewModel,负责相册信息、素材分页筛选和删除状态。
|
|
final class TravelAlbumDetailViewModel {
|
|
enum Tab: Int, Sendable {
|
|
case all = 0
|
|
case purchased = 1
|
|
}
|
|
|
|
/// 排序方式。
|
|
enum SortOption: Int, CaseIterable, Sendable {
|
|
case createdAsc = 1
|
|
case createdDesc = 2
|
|
case fileNameAsc = 3
|
|
case fileNameDesc = 4
|
|
|
|
var title: String {
|
|
switch self {
|
|
case .createdAsc: "创建时间正序"
|
|
case .createdDesc: "创建时间倒序"
|
|
case .fileNameAsc: "文件名正序"
|
|
case .fileNameDesc: "文件名倒序"
|
|
}
|
|
}
|
|
|
|
var compactTitle: String {
|
|
switch self {
|
|
case .createdAsc: "时间 ↑"
|
|
case .createdDesc: "时间 ↓"
|
|
case .fileNameAsc: "名称 ↑"
|
|
case .fileNameDesc: "名称 ↓"
|
|
}
|
|
}
|
|
}
|
|
|
|
private(set) var album: TravelAlbum?
|
|
private(set) var materials: [TravelAlbumMaterial] = []
|
|
private(set) var allPhotoCount = 0
|
|
private(set) var purchasedPhotoCount = 0
|
|
private(set) var selectedTab: Tab = .all
|
|
private(set) var sortOption: SortOption = .createdDesc
|
|
private(set) var isLoading = true
|
|
private(set) var isRefreshing = false
|
|
private(set) var isSelectionMode = false
|
|
private(set) var selectedMaterialIds: Set<Int> = []
|
|
|
|
var onStateChange: (() -> Void)?
|
|
var onShowMessage: ((String) -> Void)?
|
|
var onDeletedAlbum: ((Int) -> Void)?
|
|
|
|
let albumId: Int
|
|
private var currentPage = 1
|
|
private var canLoadMore = false
|
|
private var isLoadingMore = false
|
|
private let pageSize = 30
|
|
|
|
init(albumId: Int) {
|
|
self.albumId = albumId
|
|
}
|
|
|
|
/// 重新加载详情与素材。
|
|
func refreshAll(api: any TravelAlbumServing) async {
|
|
guard albumId > 0 else {
|
|
isLoading = false
|
|
onShowMessage?("相册不存在")
|
|
notifyStateChange()
|
|
return
|
|
}
|
|
isRefreshing = true
|
|
notifyStateChange()
|
|
await loadAlbumInfo(api: api)
|
|
await loadAllPhotoCount(api: api)
|
|
await loadPurchasedPhotoCount(api: api)
|
|
await loadMaterials(reset: true, api: api)
|
|
isRefreshing = false
|
|
isLoading = false
|
|
notifyStateChange()
|
|
}
|
|
|
|
/// 拉取相册详情。
|
|
func loadAlbumInfo(api: any TravelAlbumServing) async {
|
|
do {
|
|
album = try await api.info(id: albumId)
|
|
notifyStateChange()
|
|
} catch is CancellationError {
|
|
return
|
|
} catch {
|
|
onShowMessage?(error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
/// 拉取全部照片数量。
|
|
func loadAllPhotoCount(api: any TravelAlbumServing) async {
|
|
do {
|
|
let response = try await api.materialList(
|
|
userEquityTravelId: albumId,
|
|
page: 1,
|
|
pageSize: 1,
|
|
orderBy: sortOption.rawValue,
|
|
isPurchased: nil
|
|
)
|
|
allPhotoCount = response.total
|
|
notifyStateChange()
|
|
} catch is CancellationError {
|
|
return
|
|
} catch {
|
|
return
|
|
}
|
|
}
|
|
|
|
/// 拉取已购照片数量;失败时保留已有数量,不影响素材列表加载。
|
|
func loadPurchasedPhotoCount(api: any TravelAlbumServing) async {
|
|
do {
|
|
let response = try await api.materialList(
|
|
userEquityTravelId: albumId,
|
|
page: 1,
|
|
pageSize: 1,
|
|
orderBy: sortOption.rawValue,
|
|
isPurchased: 1
|
|
)
|
|
purchasedPhotoCount = response.total
|
|
notifyStateChange()
|
|
} catch is CancellationError {
|
|
return
|
|
} catch {
|
|
return
|
|
}
|
|
}
|
|
|
|
/// 摘要卡封面地址,依次回退相册封面、首张素材封面和首张素材原图。
|
|
var displayCoverURL: String {
|
|
TravelAlbumDisplayFormatter.albumCoverURL(album: album, materials: materials)
|
|
}
|
|
|
|
/// 当前 Tab 对应的原图项目总数,供全屏预览索引使用。
|
|
var currentPhotoCount: Int {
|
|
selectedTab == .all ? allPhotoCount : purchasedPhotoCount
|
|
}
|
|
|
|
/// 切换素材 tab。
|
|
func selectTab(_ tab: Tab, api: any TravelAlbumServing) async {
|
|
guard selectedTab != tab else { return }
|
|
selectedTab = tab
|
|
isSelectionMode = false
|
|
selectedMaterialIds = []
|
|
notifyStateChange()
|
|
await loadMaterials(reset: true, api: api)
|
|
}
|
|
|
|
/// 切换排序。
|
|
func setSortOption(_ option: SortOption, api: any TravelAlbumServing) async {
|
|
guard sortOption != option else { return }
|
|
sortOption = option
|
|
notifyStateChange()
|
|
await loadMaterials(reset: true, api: api)
|
|
}
|
|
|
|
/// 拉取素材列表。
|
|
func loadMaterials(reset: Bool, api: any TravelAlbumServing) async {
|
|
guard albumId > 0 else { return }
|
|
if reset {
|
|
currentPage = 1
|
|
canLoadMore = false
|
|
} else {
|
|
guard canLoadMore, !isLoadingMore else { return }
|
|
isLoadingMore = true
|
|
currentPage += 1
|
|
}
|
|
|
|
do {
|
|
let response = try await api.materialList(
|
|
userEquityTravelId: albumId,
|
|
page: currentPage,
|
|
pageSize: pageSize,
|
|
orderBy: sortOption.rawValue,
|
|
isPurchased: selectedTab == .purchased ? 1 : nil
|
|
)
|
|
materials = reset ? response.list : materials + response.list
|
|
canLoadMore = materials.count < response.total
|
|
if selectedTab == .all {
|
|
allPhotoCount = response.total
|
|
} else {
|
|
purchasedPhotoCount = response.total
|
|
}
|
|
isLoadingMore = false
|
|
notifyStateChange()
|
|
} catch is CancellationError {
|
|
return
|
|
} catch {
|
|
if !reset && currentPage > 1 {
|
|
currentPage -= 1
|
|
}
|
|
isLoadingMore = false
|
|
onShowMessage?(error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
/// 根据滚动位置触发加载更多。
|
|
func loadMoreIfNeeded(lastVisibleIndex: Int, api: any TravelAlbumServing) async {
|
|
if lastVisibleIndex >= materials.count - 6 {
|
|
await loadMaterials(reset: false, api: api)
|
|
}
|
|
}
|
|
|
|
/// 重新拉取当前筛选、排序下已经加载的分页范围,供全屏预览刷新关联图片。
|
|
func reloadLoadedMaterials(
|
|
api: any TravelAlbumServing
|
|
) async throws -> TravelAlbumListResponse<TravelAlbumMaterial> {
|
|
let requestedPageCount = max(currentPage, 1)
|
|
var refreshed: [TravelAlbumMaterial] = []
|
|
var refreshedTotal = 0
|
|
var loadedPageCount = 0
|
|
|
|
for page in 1 ... requestedPageCount {
|
|
let response = try await api.materialList(
|
|
userEquityTravelId: albumId,
|
|
page: page,
|
|
pageSize: pageSize,
|
|
orderBy: sortOption.rawValue,
|
|
isPurchased: selectedTab == .purchased ? 1 : nil
|
|
)
|
|
if page == 1 { refreshedTotal = response.total }
|
|
refreshed.append(contentsOf: response.list)
|
|
loadedPageCount = page
|
|
if refreshed.count >= refreshedTotal || response.list.isEmpty { break }
|
|
}
|
|
|
|
var seen = Set<Int>()
|
|
materials = refreshed.filter { seen.insert($0.id).inserted }
|
|
currentPage = max(loadedPageCount, 1)
|
|
canLoadMore = materials.count < refreshedTotal
|
|
selectedMaterialIds.formIntersection(materials.map(\.id))
|
|
if selectedTab == .all {
|
|
allPhotoCount = refreshedTotal
|
|
} else {
|
|
purchasedPhotoCount = refreshedTotal
|
|
}
|
|
notifyStateChange()
|
|
return TravelAlbumListResponse(total: refreshedTotal, list: materials)
|
|
}
|
|
|
|
/// 切换选择模式。
|
|
func toggleSelectionMode() {
|
|
guard selectedTab == .all else { return }
|
|
isSelectionMode.toggle()
|
|
if !isSelectionMode {
|
|
selectedMaterialIds = []
|
|
}
|
|
notifyStateChange()
|
|
}
|
|
|
|
/// 切换素材选中状态。
|
|
func toggleMaterialSelection(_ material: TravelAlbumMaterial) {
|
|
guard isSelectionMode else { return }
|
|
guard material.status == 1 else {
|
|
onShowMessage?("仅未购买素材可删除")
|
|
return
|
|
}
|
|
if selectedMaterialIds.contains(material.id) {
|
|
selectedMaterialIds.remove(material.id)
|
|
} else {
|
|
selectedMaterialIds.insert(material.id)
|
|
}
|
|
notifyStateChange()
|
|
}
|
|
|
|
/// AI 修图任务提交成功后退出选择模式并清空当前选择。
|
|
func completeAIRetouchSubmission() {
|
|
isSelectionMode = false
|
|
selectedMaterialIds = []
|
|
notifyStateChange()
|
|
}
|
|
|
|
/// 预览页删除成功后移除对应素材,并同步全部/已购计数。
|
|
func removeMaterialAfterPreviewDeletion(id: Int) {
|
|
guard let index = materials.firstIndex(where: { $0.id == id }) else { return }
|
|
let material = materials.remove(at: index)
|
|
selectedMaterialIds.remove(id)
|
|
allPhotoCount = max(0, allPhotoCount - 1)
|
|
if material.isPurchased {
|
|
purchasedPhotoCount = max(0, purchasedPhotoCount - 1)
|
|
}
|
|
notifyStateChange()
|
|
}
|
|
|
|
/// 删除已选素材。
|
|
func deleteSelectedMaterials(api: any TravelAlbumServing) async {
|
|
let ids = Array(selectedMaterialIds)
|
|
guard !ids.isEmpty else {
|
|
onShowMessage?("请选择要删除的素材")
|
|
return
|
|
}
|
|
do {
|
|
for id in ids {
|
|
try await api.deleteMaterial(id: id)
|
|
}
|
|
onShowMessage?("删除成功")
|
|
isSelectionMode = false
|
|
selectedMaterialIds = []
|
|
await refreshAll(api: api)
|
|
} catch is CancellationError {
|
|
return
|
|
} catch {
|
|
onShowMessage?(error.localizedDescription.isEmpty ? "删除失败" : error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
/// 删除当前相册。
|
|
func deleteAlbum(api: any TravelAlbumServing) async {
|
|
do {
|
|
try await api.deleteAlbum(id: albumId)
|
|
onShowMessage?("相册已删除")
|
|
onDeletedAlbum?(albumId)
|
|
} catch is CancellationError {
|
|
return
|
|
} catch {
|
|
onShowMessage?(error.localizedDescription.isEmpty ? "删除失败" : error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
private func notifyStateChange() {
|
|
onStateChange?()
|
|
}
|
|
}
|