新增线下收款记录和 ai 修图优化
This commit is contained in:
@@ -42,13 +42,16 @@ final class TravelAlbumEntryViewModel {
|
||||
|
||||
private let currentScenicIdProvider: () -> Int
|
||||
private let dateProvider: () -> Date
|
||||
private let autoRetouchConfigurationStore: any TravelAlbumAutoRetouchConfigurationStoring
|
||||
|
||||
init(
|
||||
currentScenicIdProvider: @escaping () -> Int = { AppStore.shared.session.currentScenicId },
|
||||
dateProvider: @escaping () -> Date = Date.init
|
||||
dateProvider: @escaping () -> Date = Date.init,
|
||||
autoRetouchConfigurationStore: any TravelAlbumAutoRetouchConfigurationStoring = TravelAlbumAutoRetouchConfigurationStore.shared
|
||||
) {
|
||||
self.currentScenicIdProvider = currentScenicIdProvider
|
||||
self.dateProvider = dateProvider
|
||||
self.autoRetouchConfigurationStore = autoRetouchConfigurationStore
|
||||
}
|
||||
|
||||
/// 重新拉取相册列表。
|
||||
@@ -110,6 +113,7 @@ final class TravelAlbumEntryViewModel {
|
||||
freeCount: String,
|
||||
singlePrice: String,
|
||||
packagePrice: String,
|
||||
autoRetouchConfiguration: TravelAlbumAutoRetouchConfiguration = .disabled,
|
||||
order: TravelAlbumAvailableOrder?,
|
||||
api: any TravelAlbumServing
|
||||
) async {
|
||||
@@ -132,6 +136,10 @@ final class TravelAlbumEntryViewModel {
|
||||
onShowMessage?("请输入有效的单张照片价格")
|
||||
return
|
||||
}
|
||||
if !autoRetouchConfiguration.isValid {
|
||||
onShowMessage?("请选择修图模板")
|
||||
return
|
||||
}
|
||||
|
||||
let albumName: String
|
||||
switch mode {
|
||||
@@ -174,6 +182,7 @@ final class TravelAlbumEntryViewModel {
|
||||
|
||||
do {
|
||||
let response = try await api.create(request)
|
||||
autoRetouchConfigurationStore.save(autoRetouchConfiguration, albumID: response.id)
|
||||
isCreateSheetVisible = false
|
||||
onShowMessage?("任务创建成功")
|
||||
onCreatedAlbum?(response)
|
||||
|
||||
@@ -63,7 +63,7 @@ final class WiredCameraTransferViewModel {
|
||||
private(set) var sonyMTPHint: String?
|
||||
private(set) var isContentCatalogReady = false
|
||||
|
||||
let retouchOption = "不修图"
|
||||
private(set) var autoRetouchConfiguration: TravelAlbumAutoRetouchConfiguration
|
||||
private(set) var photoFormatOption: TravelAlbumOTGPhotoFormatOption = .jpg
|
||||
private(set) var transferMode: TravelAlbumOTGTransferMode = .liveUpload {
|
||||
didSet { notifyStateChanged() }
|
||||
@@ -80,6 +80,9 @@ final class WiredCameraTransferViewModel {
|
||||
private let api: any TravelAlbumServing
|
||||
private let appStore: AppStore
|
||||
private let userDefaults: UserDefaults
|
||||
private let autoRetouchConfigurationStore: any TravelAlbumAutoRetouchConfigurationStoring
|
||||
private let autoRetouchProcessor: any TravelAlbumAutoRetouchProcessing
|
||||
private let aiEditResultStore: TravelAlbumAIEditResultStore
|
||||
|
||||
private var currentDriver: CameraDriver?
|
||||
private var persistedRecordsById: [String: TravelAlbumOTGPhotoRecord] = [:]
|
||||
@@ -100,7 +103,10 @@ final class WiredCameraTransferViewModel {
|
||||
uploader: (any TravelAlbumOTGUploading)? = nil,
|
||||
api: (any TravelAlbumServing)? = nil,
|
||||
appStore: AppStore = .shared,
|
||||
userDefaults: UserDefaults = .standard
|
||||
userDefaults: UserDefaults = .standard,
|
||||
autoRetouchConfigurationStore: any TravelAlbumAutoRetouchConfigurationStoring = TravelAlbumAutoRetouchConfigurationStore.shared,
|
||||
autoRetouchProcessor: (any TravelAlbumAutoRetouchProcessing)? = nil,
|
||||
aiEditResultStore: TravelAlbumAIEditResultStore? = nil
|
||||
) {
|
||||
self.albumId = albumId
|
||||
self.albumTitle = albumTitle.isEmpty ? "有线传输" : albumTitle
|
||||
@@ -113,6 +119,10 @@ final class WiredCameraTransferViewModel {
|
||||
self.api = api ?? NetworkServices.shared.travelAlbumAPI
|
||||
self.appStore = appStore
|
||||
self.userDefaults = userDefaults
|
||||
self.autoRetouchConfigurationStore = autoRetouchConfigurationStore
|
||||
self.autoRetouchProcessor = autoRetouchProcessor ?? TravelAlbumAutoRetouchProcessor()
|
||||
self.aiEditResultStore = aiEditResultStore ?? .shared
|
||||
self.autoRetouchConfiguration = autoRetouchConfigurationStore.configuration(albumID: albumId)
|
||||
// 从相册管理选择模式进入时,以本次选择为准;其他旧入口继续沿用上次记录。
|
||||
self.transferMode = initialTransferMode ?? Self.persistedTransferMode(in: userDefaults)
|
||||
if let initialTransferMode {
|
||||
@@ -218,6 +228,11 @@ final class WiredCameraTransferViewModel {
|
||||
transferMode.title
|
||||
}
|
||||
|
||||
/// 自动 AI 修图设置项展示文案。
|
||||
var retouchOption: String {
|
||||
autoRetouchConfiguration.uploadOptionTitle
|
||||
}
|
||||
|
||||
/// 指定上传弹窗选项。
|
||||
var specifyUploadOptions: [TravelAlbumOTGSpecifyUploadOption] {
|
||||
TravelAlbumOTGSpecifyUploadOption.allCases
|
||||
@@ -306,6 +321,15 @@ final class WiredCameraTransferViewModel {
|
||||
userDefaults.set(mode.rawValue, forKey: Self.transferModeDefaultsKey)
|
||||
}
|
||||
|
||||
/// 更新当前相册的自动 AI 修图配置,只影响尚未开始的上传任务。
|
||||
func updateAutoRetouchConfiguration(_ configuration: TravelAlbumAutoRetouchConfiguration) {
|
||||
let normalized = configuration.isValid ? configuration : .disabled
|
||||
guard normalized != autoRetouchConfiguration else { return }
|
||||
autoRetouchConfiguration = normalized
|
||||
autoRetouchConfigurationStore.save(normalized, albumID: albumId)
|
||||
notifyStateChanged()
|
||||
}
|
||||
|
||||
/// 切换上传格式。
|
||||
func selectPhotoFormat(_ option: TravelAlbumOTGPhotoFormatOption) {
|
||||
guard photoFormatOption != option else { return }
|
||||
@@ -467,8 +491,31 @@ final class WiredCameraTransferViewModel {
|
||||
startUploadByIds([photoId], emptyMessage: "本地文件不存在,无法重传")
|
||||
}
|
||||
|
||||
/// 使用该照片上次失败时的模板重试自动修图,不重复上传原图。
|
||||
func retryAutoRetouch(photoId: String) {
|
||||
guard let record = persistedRecordsById[photoId],
|
||||
record.autoRetouchState == .failed,
|
||||
let materialId = record.materialId,
|
||||
let templateID = record.autoRetouchTemplateId,
|
||||
let preset = TravelAlbumEditPreset.autoRetouchOptions.first(where: { $0.id == templateID }) else {
|
||||
showMessage("暂无可重试的修图任务")
|
||||
return
|
||||
}
|
||||
Task {
|
||||
await processAutoRetouch(
|
||||
photoId: photoId,
|
||||
sourceRecord: record,
|
||||
materialId: materialId,
|
||||
preset: preset
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除单张本地照片。
|
||||
func deletePhoto(photoId: String) {
|
||||
if let materialId = persistedRecordsById[photoId]?.materialId {
|
||||
aiEditResultStore.remove(materialID: materialId)
|
||||
}
|
||||
storage.remove(albumId: albumId, photoId: photoId)
|
||||
persistedRecordsById.removeValue(forKey: photoId)
|
||||
selectedPhotoIds.remove(photoId)
|
||||
@@ -478,6 +525,9 @@ final class WiredCameraTransferViewModel {
|
||||
|
||||
/// 清空当前相册本地 OTG 缓存。
|
||||
func clearLocalAlbumCache() {
|
||||
persistedRecordsById.values.compactMap(\.materialId).forEach { materialId in
|
||||
aiEditResultStore.remove(materialID: materialId)
|
||||
}
|
||||
storage.clearAlbum(albumId: albumId)
|
||||
persistedRecordsById = [:]
|
||||
photos = []
|
||||
@@ -492,9 +542,27 @@ final class WiredCameraTransferViewModel {
|
||||
private func loadPersistedPhotos() {
|
||||
let records = storage.load(albumId: albumId)
|
||||
persistedRecordsById = Dictionary(uniqueKeysWithValues: records.map { ($0.id, $0) })
|
||||
restorePersistedAutoRetouchResults(records)
|
||||
applyMergedPhotos()
|
||||
}
|
||||
|
||||
private func restorePersistedAutoRetouchResults(_ records: [TravelAlbumOTGPhotoRecord]) {
|
||||
records.forEach { record in
|
||||
guard record.autoRetouchState == .completed,
|
||||
let materialId = record.materialId,
|
||||
let templateID = record.autoRetouchTemplateId,
|
||||
let resultURL = storage.absoluteURL(for: record.retouchedPath, albumId: albumId),
|
||||
let resultData = try? Data(contentsOf: resultURL) else {
|
||||
return
|
||||
}
|
||||
_ = aiEditResultStore.completeFromImageData(
|
||||
materialID: materialId,
|
||||
presetID: templateID,
|
||||
editedImageData: resultData
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func syncServerUploadStatuses() {
|
||||
serverStatusSyncTask?.cancel()
|
||||
guard albumId > 0 else { return }
|
||||
@@ -593,6 +661,8 @@ final class WiredCameraTransferViewModel {
|
||||
}
|
||||
|
||||
private func uploadPhoto(id: String) async {
|
||||
// 每张照片开始上传时固定一次配置,避免修图过程中切换模板导致结果不一致。
|
||||
let retouchConfiguration = autoRetouchConfiguration
|
||||
do {
|
||||
let record = try await localRecordForUpload(id: id)
|
||||
updateRecord(id: id, status: .uploading, progress: max(record.progress, 1), error: nil)
|
||||
@@ -602,13 +672,89 @@ final class WiredCameraTransferViewModel {
|
||||
) { [weak self] progress in
|
||||
self?.updateRecord(id: id, status: .uploading, progress: progress, error: nil)
|
||||
}
|
||||
updateRecord(id: id, status: .uploaded, progress: 100, error: nil, remoteUrl: material.fileUrl)
|
||||
// 上传期间用户可能删除照片,删除后不再回写本地任务或发起修图。
|
||||
guard persistedRecordsById[id] != nil else { return }
|
||||
updateRecord(
|
||||
id: id,
|
||||
status: retouchConfiguration.template == nil ? .uploaded : .uploading,
|
||||
progress: 100,
|
||||
error: nil,
|
||||
remoteUrl: material.fileUrl,
|
||||
materialId: material.id
|
||||
)
|
||||
guard let preset = retouchConfiguration.template else { return }
|
||||
await processAutoRetouch(
|
||||
photoId: id,
|
||||
sourceRecord: record,
|
||||
materialId: material.id,
|
||||
preset: preset
|
||||
)
|
||||
} catch {
|
||||
updateRecord(id: id, status: .failed, progress: 0, error: error.localizedDescription)
|
||||
showMessage(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
/// 对已完成原图上传的照片执行本地演示修图,修图失败不回退上传成功状态。
|
||||
private func processAutoRetouch(
|
||||
photoId: String,
|
||||
sourceRecord: TravelAlbumOTGPhotoRecord,
|
||||
materialId: Int,
|
||||
preset: TravelAlbumEditPreset
|
||||
) async {
|
||||
let resolvedRecord = storage.recordWithResolvedFilePaths(sourceRecord, albumId: albumId)
|
||||
let sourceURL = URL(fileURLWithPath: resolvedRecord.localPath)
|
||||
updateAutoRetouchRecord(
|
||||
id: photoId,
|
||||
state: .processing,
|
||||
templateId: preset.id,
|
||||
materialId: materialId,
|
||||
status: .uploading,
|
||||
error: nil
|
||||
)
|
||||
aiEditResultStore.startProcessing(materialIDs: [materialId])
|
||||
|
||||
do {
|
||||
let resultData = try await autoRetouchProcessor.process(sourceURL: sourceURL, preset: preset)
|
||||
guard persistedRecordsById[photoId] != nil else {
|
||||
aiEditResultStore.remove(materialID: materialId)
|
||||
return
|
||||
}
|
||||
let resultURL = try storage.writeRetouchedImage(
|
||||
resultData,
|
||||
filename: sourceRecord.fileName,
|
||||
albumId: albumId
|
||||
)
|
||||
guard aiEditResultStore.completeFromImageData(
|
||||
materialID: materialId,
|
||||
presetID: preset.id,
|
||||
editedImageData: resultData
|
||||
) else {
|
||||
throw TravelAlbumAutoRetouchError.invalidImageData
|
||||
}
|
||||
updateAutoRetouchRecord(
|
||||
id: photoId,
|
||||
state: .completed,
|
||||
templateId: preset.id,
|
||||
materialId: materialId,
|
||||
status: .uploaded,
|
||||
error: nil,
|
||||
retouchedPath: storage.relativePath(for: resultURL, albumId: albumId)
|
||||
)
|
||||
} catch {
|
||||
aiEditResultStore.failProcessing(materialID: materialId)
|
||||
updateAutoRetouchRecord(
|
||||
id: photoId,
|
||||
state: .failed,
|
||||
templateId: preset.id,
|
||||
materialId: materialId,
|
||||
status: .uploaded,
|
||||
error: error.localizedDescription
|
||||
)
|
||||
showMessage("修图失败,可点击更多重试")
|
||||
}
|
||||
}
|
||||
|
||||
private func localRecordForUpload(id: String) async throws -> TravelAlbumOTGPhotoRecord {
|
||||
if let record = persistedRecordsById[id],
|
||||
!record.localPath.isEmpty,
|
||||
@@ -623,7 +769,8 @@ final class WiredCameraTransferViewModel {
|
||||
status: TravelAlbumOTGUploadStatus,
|
||||
progress: Int,
|
||||
error: String?,
|
||||
remoteUrl: String? = nil
|
||||
remoteUrl: String? = nil,
|
||||
materialId: Int? = nil
|
||||
) {
|
||||
var record = persistedRecordsById[id]
|
||||
if record == nil, let item = photos.first(where: { $0.id == id }) {
|
||||
@@ -642,7 +789,10 @@ final class WiredCameraTransferViewModel {
|
||||
errorMessage: error,
|
||||
albumId: albumId,
|
||||
userId: appStore.session.userId,
|
||||
remoteUrl: remoteUrl ?? item.remoteUrl
|
||||
remoteUrl: remoteUrl ?? item.remoteUrl,
|
||||
materialId: materialId,
|
||||
autoRetouchState: item.autoRetouchState,
|
||||
autoRetouchTemplateId: item.autoRetouchTemplateId
|
||||
)
|
||||
}
|
||||
guard var record else { return }
|
||||
@@ -652,6 +802,35 @@ final class WiredCameraTransferViewModel {
|
||||
if let remoteUrl {
|
||||
record.remoteUrl = remoteUrl
|
||||
}
|
||||
if let materialId {
|
||||
record.materialId = materialId
|
||||
}
|
||||
record.updatedAt = Int64(Date().timeIntervalSince1970 * 1000)
|
||||
persistedRecordsById[id] = record
|
||||
storage.upsert(record, albumId: albumId)
|
||||
applyMergedPhotos()
|
||||
}
|
||||
|
||||
/// 单独更新修图阶段,保留已完成的原图上传信息。
|
||||
private func updateAutoRetouchRecord(
|
||||
id: String,
|
||||
state: TravelAlbumAutoRetouchState,
|
||||
templateId: String,
|
||||
materialId: Int,
|
||||
status: TravelAlbumOTGUploadStatus,
|
||||
error: String?,
|
||||
retouchedPath: String? = nil
|
||||
) {
|
||||
guard var record = persistedRecordsById[id] else { return }
|
||||
record.status = status
|
||||
record.progress = 100
|
||||
record.errorMessage = error
|
||||
record.materialId = materialId
|
||||
record.autoRetouchState = state
|
||||
record.autoRetouchTemplateId = templateId
|
||||
if let retouchedPath {
|
||||
record.retouchedPath = retouchedPath
|
||||
}
|
||||
record.updatedAt = Int64(Date().timeIntervalSince1970 * 1000)
|
||||
persistedRecordsById[id] = record
|
||||
storage.upsert(record, albumId: albumId)
|
||||
|
||||
Reference in New Issue
Block a user