feat: 完善相册 AI 修图模板选择流程
This commit is contained in:
@@ -29,6 +29,9 @@ protocol TravelAlbumServing {
|
||||
isPurchased: Int?
|
||||
) async throws -> TravelAlbumListResponse<TravelAlbumMaterial>
|
||||
|
||||
/// 拉取单个相册素材及其最新关联图片信息。
|
||||
func materialInfo(userEquityTravelId: Int, materialId: Int) async throws -> TravelAlbumMaterial
|
||||
|
||||
/// 上传并登记旅拍相册素材。
|
||||
func uploadMaterial(_ request: TravelAlbumUploadMaterialRequest) async throws -> TravelAlbumMaterial
|
||||
|
||||
@@ -122,6 +125,20 @@ final class TravelAlbumAPI: TravelAlbumServing {
|
||||
)
|
||||
}
|
||||
|
||||
/// 拉取单个相册素材及其最新关联图片信息。
|
||||
func materialInfo(userEquityTravelId: Int, materialId: Int) async throws -> TravelAlbumMaterial {
|
||||
try await client.send(
|
||||
APIRequest(
|
||||
method: .get,
|
||||
path: "\(basePath)/material-info",
|
||||
queryItems: [
|
||||
URLQueryItem(name: "user_equity_travel_id", value: String(userEquityTravelId)),
|
||||
URLQueryItem(name: "material_id", value: String(materialId)),
|
||||
]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// 上传并登记旅拍相册素材。
|
||||
func uploadMaterial(_ request: TravelAlbumUploadMaterialRequest) async throws -> TravelAlbumMaterial {
|
||||
try await client.send(APIRequest(method: .post, path: "\(basePath)/upload-material", body: request))
|
||||
|
||||
@@ -428,22 +428,35 @@ struct TravelAlbumAIRetouchTemplatesResponse: Decodable, Sendable, Equatable {
|
||||
let refinedTemplates: [TravelAlbumAIRetouchTemplate]
|
||||
let atmosphereTemplates: [TravelAlbumAIRetouchTemplate]
|
||||
let coverTemplates: [TravelAlbumAIRetouchTemplate]
|
||||
let remainingQuota: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case refinedTemplates = "refined_templates"
|
||||
case atmosphereTemplates = "atmosphere_templates"
|
||||
case coverTemplates = "cover_templates"
|
||||
case remainingQuota = "remaining_quota"
|
||||
}
|
||||
|
||||
/// 创建分组模板响应,默认各组为空。
|
||||
init(
|
||||
refinedTemplates: [TravelAlbumAIRetouchTemplate] = [],
|
||||
atmosphereTemplates: [TravelAlbumAIRetouchTemplate] = [],
|
||||
coverTemplates: [TravelAlbumAIRetouchTemplate] = []
|
||||
coverTemplates: [TravelAlbumAIRetouchTemplate] = [],
|
||||
remainingQuota: Int = 0
|
||||
) {
|
||||
self.refinedTemplates = refinedTemplates
|
||||
self.atmosphereTemplates = atmosphereTemplates
|
||||
self.coverTemplates = coverTemplates
|
||||
self.remainingQuota = max(0, remainingQuota)
|
||||
}
|
||||
|
||||
/// 解码模板和剩余额度;旧响应缺少额度时按零处理,避免误提交付费任务。
|
||||
init(from decoder: any Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
refinedTemplates = try container.decode([TravelAlbumAIRetouchTemplate].self, forKey: .refinedTemplates)
|
||||
atmosphereTemplates = try container.decode([TravelAlbumAIRetouchTemplate].self, forKey: .atmosphereTemplates)
|
||||
coverTemplates = try container.decode([TravelAlbumAIRetouchTemplate].self, forKey: .coverTemplates)
|
||||
remainingQuota = max(0, try container.decodeIfPresent(Int.self, forKey: .remainingQuota) ?? 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ final class TravelAlbumAIRetouchTemplateViewModel {
|
||||
private(set) var selectedRefinedTemplateId: Int?
|
||||
private(set) var selectedAtmosphereTemplateId: Int?
|
||||
private(set) var selectedCoverTemplateId: Int?
|
||||
private(set) var remainingQuota: Int?
|
||||
private(set) var isLoading = false
|
||||
private(set) var isSubmitting = false
|
||||
private(set) var loadErrorMessage: String?
|
||||
@@ -56,6 +57,22 @@ final class TravelAlbumAIRetouchTemplateViewModel {
|
||||
workflow.isOptional(category)
|
||||
}
|
||||
|
||||
/// 当前选择预计消耗的修图次数;封面模板免费,不计入额度。
|
||||
var requiredQuota: Int {
|
||||
switch workflow {
|
||||
case .initial(_, let materialIds):
|
||||
let outputCount = 1 + (selectedAtmosphereTemplateId == nil ? 0 : 1)
|
||||
return materialIds.count * outputCount
|
||||
case .reretouch(_, _, let type):
|
||||
switch type {
|
||||
case .refined, .atmosphere:
|
||||
return 1
|
||||
case .all:
|
||||
return 1 + (selectedAtmosphereTemplateId == nil ? 0 : 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 当前必选模板或业务参数缺失时用于底部提示的文案。
|
||||
var validationMessage: String? {
|
||||
guard !isLoading, loadErrorMessage == nil else { return nil }
|
||||
@@ -70,6 +87,12 @@ final class TravelAlbumAIRetouchTemplateViewModel {
|
||||
return unavailableMessage(for: category)
|
||||
}
|
||||
}
|
||||
guard let remainingQuota else {
|
||||
return "剩余修图次数获取失败,请刷新后重试"
|
||||
}
|
||||
if requiredQuota > remainingQuota {
|
||||
return "剩余修图次数不足,需要\(requiredQuota)次,当前剩余\(remainingQuota)次"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -92,6 +115,7 @@ final class TravelAlbumAIRetouchTemplateViewModel {
|
||||
guard !isLoading else { return }
|
||||
isLoading = true
|
||||
loadErrorMessage = nil
|
||||
remainingQuota = nil
|
||||
notifyStateChange()
|
||||
|
||||
do {
|
||||
@@ -99,6 +123,7 @@ final class TravelAlbumAIRetouchTemplateViewModel {
|
||||
refinedTemplates = response.refinedTemplates
|
||||
atmosphereTemplates = response.atmosphereTemplates
|
||||
coverTemplates = response.coverTemplates
|
||||
remainingQuota = response.remainingQuota
|
||||
selectedRefinedTemplateId = visibleCategories.contains(.refined) ? refinedTemplates.first?.id : nil
|
||||
selectedAtmosphereTemplateId = visibleCategories.contains(.atmosphere) && !isOptional(.atmosphere)
|
||||
? atmosphereTemplates.first?.id
|
||||
@@ -116,6 +141,7 @@ final class TravelAlbumAIRetouchTemplateViewModel {
|
||||
selectedRefinedTemplateId = nil
|
||||
selectedAtmosphereTemplateId = nil
|
||||
selectedCoverTemplateId = nil
|
||||
remainingQuota = nil
|
||||
isLoading = false
|
||||
loadErrorMessage = error.localizedDescription.isEmpty ? "模板加载失败" : error.localizedDescription
|
||||
notifyStateChange()
|
||||
|
||||
@@ -207,41 +207,18 @@ final class TravelAlbumDetailViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// 重新拉取当前筛选、排序下已经加载的分页范围,供全屏预览刷新关联图片。
|
||||
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 }
|
||||
/// 定向刷新单个素材,并同步替换网格列表中的对应缓存。
|
||||
func refreshMaterial(id: Int, api: any TravelAlbumServing) async throws -> TravelAlbumMaterial {
|
||||
let material = try await api.materialInfo(
|
||||
userEquityTravelId: albumId,
|
||||
materialId: id
|
||||
)
|
||||
guard material.id == id else { throw APIError.invalidResponse }
|
||||
if let index = materials.firstIndex(where: { $0.id == id }) {
|
||||
materials[index] = material
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
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)
|
||||
return material
|
||||
}
|
||||
|
||||
/// 切换选择模式。
|
||||
|
||||
@@ -12,13 +12,11 @@ final class TravelAlbumAIRetouchTemplateViewController: BaseViewController {
|
||||
/// 页面使用的 collection section。
|
||||
private enum Section: Hashable {
|
||||
case templates(TravelAlbumAIRetouchTemplateCategory)
|
||||
case mode
|
||||
}
|
||||
|
||||
/// 页面使用的 diffable item。
|
||||
private enum Item: Hashable {
|
||||
case template(TravelAlbumAIRetouchTemplateCategory, TravelAlbumAIRetouchTemplate)
|
||||
case mode
|
||||
}
|
||||
|
||||
private let viewModel: TravelAlbumAIRetouchTemplateViewModel
|
||||
@@ -35,6 +33,7 @@ final class TravelAlbumAIRetouchTemplateViewController: BaseViewController {
|
||||
private let bottomBar = UIView()
|
||||
private let bottomDivider = UIView()
|
||||
private let footerStack = UIStackView()
|
||||
private let quotaLabel = UILabel()
|
||||
private let validationLabel = UILabel()
|
||||
private let actionStack = UIStackView()
|
||||
private let cancelButton = UIButton(type: .system)
|
||||
@@ -78,10 +77,6 @@ final class TravelAlbumAIRetouchTemplateViewController: BaseViewController {
|
||||
TravelAlbumAIRetouchTemplateCell.self,
|
||||
forCellWithReuseIdentifier: TravelAlbumAIRetouchTemplateCell.reuseIdentifier
|
||||
)
|
||||
collectionView.register(
|
||||
TravelAlbumAIRetouchModeCell.self,
|
||||
forCellWithReuseIdentifier: TravelAlbumAIRetouchModeCell.reuseIdentifier
|
||||
)
|
||||
collectionView.register(
|
||||
TravelAlbumAIRetouchSectionHeader.self,
|
||||
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
|
||||
@@ -109,6 +104,10 @@ final class TravelAlbumAIRetouchTemplateViewController: BaseViewController {
|
||||
footerStack.axis = .vertical
|
||||
footerStack.spacing = 8
|
||||
footerStack.alignment = .fill
|
||||
quotaLabel.font = .monospacedDigitSystemFont(ofSize: 14, weight: .medium)
|
||||
quotaLabel.textColor = AIRetouchTemplateStyle.primary
|
||||
quotaLabel.textAlignment = .center
|
||||
quotaLabel.accessibilityIdentifier = "travelAlbum.aiRetouchRemainingQuotaLabel"
|
||||
actionStack.axis = .horizontal
|
||||
actionStack.spacing = 12
|
||||
actionStack.distribution = .fillEqually
|
||||
@@ -117,6 +116,7 @@ final class TravelAlbumAIRetouchTemplateViewController: BaseViewController {
|
||||
configureConfirmButton()
|
||||
actionStack.addArrangedSubview(cancelButton)
|
||||
actionStack.addArrangedSubview(confirmButton)
|
||||
footerStack.addArrangedSubview(quotaLabel)
|
||||
footerStack.addArrangedSubview(validationLabel)
|
||||
footerStack.addArrangedSubview(actionStack)
|
||||
|
||||
@@ -255,13 +255,6 @@ final class TravelAlbumAIRetouchTemplateViewController: BaseViewController {
|
||||
selected: self.viewModel.selectedTemplateId(for: category) == template.id
|
||||
)
|
||||
return cell
|
||||
case .mode:
|
||||
let cell = collectionView.dequeueReusableCell(
|
||||
withReuseIdentifier: TravelAlbumAIRetouchModeCell.reuseIdentifier,
|
||||
for: indexPath
|
||||
) as! TravelAlbumAIRetouchModeCell
|
||||
cell.apply()
|
||||
return cell
|
||||
}
|
||||
}
|
||||
dataSource.supplementaryViewProvider = { [weak self] collectionView, kind, indexPath in
|
||||
@@ -288,40 +281,27 @@ final class TravelAlbumAIRetouchTemplateViewController: BaseViewController {
|
||||
self.dataSource.snapshot().sectionIdentifiers.indices.contains(sectionIndex) else {
|
||||
return nil
|
||||
}
|
||||
switch self.dataSource.snapshot().sectionIdentifiers[sectionIndex] {
|
||||
case .templates:
|
||||
let itemSize = NSCollectionLayoutSize(
|
||||
widthDimension: .absolute(118),
|
||||
heightDimension: .absolute(154)
|
||||
)
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize, subitems: [item])
|
||||
let section = NSCollectionLayoutSection(group: group)
|
||||
section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
|
||||
section.interGroupSpacing = 12
|
||||
section.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 18, bottom: 12, trailing: 18)
|
||||
section.boundarySupplementaryItems = [
|
||||
NSCollectionLayoutBoundarySupplementaryItem(
|
||||
layoutSize: NSCollectionLayoutSize(
|
||||
widthDimension: .fractionalWidth(1),
|
||||
heightDimension: .absolute(40)
|
||||
),
|
||||
elementKind: UICollectionView.elementKindSectionHeader,
|
||||
alignment: .top
|
||||
let itemSize = NSCollectionLayoutSize(
|
||||
widthDimension: .absolute(118),
|
||||
heightDimension: .absolute(154)
|
||||
)
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize, subitems: [item])
|
||||
let section = NSCollectionLayoutSection(group: group)
|
||||
section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
|
||||
section.interGroupSpacing = 12
|
||||
section.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 18, bottom: 12, trailing: 18)
|
||||
section.boundarySupplementaryItems = [
|
||||
NSCollectionLayoutBoundarySupplementaryItem(
|
||||
layoutSize: NSCollectionLayoutSize(
|
||||
widthDimension: .fractionalWidth(1),
|
||||
heightDimension: .absolute(40)
|
||||
),
|
||||
]
|
||||
return section
|
||||
case .mode:
|
||||
let itemSize = NSCollectionLayoutSize(
|
||||
widthDimension: .fractionalWidth(1),
|
||||
heightDimension: .absolute(72)
|
||||
)
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize, subitems: [item])
|
||||
let section = NSCollectionLayoutSection(group: group)
|
||||
section.contentInsets = NSDirectionalEdgeInsets(top: 12, leading: 18, bottom: 22, trailing: 18)
|
||||
return section
|
||||
}
|
||||
elementKind: UICollectionView.elementKindSectionHeader,
|
||||
alignment: .top
|
||||
),
|
||||
]
|
||||
return section
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,6 +326,17 @@ final class TravelAlbumAIRetouchTemplateViewController: BaseViewController {
|
||||
|
||||
validationLabel.text = viewModel.validationMessage
|
||||
validationLabel.isHidden = viewModel.validationMessage == nil
|
||||
if let remainingQuota = viewModel.remainingQuota {
|
||||
quotaLabel.text = "剩余 \(remainingQuota) 次"
|
||||
quotaLabel.accessibilityLabel = "剩余\(remainingQuota)次修图额度"
|
||||
quotaLabel.textColor = viewModel.requiredQuota > remainingQuota
|
||||
? AIRetouchTemplateStyle.danger
|
||||
: AIRetouchTemplateStyle.primary
|
||||
} else {
|
||||
quotaLabel.text = "剩余 -- 次"
|
||||
quotaLabel.accessibilityLabel = "剩余修图额度暂不可用"
|
||||
quotaLabel.textColor = AIRetouchTemplateStyle.textSecondary
|
||||
}
|
||||
cancelButton.isEnabled = !viewModel.isSubmitting
|
||||
confirmButton.isEnabled = viewModel.canSubmit
|
||||
confirmButton.alpha = viewModel.canSubmit ? 1 : 0.45
|
||||
@@ -362,8 +353,6 @@ final class TravelAlbumAIRetouchTemplateViewController: BaseViewController {
|
||||
for category in viewModel.visibleCategories {
|
||||
appendTemplates(category, to: &snapshot)
|
||||
}
|
||||
snapshot.appendSections([.mode])
|
||||
snapshot.appendItems([.mode], toSection: .mode)
|
||||
snapshot.reconfigureItems(snapshot.itemIdentifiers)
|
||||
dataSource.apply(snapshot, animatingDifferences: true)
|
||||
}
|
||||
@@ -539,53 +528,6 @@ final class TravelAlbumAIRetouchSectionHeader: UICollectionReusableView {
|
||||
}
|
||||
}
|
||||
|
||||
/// AI 修图模式卡片,展示计费方式与剩余张数占位。
|
||||
final class TravelAlbumAIRetouchModeCell: UICollectionViewCell {
|
||||
static let reuseIdentifier = "TravelAlbumAIRetouchModeCell"
|
||||
|
||||
private let titleLabel = UILabel()
|
||||
private let remainingLabel = UILabel()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
contentView.backgroundColor = .white
|
||||
contentView.layer.cornerRadius = 14
|
||||
contentView.layer.borderWidth = 1
|
||||
contentView.layer.borderColor = AIRetouchTemplateStyle.border.cgColor
|
||||
|
||||
titleLabel.font = .systemFont(ofSize: 15, weight: .semibold)
|
||||
titleLabel.textColor = AIRetouchTemplateStyle.textPrimary
|
||||
remainingLabel.font = .monospacedDigitSystemFont(ofSize: 14, weight: .medium)
|
||||
remainingLabel.textColor = AIRetouchTemplateStyle.primary
|
||||
remainingLabel.textAlignment = .right
|
||||
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(remainingLabel)
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(16)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
remainingLabel.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-16)
|
||||
make.centerY.equalToSuperview()
|
||||
make.leading.greaterThanOrEqualTo(titleLabel.snp.trailing).offset(12)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
/// 应用当前固定修图模式与额度占位文案。
|
||||
func apply() {
|
||||
titleLabel.text = "AI精修 按张收费"
|
||||
remainingLabel.text = "剩余--张"
|
||||
accessibilityIdentifier = "travelAlbum.aiRetouchModeCell"
|
||||
accessibilityLabel = "AI精修,按张收费,剩余张数暂不可用"
|
||||
}
|
||||
}
|
||||
|
||||
/// AI 修图模板页视觉常量。
|
||||
private enum AIRetouchTemplateStyle {
|
||||
static let primary = UIColor(hex: 0x1677FF)
|
||||
|
||||
@@ -499,12 +499,9 @@ final class TravelAlbumDetailViewController: BaseViewController {
|
||||
previewViewModel.currentPhotoCount
|
||||
)
|
||||
},
|
||||
reload: {
|
||||
let response = try await previewViewModel.reloadLoadedMaterials(api: previewAPI)
|
||||
return (
|
||||
response.list.map(TravelAlbumPreviewProject.init(material:)),
|
||||
response.total
|
||||
)
|
||||
reload: { materialId in
|
||||
let material = try await previewViewModel.refreshMaterial(id: materialId, api: previewAPI)
|
||||
return TravelAlbumPreviewProject(material: material)
|
||||
},
|
||||
onProjectDeleted: { materialId in
|
||||
previewViewModel.removeMaterialAfterPreviewDeletion(id: materialId)
|
||||
|
||||
@@ -10,8 +10,8 @@ import UIKit
|
||||
/// 相册项目续页回调,返回当前筛选和排序下的完整已加载项目及总数。
|
||||
typealias TravelAlbumPreviewLoadMore = () async -> (projects: [TravelAlbumPreviewProject], totalCount: Int)
|
||||
|
||||
/// 相册项目刷新回调,失败时由预览页保留当前内容并展示错误。
|
||||
typealias TravelAlbumPreviewReload = () async throws -> (projects: [TravelAlbumPreviewProject], totalCount: Int)
|
||||
/// 相册项目定向刷新回调,根据原素材 ID 返回该项目的最新数据。
|
||||
typealias TravelAlbumPreviewReload = (_ materialId: Int) async throws -> TravelAlbumPreviewProject
|
||||
|
||||
/// 旅拍相册全屏图片预览页,支持项目分页、关联图 Tab、缩放和沉浸式工具栏。
|
||||
final class TravelAlbumPhotoPreviewViewController: UIViewController {
|
||||
@@ -481,7 +481,7 @@ final class TravelAlbumPhotoPreviewViewController: UIViewController {
|
||||
onSubmitted: { [weak self] in
|
||||
guard let self else { return }
|
||||
self.showPreviewToast("AI修图任务已提交")
|
||||
self.reloadProjects(showSuccessToast: false, forceRefreshImage: false)
|
||||
self.reloadProjects(showGlobalLoading: false, forceRefreshImage: false)
|
||||
}
|
||||
)
|
||||
present(controller, animated: true)
|
||||
@@ -578,32 +578,36 @@ final class TravelAlbumPhotoPreviewViewController: UIViewController {
|
||||
}
|
||||
|
||||
@objc private func refreshTapped() {
|
||||
reloadProjects(showSuccessToast: true, forceRefreshImage: true)
|
||||
reloadProjects(showGlobalLoading: true, forceRefreshImage: true)
|
||||
}
|
||||
|
||||
private func reloadProjects(showSuccessToast: Bool, forceRefreshImage: Bool) {
|
||||
private func reloadProjects(showGlobalLoading: Bool, forceRefreshImage: Bool) {
|
||||
guard !isRefreshingProject else { return }
|
||||
guard let reload else {
|
||||
if showSuccessToast { showPreviewToast("关联图片刷新接口待接入") }
|
||||
return
|
||||
}
|
||||
guard let reload else { return }
|
||||
isRefreshingProject = true
|
||||
updateRefreshButton()
|
||||
if showGlobalLoading { GlobalLoadingManager.shared.show() }
|
||||
let currentProjectId = currentProject?.id
|
||||
let currentProjectIndex = currentNode?.projectIndex ?? 0
|
||||
let kind = currentAsset?.kind ?? .original
|
||||
|
||||
Task { [weak self] in
|
||||
defer {
|
||||
if showGlobalLoading { GlobalLoadingManager.shared.hide() }
|
||||
}
|
||||
guard let self else { return }
|
||||
do {
|
||||
let result = try await reload()
|
||||
guard let currentProjectId else {
|
||||
self.isRefreshingProject = false
|
||||
self.updateRefreshButton()
|
||||
return
|
||||
}
|
||||
let project = try await reload(currentProjectId)
|
||||
self.applySuccessfulReload(
|
||||
projects: result.projects,
|
||||
totalCount: result.totalCount,
|
||||
project: project,
|
||||
currentProjectId: currentProjectId,
|
||||
fallbackProjectIndex: currentProjectIndex,
|
||||
kind: kind,
|
||||
showSuccessToast: showSuccessToast,
|
||||
forceRefreshImage: forceRefreshImage
|
||||
)
|
||||
} catch is CancellationError {
|
||||
@@ -612,37 +616,28 @@ final class TravelAlbumPhotoPreviewViewController: UIViewController {
|
||||
} catch {
|
||||
self.isRefreshingProject = false
|
||||
self.updateRefreshButton()
|
||||
if showSuccessToast {
|
||||
let message = error.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
self.showPreviewToast(message.isEmpty ? "刷新失败" : message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func applySuccessfulReload(
|
||||
projects refreshedProjects: [TravelAlbumPreviewProject],
|
||||
totalCount: Int,
|
||||
currentProjectId: Int?,
|
||||
project refreshedProject: TravelAlbumPreviewProject,
|
||||
currentProjectId: Int,
|
||||
fallbackProjectIndex: Int,
|
||||
kind: TravelAlbumPreviewAssetKind,
|
||||
showSuccessToast: Bool,
|
||||
forceRefreshImage: Bool
|
||||
) {
|
||||
let incoming = Self.deduplicated(refreshedProjects)
|
||||
guard !incoming.isEmpty else {
|
||||
guard !projects.isEmpty else {
|
||||
isRefreshingProject = false
|
||||
updateRefreshButton()
|
||||
if showSuccessToast { dismiss(animated: true) }
|
||||
dismiss(animated: true)
|
||||
return
|
||||
}
|
||||
|
||||
projects = incoming
|
||||
self.totalCount = max(totalCount, incoming.count)
|
||||
let targetProjectIndex = currentProjectId.flatMap { id in
|
||||
incoming.firstIndex { $0.id == id }
|
||||
} ?? min(max(0, fallbackProjectIndex), incoming.count - 1)
|
||||
let resolvedKind = incoming[targetProjectIndex].asset(for: kind) == nil ? .original : kind
|
||||
let targetProjectIndex = projects.firstIndex { $0.id == currentProjectId }
|
||||
?? min(max(0, fallbackProjectIndex), projects.count - 1)
|
||||
projects[targetProjectIndex] = refreshedProject
|
||||
let resolvedKind = refreshedProject.asset(for: kind) == nil ? .original : kind
|
||||
selectedKind = resolvedKind
|
||||
rebuildNodes(keepingProjectIndex: targetProjectIndex, kind: resolvedKind)
|
||||
collectionView.reloadData()
|
||||
@@ -652,7 +647,6 @@ final class TravelAlbumPhotoPreviewViewController: UIViewController {
|
||||
if forceRefreshImage { forceRefreshCurrentImage() }
|
||||
isRefreshingProject = false
|
||||
updateRefreshButton()
|
||||
if showSuccessToast { showPreviewToast("刷新成功") }
|
||||
}
|
||||
|
||||
private func forceRefreshCurrentImage() {
|
||||
|
||||
Reference in New Issue
Block a user