Compare commits
2
Commits
8e444f7434
...
b127495e52
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b127495e52 | ||
|
|
76aa7bb4aa |
@@ -386,10 +386,17 @@ enum TravelAlbumAIRetouchWorkflow: Sendable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
/// 当前分组是否允许不选择;仅首次修图的氛围感模板选填。
|
||||
/// 当前分组是否允许不选择;首次修图和原图 Tab 覆盖重修的氛围感模板选填。
|
||||
func isOptional(_ category: TravelAlbumAIRetouchTemplateCategory) -> Bool {
|
||||
if case .initial = self, category == .atmosphere { return true }
|
||||
return false
|
||||
guard category == .atmosphere else { return false }
|
||||
switch self {
|
||||
case .initial:
|
||||
return true
|
||||
case .reretouch(_, _, .all):
|
||||
return true
|
||||
case .reretouch:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// 工作流目标是否满足接口的最小参数要求。
|
||||
|
||||
+5
-2
@@ -66,9 +66,12 @@ final class TravelAlbumAIRetouchTemplateViewModel {
|
||||
return false
|
||||
}
|
||||
|
||||
/// 首次修图页固定展示的氛围感与封面生成规则说明。
|
||||
/// 首次修图页按照当前选图数量展示氛围感与封面生成规则。
|
||||
var initialTipsText: String {
|
||||
"Tips:氛围感修图为选填,可横向选择一种样式;选中后每张照片会额外生成1个独立结果,第一张照片仍另生成封面。"
|
||||
if visibleCategories.contains(.cover) {
|
||||
return "Tips:氛围感修图为选填;封面风格为必选,将使用第一张照片另生成封面。"
|
||||
}
|
||||
return "Tips:氛围感修图为选填,选中后每张照片会额外生成1个独立结果。"
|
||||
}
|
||||
|
||||
/// 当前分组是否为选填。
|
||||
|
||||
@@ -52,7 +52,10 @@ final class TravelAlbumEntryViewModel {
|
||||
}
|
||||
|
||||
/// 重新拉取相册列表。
|
||||
func loadAlbums(api: any TravelAlbumServing) async {
|
||||
func loadAlbums(
|
||||
api: any TravelAlbumServing,
|
||||
preservingContentOnFailure: Bool = false
|
||||
) async {
|
||||
isLoading = true
|
||||
notifyStateChange()
|
||||
defer {
|
||||
@@ -67,12 +70,20 @@ final class TravelAlbumEntryViewModel {
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
albums = []
|
||||
albumTotal = 0
|
||||
if !preservingContentOnFailure {
|
||||
albums = []
|
||||
albumTotal = 0
|
||||
}
|
||||
onShowMessage?(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
/// 用户下拉刷新相册列表;失败时保留当前内容,避免页面瞬间清空。
|
||||
func refreshAlbums(api: any TravelAlbumServing) async {
|
||||
guard !isLoading else { return }
|
||||
await loadAlbums(api: api, preservingContentOnFailure: true)
|
||||
}
|
||||
|
||||
/// 打开创建相册弹窗。
|
||||
func openCreateSheet(api: any TravelAlbumServing) async {
|
||||
guard !isCreating else { return }
|
||||
|
||||
@@ -284,8 +284,8 @@ final class TravelAlbumAIRetouchTemplateViewController: BaseViewController {
|
||||
header.apply(
|
||||
title: category.title,
|
||||
badge: category == .cover
|
||||
? .gift
|
||||
: (self.viewModel.isOptional(category) ? .optional : nil)
|
||||
? .requiredGift
|
||||
: (self.viewModel.isOptional(category) ? .optional : .required)
|
||||
)
|
||||
case .mode:
|
||||
return nil
|
||||
@@ -845,11 +845,12 @@ final class TravelAlbumAIRetouchModeCell: UICollectionViewCell {
|
||||
|
||||
/// 模板分组标题右侧的业务标记。
|
||||
fileprivate enum AIRetouchTemplateSectionBadge {
|
||||
case required
|
||||
case optional
|
||||
case gift
|
||||
case requiredGift
|
||||
}
|
||||
|
||||
/// AI 修图模板分组标题,可附带选填或赠送标记。
|
||||
/// AI 修图模板分组标题,展示必选、选填或封面赠送规则。
|
||||
final class TravelAlbumAIRetouchSectionHeader: UICollectionReusableView {
|
||||
static let reuseIdentifier = "TravelAlbumAIRetouchSectionHeader"
|
||||
|
||||
@@ -896,12 +897,16 @@ final class TravelAlbumAIRetouchSectionHeader: UICollectionReusableView {
|
||||
titleLabel.text = title
|
||||
badgeContainer.isHidden = badge == nil
|
||||
switch badge {
|
||||
case .required:
|
||||
badgeLabel.text = "必选"
|
||||
badgeLabel.textColor = AIRetouchTemplateStyle.danger
|
||||
badgeContainer.backgroundColor = AIRetouchTemplateStyle.danger.withAlphaComponent(0.1)
|
||||
case .optional:
|
||||
badgeLabel.text = "选填"
|
||||
badgeLabel.textColor = AIRetouchTemplateStyle.primary
|
||||
badgeContainer.backgroundColor = AIRetouchTemplateStyle.primary.withAlphaComponent(0.1)
|
||||
case .gift:
|
||||
badgeLabel.text = "赠送 · 不占额度"
|
||||
case .requiredGift:
|
||||
badgeLabel.text = "必选 · 赠送 · 不占额度"
|
||||
badgeLabel.textColor = AIRetouchTemplateStyle.gift
|
||||
badgeContainer.backgroundColor = AIRetouchTemplateStyle.giftBackground
|
||||
case nil:
|
||||
|
||||
@@ -36,6 +36,7 @@ final class TravelAlbumEntryViewController: BaseViewController {
|
||||
private let heroCard = TravelAlbumHeroCard()
|
||||
private let titleLabel = UILabel()
|
||||
private let tableView = UITableView(frame: .zero, style: .plain)
|
||||
private let refreshControl = UIRefreshControl()
|
||||
private let emptyView = TravelAlbumEmptyView()
|
||||
private var dataSource: UITableViewDiffableDataSource<Int, TravelAlbum>!
|
||||
private var refreshState = TravelAlbumReturnRefreshState()
|
||||
@@ -65,6 +66,8 @@ final class TravelAlbumEntryViewController: BaseViewController {
|
||||
tableView.separatorStyle = .none
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.estimatedRowHeight = 148
|
||||
tableView.alwaysBounceVertical = true
|
||||
tableView.refreshControl = refreshControl
|
||||
tableView.delegate = self
|
||||
tableView.register(TravelAlbumTaskCell.self, forCellReuseIdentifier: TravelAlbumTaskCell.reuseIdentifier)
|
||||
|
||||
@@ -84,6 +87,7 @@ final class TravelAlbumEntryViewController: BaseViewController {
|
||||
view.addSubview(titleLabel)
|
||||
view.addSubview(tableView)
|
||||
view.addSubview(emptyView)
|
||||
emptyView.isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
override func setupConstraints() {
|
||||
@@ -108,6 +112,7 @@ final class TravelAlbumEntryViewController: BaseViewController {
|
||||
|
||||
override func bindActions() {
|
||||
heroCard.addTarget(self, action: #selector(createTapped), for: .touchUpInside)
|
||||
refreshControl.addTarget(self, action: #selector(refreshPulled), for: .valueChanged)
|
||||
viewModel.onStateChange = { [weak self] in
|
||||
Task { @MainActor in self?.applyViewModel() }
|
||||
}
|
||||
@@ -138,8 +143,8 @@ final class TravelAlbumEntryViewController: BaseViewController {
|
||||
heroCard.isLoading = viewModel.isCreating
|
||||
titleLabel.text = "我的任务(\(viewModel.albumTotal))"
|
||||
titleLabel.isHidden = viewModel.albums.isEmpty
|
||||
tableView.isHidden = viewModel.albums.isEmpty
|
||||
emptyView.isHidden = !viewModel.albums.isEmpty || viewModel.isLoading
|
||||
emptyView.isHidden = !viewModel.albums.isEmpty
|
||||
|| (viewModel.isLoading && !refreshControl.isRefreshing)
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Int, TravelAlbum>()
|
||||
snapshot.appendSections([0])
|
||||
snapshot.appendItems(viewModel.albums)
|
||||
@@ -147,13 +152,23 @@ final class TravelAlbumEntryViewController: BaseViewController {
|
||||
snapshot,
|
||||
animatingDifferences: !refreshState.suppressesSnapshotAnimations
|
||||
)
|
||||
if viewModel.isLoading && viewModel.albums.isEmpty {
|
||||
if viewModel.isLoading && viewModel.albums.isEmpty && !refreshControl.isRefreshing {
|
||||
showLoading()
|
||||
} else {
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func refreshPulled() {
|
||||
Task {
|
||||
await viewModel.refreshAlbums(api: api)
|
||||
await MainActor.run {
|
||||
self.refreshControl.endRefreshing()
|
||||
self.applyViewModel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func createTapped() {
|
||||
Task {
|
||||
await viewModel.openCreateSheet(api: api)
|
||||
|
||||
@@ -25,7 +25,7 @@ final class TravelAlbumAIRetouchTemplateViewModelTests: XCTestCase {
|
||||
XCTAssertTrue(viewModel.shouldShowInitialTips)
|
||||
XCTAssertEqual(
|
||||
viewModel.initialTipsText,
|
||||
"Tips:氛围感修图为选填,可横向选择一种样式;选中后每张照片会额外生成1个独立结果,第一张照片仍另生成封面。"
|
||||
"Tips:氛围感修图为选填;封面风格为必选,将使用第一张照片另生成封面。"
|
||||
)
|
||||
XCTAssertEqual(viewModel.visibleCategories, [.refined, .atmosphere, .cover])
|
||||
XCTAssertEqual(viewModel.selectedRefinedTemplateId, 11)
|
||||
@@ -44,6 +44,14 @@ final class TravelAlbumAIRetouchTemplateViewModelTests: XCTestCase {
|
||||
)
|
||||
await viewModel.loadTemplates(api: api)
|
||||
|
||||
XCTAssertEqual(
|
||||
viewModel.initialTipsText,
|
||||
"Tips:氛围感修图为选填,选中后每张照片会额外生成1个独立结果。"
|
||||
)
|
||||
|
||||
viewModel.toggleTemplate(id: 11, category: .refined)
|
||||
XCTAssertEqual(viewModel.selectedRefinedTemplateId, 11)
|
||||
|
||||
viewModel.toggleTemplate(id: 21, category: .atmosphere)
|
||||
XCTAssertEqual(viewModel.selectedAtmosphereTemplateId, 21)
|
||||
|
||||
@@ -126,7 +134,10 @@ final class TravelAlbumAIRetouchTemplateViewModelTests: XCTestCase {
|
||||
await all.loadTemplates(api: api)
|
||||
XCTAssertEqual(all.visibleCategories, [.refined, .atmosphere])
|
||||
XCTAssertEqual(all.selectedRefinedTemplateId, 11)
|
||||
XCTAssertEqual(all.selectedAtmosphereTemplateId, 21)
|
||||
XCTAssertNil(all.selectedAtmosphereTemplateId)
|
||||
XCTAssertTrue(all.isOptional(.atmosphere))
|
||||
XCTAssertEqual(all.requiredQuota, 1)
|
||||
XCTAssertTrue(all.canSubmit)
|
||||
await all.submit(api: api)
|
||||
|
||||
XCTAssertEqual(api.aiReretouchRequests, [
|
||||
@@ -149,7 +160,7 @@ final class TravelAlbumAIRetouchTemplateViewModelTests: XCTestCase {
|
||||
aiRetouchBatchId: 90,
|
||||
type: .all,
|
||||
refinedTemplateId: 11,
|
||||
atmosphereTemplateId: 21
|
||||
atmosphereTemplateId: nil
|
||||
),
|
||||
])
|
||||
}
|
||||
@@ -172,12 +183,13 @@ final class TravelAlbumAIRetouchTemplateViewModelTests: XCTestCase {
|
||||
XCTAssertTrue(api.aiReretouchRequests.isEmpty)
|
||||
}
|
||||
|
||||
func testMissingRequiredTemplateDisablesMatchingWorkflow() async {
|
||||
func testMissingOptionalAtmosphereDoesNotDisableAllReretouchWorkflow() async {
|
||||
let api = makeAPI()
|
||||
api.aiRetouchTemplatesResponse = TravelAlbumAIRetouchTemplatesResponse(
|
||||
refinedTemplates: [template(11, "清透")],
|
||||
atmosphereTemplates: [],
|
||||
coverTemplates: []
|
||||
coverTemplates: [],
|
||||
remainingQuota: 100
|
||||
)
|
||||
let viewModel = TravelAlbumAIRetouchTemplateViewModel(
|
||||
scenicId: 18,
|
||||
@@ -186,8 +198,42 @@ final class TravelAlbumAIRetouchTemplateViewModelTests: XCTestCase {
|
||||
|
||||
await viewModel.loadTemplates(api: api)
|
||||
|
||||
XCTAssertFalse(viewModel.canSubmit)
|
||||
XCTAssertEqual(viewModel.validationMessage, "暂无可用的氛围感修图模板")
|
||||
XCTAssertTrue(viewModel.canSubmit)
|
||||
XCTAssertNil(viewModel.validationMessage)
|
||||
XCTAssertEqual(viewModel.requiredQuota, 1)
|
||||
}
|
||||
|
||||
func testMissingRequiredRefinedAndCoverTemplatesDisableMatchingWorkflows() async {
|
||||
let refinedAPI = makeAPI()
|
||||
refinedAPI.aiRetouchTemplatesResponse = TravelAlbumAIRetouchTemplatesResponse(
|
||||
refinedTemplates: [],
|
||||
atmosphereTemplates: [template(21, "暖阳")],
|
||||
coverTemplates: [template(31, "杂志")],
|
||||
remainingQuota: 100
|
||||
)
|
||||
let reretouch = TravelAlbumAIRetouchTemplateViewModel(
|
||||
scenicId: 18,
|
||||
workflow: .reretouch(materialId: 7, batchId: 70, type: .all)
|
||||
)
|
||||
await reretouch.loadTemplates(api: refinedAPI)
|
||||
XCTAssertFalse(reretouch.canSubmit)
|
||||
XCTAssertEqual(reretouch.validationMessage, "暂无可用的原图精修模板")
|
||||
|
||||
let coverAPI = makeAPI()
|
||||
coverAPI.aiRetouchTemplatesResponse = TravelAlbumAIRetouchTemplatesResponse(
|
||||
refinedTemplates: [template(11, "清透")],
|
||||
atmosphereTemplates: [template(21, "暖阳")],
|
||||
coverTemplates: [],
|
||||
remainingQuota: 100
|
||||
)
|
||||
let batch = TravelAlbumAIRetouchTemplateViewModel(
|
||||
albumId: 8,
|
||||
scenicId: 18,
|
||||
materialIds: [1, 2, 3, 4]
|
||||
)
|
||||
await batch.loadTemplates(api: coverAPI)
|
||||
XCTAssertFalse(batch.canSubmit)
|
||||
XCTAssertEqual(batch.validationMessage, "暂无可用的封面风格模板")
|
||||
}
|
||||
|
||||
func testLoadFailureExposesRetryMessageAndKeepsSubmissionDisabled() async {
|
||||
|
||||
@@ -243,6 +243,7 @@ final class TravelAlbumAPITests: XCTestCase {
|
||||
jobSubmissionJSON(batchId: 51, albumId: 6),
|
||||
jobSubmissionJSON(batchId: 52, albumId: 6),
|
||||
jobSubmissionJSON(batchId: 53, albumId: 6),
|
||||
jobSubmissionJSON(batchId: 54, albumId: 6),
|
||||
])
|
||||
let api = TravelAlbumAPI(client: APIClient(environment: .testing, session: session))
|
||||
|
||||
@@ -273,13 +274,22 @@ final class TravelAlbumAPITests: XCTestCase {
|
||||
atmosphereTemplateId: 22
|
||||
)
|
||||
)
|
||||
try await api.submitAIReretouch(
|
||||
TravelAlbumAIReretouchRequest(
|
||||
id: 14,
|
||||
aiRetouchBatchId: 54,
|
||||
type: .all,
|
||||
refinedTemplateId: 21,
|
||||
atmosphereTemplateId: nil
|
||||
)
|
||||
)
|
||||
|
||||
let bodies = try session.requests.map { request in
|
||||
try JSONSerialization.jsonObject(with: XCTUnwrap(request.httpBody)) as? [String: Any]
|
||||
}
|
||||
XCTAssertEqual(session.requests.map { $0.url?.path }, Array(
|
||||
repeating: "/api/yf-handset-app/photog/travel-album/ai-reretouch",
|
||||
count: 3
|
||||
count: 4
|
||||
))
|
||||
XCTAssertEqual(Set(bodies[0]?.keys.map { $0 } ?? []), ["id", "ai_retouch_batch_id", "type", "refined_template_id"])
|
||||
XCTAssertEqual(Set(bodies[1]?.keys.map { $0 } ?? []), ["id", "ai_retouch_batch_id", "type", "atmosphere_template_id"])
|
||||
@@ -290,9 +300,16 @@ final class TravelAlbumAPITests: XCTestCase {
|
||||
"refined_template_id",
|
||||
"atmosphere_template_id",
|
||||
])
|
||||
XCTAssertEqual(Set(bodies[3]?.keys.map { $0 } ?? []), [
|
||||
"id",
|
||||
"ai_retouch_batch_id",
|
||||
"type",
|
||||
"refined_template_id",
|
||||
])
|
||||
XCTAssertEqual(bodies[0]?["type"] as? Int, 1)
|
||||
XCTAssertEqual(bodies[1]?["type"] as? Int, 2)
|
||||
XCTAssertEqual(bodies[2]?["type"] as? Int, 3)
|
||||
XCTAssertEqual(bodies[3]?["type"] as? Int, 3)
|
||||
}
|
||||
|
||||
func testAIJobListBuildsCursorQueryAndDecodesUnknownStatusSafely() async throws {
|
||||
|
||||
@@ -763,7 +763,7 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase {
|
||||
XCTAssertEqual(selectionCountLabel.text, "已选择 4 张照片")
|
||||
XCTAssertEqual(
|
||||
tipsLabel.text,
|
||||
"Tips:氛围感修图为选填,可横向选择一种样式;选中后每张照片会额外生成1个独立结果,第一张照片仍另生成封面。"
|
||||
"Tips:氛围感修图为选填;封面风格为必选,将使用第一张照片另生成封面。"
|
||||
)
|
||||
XCTAssertFalse(tipsContainer.isHidden)
|
||||
XCTAssertEqual(tipsContainer.backgroundColor?.travelAlbumTestHexRGB, 0xF4F8FF)
|
||||
@@ -806,10 +806,9 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase {
|
||||
} as? UICollectionView
|
||||
)
|
||||
let accessibleLabels = controller.view.allAccessibilityLabels()
|
||||
XCTAssertTrue(accessibleLabels.contains("原图精修"))
|
||||
XCTAssertFalse(accessibleLabels.contains("原图精修,必选"))
|
||||
XCTAssertTrue(accessibleLabels.contains("原图精修,必选"))
|
||||
XCTAssertTrue(accessibleLabels.contains("氛围感修图,选填"))
|
||||
XCTAssertTrue(accessibleLabels.contains("封面风格,赠送 · 不占额度"))
|
||||
XCTAssertTrue(accessibleLabels.contains("封面风格,必选 · 赠送 · 不占额度"))
|
||||
|
||||
collectionView.scrollToItem(
|
||||
at: IndexPath(item: 0, section: 4),
|
||||
@@ -954,7 +953,7 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase {
|
||||
XCTAssertEqual(comparisonButton.bounds.size, CGSize(width: 48, height: 48))
|
||||
}
|
||||
|
||||
func testPreviewAIRetouchUsesSelectedRetouchedTabWorkflowAndShowsTaskAction() async throws {
|
||||
func testPreviewAIRetouchUsesSelectedRetouchedTabWorkflowAndNotifiesSubmission() async throws {
|
||||
UIView.setAnimationsEnabled(false)
|
||||
defer { UIView.setAnimationsEnabled(true) }
|
||||
let api = TravelAlbumMockAPI()
|
||||
@@ -985,13 +984,15 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase {
|
||||
),
|
||||
]
|
||||
)
|
||||
var submissionNotified = false
|
||||
let controller = TravelAlbumPhotoPreviewViewController(
|
||||
projects: [project],
|
||||
totalCount: 1,
|
||||
startProjectIndex: 0,
|
||||
albumId: 8,
|
||||
scenicIdProvider: { 18 },
|
||||
aiRetouchAPI: api
|
||||
aiRetouchAPI: api,
|
||||
onAIRetouchSubmitted: { submissionNotified = true }
|
||||
)
|
||||
let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 390, height: 844))
|
||||
window.rootViewController = controller
|
||||
@@ -1048,12 +1049,10 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase {
|
||||
XCTAssertTrue(tipsLabel.isHidden)
|
||||
confirmButton.sendActions(for: .touchUpInside)
|
||||
await waitUntil { api.aiReretouchRequests.count == 1 }
|
||||
await waitUntil { controller.presentedViewController is UIAlertController }
|
||||
await waitUntil { submissionNotified }
|
||||
|
||||
XCTAssertEqual(api.aiReretouchRequests.first?.type, .refined)
|
||||
let successAlert = try XCTUnwrap(controller.presentedViewController as? UIAlertController)
|
||||
XCTAssertEqual(successAlert.title, "AI修图任务已提交")
|
||||
XCTAssertEqual(successAlert.actions.map(\.title), ["知道了", "查看任务"])
|
||||
XCTAssertTrue(submissionNotified)
|
||||
XCTAssertTrue(window.rootViewController === controller)
|
||||
}
|
||||
|
||||
|
||||
@@ -123,6 +123,19 @@ final class TravelAlbumEntryViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(viewModel.albums.first?.name, "A")
|
||||
}
|
||||
|
||||
func testPullRefreshFailureKeepsExistingAlbums() async {
|
||||
let api = TravelAlbumMockAPI()
|
||||
api.listResponse = TravelAlbumListResponse(total: 1, list: [TravelAlbum(id: 1, name: "A")])
|
||||
let viewModel = TravelAlbumEntryViewModel(currentScenicIdProvider: { 1 })
|
||||
await viewModel.loadAlbums(api: api)
|
||||
api.listError = APIError.serverCode(500, "刷新失败")
|
||||
|
||||
await viewModel.refreshAlbums(api: api)
|
||||
|
||||
XCTAssertEqual(viewModel.albumTotal, 1)
|
||||
XCTAssertEqual(viewModel.albums.map(\.id), [1])
|
||||
}
|
||||
|
||||
func testPreShootRequiresSinglePrice() async {
|
||||
let api = TravelAlbumMockAPI()
|
||||
let viewModel = TravelAlbumEntryViewModel(currentScenicIdProvider: { 1 })
|
||||
@@ -1226,6 +1239,7 @@ final class TravelAlbumMockAPI: TravelAlbumServing {
|
||||
|
||||
var availableOrdersResponse: [TravelAlbumAvailableOrder] = []
|
||||
var listResponse = TravelAlbumListResponse<TravelAlbum>()
|
||||
var listError: Error?
|
||||
var createResponse = TravelAlbumCreateResponse(id: 0)
|
||||
var infoResponse = TravelAlbum()
|
||||
var materialListResponses: [TravelAlbumListResponse<TravelAlbumMaterial>] = []
|
||||
@@ -1281,7 +1295,8 @@ final class TravelAlbumMockAPI: TravelAlbumServing {
|
||||
}
|
||||
|
||||
func list(page: Int, pageSize: Int) async throws -> TravelAlbumListResponse<TravelAlbum> {
|
||||
listResponse
|
||||
if let listError { throw listError }
|
||||
return listResponse
|
||||
}
|
||||
|
||||
func info(id: Int) async throws -> TravelAlbum {
|
||||
|
||||
Reference in New Issue
Block a user