Files
suixinkan_ios_new/suixinkan/Features/Assets/ViewModels/AssetsViewModels.swift
汉秋 8e34a45d50 移除 MJRefresh 依赖,恢复 SwiftUI 系统下拉刷新。
同时收紧各 ViewModel 内部 isLoading 可见性,避免不必要的视图刷新。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 10:55:45 +08:00

968 lines
33 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// AssetsViewModels.swift
// suixinkan
//
// Created by Codex on 2026/6/23.
//
import Foundation
import Combine
/// App
@MainActor
final class CloudTransferStore: ObservableObject {
static let shared = CloudTransferStore()
@Published private(set) var records: [CloudTransferItem] = []
/// ID
@discardableResult
func start(fileName: String, direction: CloudTransferDirection) -> UUID {
let id = UUID()
records.insert(CloudTransferItem(id: id, fileName: fileName, direction: direction), at: 0)
return id
}
///
func update(id: UUID, progress: Int, message: String = "") {
guard let index = records.firstIndex(where: { $0.id == id }) else { return }
records[index].progress = max(0, min(100, progress))
if !message.isEmpty {
records[index].message = message
}
}
///
func succeed(id: UUID, message: String = "已完成") {
guard let index = records.firstIndex(where: { $0.id == id }) else { return }
records[index].progress = 100
records[index].status = .success
records[index].message = message
}
///
func fail(id: UUID, message: String) {
guard let index = records.firstIndex(where: { $0.id == id }) else { return }
records[index].status = .failed
records[index].message = message
}
///
func clear() {
records = []
}
}
/// ViewModel
@MainActor
final class CloudStorageViewModel: ObservableObject {
@Published var path: [CloudDriveFile] = [CloudDriveFile(id: 0, name: "云盘")]
@Published var files: [CloudDriveFile] = []
@Published var total = 0
@Published var page = 1
@Published var searchText = ""
@Published var selectedFilter: CloudDriveFilter = .all
@Published var selectedSort: CloudDriveSort = .updatedDesc
private var isLoading = false
@Published var isLoadingMore = false
@Published var isMutating = false
@Published var errorMessage: String?
private let pageSize = 20
/// ID
var currentFolderId: Int {
path.last?.id ?? 0
}
///
var hasMore: Bool {
files.count < total
}
///
func reload(api: any AssetsServing) async {
isLoading = true
errorMessage = nil
defer { isLoading = false }
do {
let payload = try await requestFiles(api: api, page: 1)
page = 1
files = payload.list
total = payload.total
} catch {
files = []
total = 0
page = 1
errorMessage = error.localizedDescription
}
}
///
func loadMore(api: any AssetsServing) async {
guard hasMore, !isLoadingMore else { return }
isLoadingMore = true
defer { isLoadingMore = false }
do {
let nextPage = page + 1
let payload = try await requestFiles(api: api, page: nextPage)
page = nextPage
total = payload.total
files.append(contentsOf: payload.list)
} catch {
errorMessage = error.localizedDescription
}
}
///
func enterFolder(_ folder: CloudDriveFile, api: any AssetsServing) async {
guard folder.isFolder else { return }
path.append(folder)
await reload(api: api)
}
///
func popToFolder(at index: Int, api: any AssetsServing) async {
guard path.indices.contains(index) else { return }
path = Array(path.prefix(index + 1))
await reload(api: api)
}
///
func createFolder(name: String, api: any AssetsServing) async -> Bool {
let folderName = name.trimmingCharacters(in: .whitespacesAndNewlines)
guard !folderName.isEmpty else {
errorMessage = "请输入文件夹名称"
return false
}
return await mutate(api: api) {
try await api.cloudFolderCreate(CloudFolderCreateRequest(parentFolderId: currentFolderId, name: folderName))
}
}
///
func rename(_ item: CloudDriveFile, newName: String, api: any AssetsServing) async -> Bool {
let fileName = newName.trimmingCharacters(in: .whitespacesAndNewlines)
guard !fileName.isEmpty else {
errorMessage = "请输入名称"
return false
}
return await mutate(api: api) {
if item.isFolder {
try await api.cloudFolderEdit(CloudFolderModifyRequest(id: item.id, name: fileName))
} else {
try await api.cloudFileEdit(CloudFileModifyRequest(id: item.id, fileName: fileName))
}
}
}
///
func delete(_ item: CloudDriveFile, api: any AssetsServing) async -> Bool {
await mutate(api: api) {
let action = CloudFileActionItem(id: item.id, type: item.type)
try await api.cloudFileDelete(CloudFileDeleteRequest(list: [action]))
}
}
///
func move(_ item: CloudDriveFile, targetFolderId: Int, api: any AssetsServing) async -> Bool {
await mutate(api: api) {
let action = CloudFileActionItem(id: item.id, type: item.type)
try await api.cloudFileMove(CloudFileMoveRequest(targetFolderId: targetFolderId, list: [action]))
}
}
/// OSS
func upload(
localFiles: [CloudLocalUploadFile],
scenicId: Int?,
api: any AssetsServing,
uploadService: any OSSUploadServing,
transferStore: CloudTransferStore
) async -> Bool {
guard let scenicId else {
errorMessage = "缺少当前景区,无法上传"
return false
}
guard !localFiles.isEmpty else { return true }
isMutating = true
defer { isMutating = false }
do {
let permission = try await api.checkCloudUploadPermission()
guard permission.canUpload else {
let reason = permission.reason.trimmingCharacters(in: .whitespacesAndNewlines)
errorMessage = reason.isEmpty ? "当前账号暂无上传权限" : reason
return false
}
for file in localFiles {
let transferId = transferStore.start(fileName: file.fileName, direction: .upload)
do {
let url = try await uploadService.uploadCloudFile(
data: file.data,
fileName: file.fileName,
fileType: file.fileType,
scenicId: scenicId
) { progress in
Task { @MainActor in
transferStore.update(id: transferId, progress: progress)
}
}
try await api.cloudFileUpload(
CloudFileUploadRequest(parentFolderId: currentFolderId, fileUrl: url, fileName: file.fileName)
)
transferStore.succeed(id: transferId)
} catch {
transferStore.fail(id: transferId, message: error.localizedDescription)
throw error
}
}
await reload(api: api)
return true
} catch {
errorMessage = error.localizedDescription
return false
}
}
///
private func mutate(api: any AssetsServing, operation: () async throws -> Void) async -> Bool {
isMutating = true
defer { isMutating = false }
do {
try await operation()
await reload(api: api)
return true
} catch {
errorMessage = error.localizedDescription
return false
}
}
///
private func requestFiles(api: any AssetsServing, page: Int) async throws -> ListPayload<CloudDriveFile> {
try await api.cloudFileList(
parentFolderId: currentFolderId,
name: searchText.trimmingCharacters(in: .whitespacesAndNewlines),
type: selectedFilter.rawValue,
orderBy: selectedSort.rawValue,
page: page,
pageSize: pageSize
)
}
}
/// ViewModel
@MainActor
final class MediaLibraryViewModel: ObservableObject {
let kind: MediaLibraryKind
@Published var items: [MediaLibraryItem] = []
@Published var selectedDetail: MediaLibraryDetail?
@Published var orderInfo: MediaLibraryOrderInfo?
@Published var total = 0
@Published var page = 1
@Published var keyword = ""
@Published var selectedAuditFilter: MediaLibraryAuditFilter = .all
private var isLoading = false
@Published var isLoadingMore = false
private var isLoadingDetail = false
@Published var errorMessage: String?
private let pageSize = 10
/// ViewModel
init(kind: MediaLibraryKind = .material) {
self.kind = kind
}
///
var hasMore: Bool {
items.count < total
}
///
func reload(api: any AssetsServing) async {
isLoading = true
errorMessage = nil
defer { isLoading = false }
do {
let response = try await requestList(api: api, page: 1)
page = 1
items = response.list
total = response.total
orderInfo = response.order
} catch {
items = []
total = 0
orderInfo = nil
page = 1
errorMessage = error.localizedDescription
}
}
///
func loadMore(api: any AssetsServing) async {
guard hasMore, !isLoadingMore else { return }
isLoadingMore = true
defer { isLoadingMore = false }
do {
let nextPage = page + 1
let response = try await requestList(api: api, page: nextPage)
page = nextPage
total = response.total
orderInfo = response.order ?? orderInfo
items.append(contentsOf: response.list)
} catch {
errorMessage = error.localizedDescription
}
}
///
func loadDetail(id: Int, api: any AssetsServing) async {
isLoadingDetail = true
errorMessage = nil
defer { isLoadingDetail = false }
do {
selectedDetail = try await api.mediaAlbumDetail(id: id, kind: kind)
} catch {
errorMessage = error.localizedDescription
}
}
///
func toggleListing(_ item: MediaLibraryItem, api: any AssetsServing) async -> Bool {
guard item.isApproved else {
errorMessage = "审核通过后才能上下架"
return false
}
return await operate(api: api) {
let target = item.listing.toggled
try await api.mediaAlbumOperation(MediaAlbumOperationRequest(id: item.id, type: kind.rawValue, listingStatus: target.rawValue))
}
}
///
func toggleListing(detail: MediaLibraryDetail, api: any AssetsServing) async -> Bool {
guard detail.isApproved else {
errorMessage = "审核通过后才能上下架"
return false
}
return await operate(api: api) {
let target = detail.listing.toggled
try await api.mediaAlbumOperation(MediaAlbumOperationRequest(id: detail.id, type: kind.rawValue, listingStatus: target.rawValue))
}
}
///
func delete(id: Int, api: any AssetsServing) async -> Bool {
await operate(api: api) {
try await api.mediaAlbumDelete(MediaAlbumDeleteRequest(id: id, type: kind.rawValue))
}
}
///
private func operate(api: any AssetsServing, operation: () async throws -> Void) async -> Bool {
do {
try await operation()
await reload(api: api)
return true
} catch {
errorMessage = error.localizedDescription
return false
}
}
///
private func requestList(api: any AssetsServing, page: Int) async throws -> MediaLibraryListResponse {
try await api.mediaAlbumList(
kind: kind,
keyword: keyword.trimmingCharacters(in: .whitespacesAndNewlines),
status: selectedAuditFilter.rawValue >= 0 ? selectedAuditFilter.rawValue : nil,
page: page,
pageSize: pageSize
)
}
}
/// ViewModelOSS
@MainActor
final class MediaLibraryEditorViewModel: ObservableObject {
@Published var name = ""
@Published var description = ""
@Published var selectedSpotId: Int?
@Published var selectedProjectId: Int?
@Published var projects: [PhotographerProjectItem] = []
@Published var tagsText = ""
@Published var coverFile: MediaLocalUploadFile?
@Published var mediaFiles: [MediaLocalUploadFile] = []
@Published var isSubmitting = false
@Published var isLoadingProjects = false
@Published var errorMessage: String?
@Published var didSubmitSuccessfully = false
private let editingId: Int?
///
init(detail: MediaLibraryDetail? = nil) {
editingId = detail?.id
name = detail?.name ?? ""
description = detail?.description ?? ""
}
///
var isEditing: Bool {
editingId != nil
}
///
func loadProjects(scenicId: Int?, api: any AssetsServing) async {
guard let scenicId else {
projects = []
errorMessage = "缺少当前景区,无法加载项目"
return
}
isLoadingProjects = true
errorMessage = nil
defer { isLoadingProjects = false }
do {
let payload = try await api.projectList(scenicId: scenicId, name: nil, page: 1, pageSize: 50)
projects = payload.list
if let selectedProjectId, !projects.contains(where: { $0.id == selectedProjectId }) {
self.selectedProjectId = nil
}
} catch {
projects = []
errorMessage = error.localizedDescription
}
}
///
func setCover(_ file: MediaLocalUploadFile) {
coverFile = file
}
///
func addMediaFiles(_ files: [MediaLocalUploadFile]) {
mediaFiles.append(contentsOf: files)
}
///
func removeMediaFile(id: UUID) {
mediaFiles.removeAll { $0.id == id }
}
///
func submit(
kind: MediaLibraryKind = .material,
scenicId: Int?,
api: any AssetsServing,
uploadService: any OSSUploadServing
) async -> Bool {
guard validate(kind: kind, scenicId: scenicId) else { return false }
guard !isSubmitting else { return false }
isSubmitting = true
defer { isSubmitting = false }
do {
let actualScenicId = scenicId ?? 0
let coverUpload = try await uploadCover(scenicId: actualScenicId, uploadService: uploadService)
let uploads = try await uploadMediaFiles(scenicId: actualScenicId, uploadService: uploadService)
if let editingId {
let request = MediaAlbumEditRequest(
id: editingId,
name: name.trimmingCharacters(in: .whitespacesAndNewlines),
type: kind.rawValue,
mediaType: resolvedMediaType(),
mediaList: uploads,
coverUrl: coverUpload.url,
coverSize: coverUpload.size,
scenicSpotId: selectedSpotId ?? 0,
description: description,
materialTag: normalizedTags()
)
try await api.mediaAlbumEdit(request)
} else {
let request = MediaAlbumUploadRequest(
name: name.trimmingCharacters(in: .whitespacesAndNewlines),
type: kind.rawValue,
mediaType: resolvedMediaType(),
mediaList: uploads,
coverUrl: coverUpload.url,
coverSize: coverUpload.size,
scenicSpotId: selectedSpotId ?? 0,
description: description,
materialTag: normalizedTags(),
projectId: kind == .sample ? (selectedProjectId ?? 0) : 0
)
try await api.mediaAlbumUpload(request)
}
didSubmitSuccessfully = true
return true
} catch {
errorMessage = error.localizedDescription
return false
}
}
///
private func validate(kind: MediaLibraryKind, scenicId: Int?) -> Bool {
let title = name.trimmingCharacters(in: .whitespacesAndNewlines)
guard scenicId != nil else {
errorMessage = "缺少当前景区,无法提交\(kind == .sample ? "样片" : "素材")"
return false
}
guard !title.isEmpty else {
errorMessage = "请输入\(kind == .sample ? "样片" : "素材")名称"
return false
}
guard selectedSpotId != nil else {
errorMessage = "请选择打卡点"
return false
}
guard kind != .sample || selectedProjectId != nil else {
errorMessage = "请选择关联项目"
return false
}
guard coverFile != nil else {
errorMessage = "请选择封面"
return false
}
guard !mediaFiles.isEmpty else {
errorMessage = "请选择\(kind == .sample ? "样片" : "素材")文件"
return false
}
guard normalizedTags().count <= 120 else {
errorMessage = "标签内容过长"
return false
}
return true
}
/// URL
private func uploadCover(
scenicId: Int,
uploadService: any OSSUploadServing
) async throws -> (url: String, size: MediaAlbumFileSize) {
guard let coverFile else {
throw APIError.emptyData
}
let url = try await uploadService.uploadCloudFile(
data: coverFile.data,
fileName: coverFile.fileName,
fileType: coverFile.fileType,
scenicId: scenicId,
onProgress: { _ in }
)
return (url, MediaAlbumFileSize(width: coverFile.width, height: coverFile.height))
}
///
private func uploadMediaFiles(
scenicId: Int,
uploadService: any OSSUploadServing
) async throws -> [MediaAlbumUploadItem] {
var result: [MediaAlbumUploadItem] = []
for file in mediaFiles {
let url = try await uploadService.uploadCloudFile(
data: file.data,
fileName: file.fileName,
fileType: file.fileType,
scenicId: scenicId,
onProgress: { _ in }
)
result.append(
MediaAlbumUploadItem(
originalName: file.fileName,
ossUrl: url,
size: Int64(file.data.count),
fileWidthSize: MediaAlbumFileSize(width: file.width, height: file.height)
)
)
}
return result
}
///
private func resolvedMediaType() -> Int {
mediaFiles.contains { $0.fileType == 1 } ? 1 : 2
}
///
private func normalizedTags() -> String {
tagsText
.split { $0 == "," || $0 == "" || $0 == " " || $0 == "\n" }
.map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
.joined(separator: ",")
}
}
/// ViewModel
@MainActor
final class AlbumListViewModel: ObservableObject {
@Published var folders: [AlbumFolderItem] = []
@Published var total = 0
@Published var page = 1
@Published var searchText = ""
@Published var startTime = ""
@Published var endTime = ""
private var isLoading = false
@Published var isLoadingMore = false
@Published var isMutating = false
@Published var errorMessage: String?
private let pageSize = 10
///
var hasMore: Bool {
folders.count < total
}
///
func reload(api: any AssetsServing, scenicId: Int?) async {
guard let scenicId else {
folders = []
total = 0
page = 1
errorMessage = "缺少当前景区,无法加载相册"
return
}
isLoading = true
errorMessage = nil
defer { isLoading = false }
do {
let payload = try await requestFolders(api: api, scenicId: scenicId, page: 1)
self.page = 1
folders = payload.list
total = payload.total
} catch {
folders = []
total = 0
page = 1
errorMessage = error.localizedDescription
}
}
///
func loadMore(api: any AssetsServing, scenicId: Int?) async {
guard let scenicId, hasMore, !isLoadingMore else { return }
isLoadingMore = true
defer { isLoadingMore = false }
do {
let nextPage = page + 1
let payload = try await requestFolders(api: api, scenicId: scenicId, page: nextPage)
page = nextPage
total = payload.total
folders.append(contentsOf: payload.list)
} catch {
errorMessage = error.localizedDescription
}
}
///
func createFolder(name: String, remark: String, scenicId: Int?, api: any AssetsServing) async -> Bool {
let folderName = name.trimmingCharacters(in: .whitespacesAndNewlines)
guard let scenicId else {
errorMessage = "缺少当前景区,无法创建相册"
return false
}
guard !folderName.isEmpty else {
errorMessage = "请输入相册名称"
return false
}
guard !isMutating else { return false }
isMutating = true
defer { isMutating = false }
do {
try await api.addAlbumFolder(scenicId: scenicId, name: folderName, remark: remark.trimmingCharacters(in: .whitespacesAndNewlines))
await reload(api: api, scenicId: scenicId)
return true
} catch {
errorMessage = error.localizedDescription
return false
}
}
///
private func requestFolders(api: any AssetsServing, scenicId: Int, page: Int) async throws -> ListPayload<AlbumFolderItem> {
try await api.albumFolderList(
scenicId: scenicId,
page: page,
pageSize: pageSize,
name: searchText.trimmingCharacters(in: .whitespacesAndNewlines),
startTime: startTime.trimmingCharacters(in: .whitespacesAndNewlines).assetsNilIfEmpty,
endTime: endTime.trimmingCharacters(in: .whitespacesAndNewlines).assetsNilIfEmpty
)
}
}
/// ViewModel/
@MainActor
final class AlbumDetailViewModel: ObservableObject {
let folderId: Int
@Published var folder: AlbumFolderItem?
@Published var files: [AlbumFileItem] = []
@Published var selectedTab: AlbumFileTab = .image
@Published var total = 0
@Published var page = 1
private var isLoading = false
@Published var isLoadingMore = false
@Published var isMutating = false
@Published var errorMessage: String?
private let pageSize = 20
/// ViewModel
init(folderId: Int, summary: AlbumFolderItem? = nil) {
self.folderId = folderId
folder = summary
}
///
var hasMore: Bool {
files.count < total
}
/// Tab
func reload(api: any AssetsServing, scenicId: Int?) async {
guard let scenicId else {
files = []
total = 0
page = 1
errorMessage = "缺少当前景区,无法加载相册内容"
return
}
isLoading = true
errorMessage = nil
defer { isLoading = false }
do {
folder = try await api.albumFolderInfo(id: folderId)
let listPayload = try await api.albumFileList(
scenicId: scenicId,
folderId: folderId,
fileType: selectedTab.rawValue,
page: 1,
pageSize: pageSize
)
files = listPayload.list
total = listPayload.total
page = 1
} catch {
page = 1
errorMessage = error.localizedDescription
}
}
///
func selectTab(_ tab: AlbumFileTab, api: any AssetsServing, scenicId: Int?) async {
guard selectedTab != tab else { return }
selectedTab = tab
files = []
total = 0
page = 1
await reload(api: api, scenicId: scenicId)
}
/// Tab
func loadMore(api: any AssetsServing, scenicId: Int?) async {
guard let scenicId, hasMore, !isLoadingMore else { return }
isLoadingMore = true
defer { isLoadingMore = false }
do {
let nextPage = page + 1
let payload = try await api.albumFileList(
scenicId: scenicId,
folderId: folderId,
fileType: selectedTab.rawValue,
page: nextPage,
pageSize: pageSize
)
page = nextPage
total = payload.total
files.append(contentsOf: payload.list)
} catch {
errorMessage = error.localizedDescription
}
}
///
func delete(file: AlbumFileItem, scenicId: Int?, api: any AssetsServing) async -> Bool {
await mutate(api: api, scenicId: scenicId) {
try await api.deleteAlbumFiles(folderId: folderId, idList: [file.id])
}
}
///
func setCover(file: AlbumFileItem, scenicId: Int?, api: any AssetsServing) async -> Bool {
await mutate(api: api, scenicId: scenicId) {
try await api.editAlbumFolder(folderId: folderId, coverFileId: file.id, name: nil, remark: nil)
}
}
///
func rename(name: String, scenicId: Int?, api: any AssetsServing) async -> Bool {
let folderName = name.trimmingCharacters(in: .whitespacesAndNewlines)
guard !folderName.isEmpty else {
errorMessage = "请输入相册名称"
return false
}
return await mutate(api: api, scenicId: scenicId) {
try await api.editAlbumFolder(folderId: folderId, coverFileId: nil, name: folderName, remark: nil)
}
}
///
func updateRemark(_ remark: String, scenicId: Int?, api: any AssetsServing) async -> Bool {
await mutate(api: api, scenicId: scenicId) {
try await api.editAlbumFolder(folderId: folderId, coverFileId: nil, name: nil, remark: remark.trimmingCharacters(in: .whitespacesAndNewlines))
}
}
///
private func mutate(api: any AssetsServing, scenicId: Int?, operation: () async throws -> Void) async -> Bool {
guard !isMutating else { return false }
guard scenicId != nil else {
errorMessage = "缺少当前景区,无法操作相册"
return false
}
isMutating = true
defer { isMutating = false }
do {
try await operation()
await reload(api: api, scenicId: scenicId)
return true
} catch {
errorMessage = error.localizedDescription
return false
}
}
}
/// ViewModel
@MainActor
final class AlbumTrailerViewModel: ObservableObject {
@Published var folders: [AlbumFolderItem] = []
@Published var selectedFolderId: Int?
@Published var localFiles: [AlbumLocalUploadFile] = []
@Published var uploadProgress = 0
private var isLoadingFolders = false
@Published var isSubmitting = false
@Published var errorMessage: String?
@Published var didUploadSuccessfully = false
///
func loadFolders(api: any AssetsServing, scenicId: Int?) async {
guard let scenicId else {
folders = []
selectedFolderId = nil
errorMessage = "缺少当前景区,无法加载相册"
return
}
isLoadingFolders = true
errorMessage = nil
defer { isLoadingFolders = false }
do {
let payload = try await api.albumFolderList(
scenicId: scenicId,
page: 1,
pageSize: 100,
name: "",
startTime: nil,
endTime: nil
)
folders = payload.list
if selectedFolderId == nil {
selectedFolderId = folders.first?.id
}
} catch {
folders = []
selectedFolderId = nil
errorMessage = error.localizedDescription
}
}
///
func addLocalFiles(_ files: [AlbumLocalUploadFile]) {
localFiles.append(contentsOf: files)
}
///
func removeLocalFile(id: UUID) {
localFiles.removeAll { $0.id == id }
}
/// OSS
func submit(scenicId: Int?, api: any AssetsServing, uploadService: any OSSUploadServing) async -> Bool {
guard let scenicId else {
errorMessage = "缺少当前景区,无法上传"
return false
}
guard let folderId = selectedFolderId else {
errorMessage = "请选择相册"
return false
}
guard !localFiles.isEmpty else {
errorMessage = "请选择要上传的图片或视频"
return false
}
guard !isSubmitting else { return false }
isSubmitting = true
didUploadSuccessfully = false
uploadProgress = 0
defer { isSubmitting = false }
do {
let fileCount = max(localFiles.count, 1)
for (index, file) in localFiles.enumerated() {
let url = try await uploadService.uploadAlbumFile(
data: file.data,
fileName: file.fileName,
fileType: file.fileType,
scenicId: scenicId
) { progress in
Task { @MainActor in
let base = Double(index) / Double(fileCount)
let step = Double(progress) / Double(fileCount)
self.uploadProgress = min(99, Int((base + step / 100) * 100))
}
}
try await api.albumFileUploadURL(scenicId: scenicId, fileURL: url, folderId: folderId)
}
uploadProgress = 100
didUploadSuccessfully = true
localFiles = []
return true
} catch {
errorMessage = error.localizedDescription
return false
}
}
}
private extension String {
/// nil便
var assetsNilIfEmpty: String? {
isEmpty ? nil : self
}
}