新增ai修图

This commit is contained in:
XHYourName
2026-07-31 16:48:28 +08:00
parent 7ccbc5568d
commit 6129a1632f
42 changed files with 3696 additions and 54 deletions

View File

@ -116,6 +116,8 @@ struct TravelAlbumAvailableOrder: Decodable, Sendable, Equatable, Hashable {
/// Android `TravelAlbumMaterialEntity`
struct TravelAlbumMaterial: Decodable, Sendable, Equatable, Hashable, Identifiable {
private static let bundledImageURLPrefix = "asset://"
let id: Int
let userEquityTravelId: Int
let status: Int
@ -175,6 +177,18 @@ struct TravelAlbumMaterial: Decodable, Sendable, Equatable, Hashable, Identifiab
self.createdAt = createdAt
self.updatedAt = updatedAt
}
/// 使
static func bundledImageURL(assetName: String) -> String {
bundledImageURLPrefix + assetName
}
/// `nil`
var bundledImageAssetName: String? {
guard fileUrl.hasPrefix(Self.bundledImageURLPrefix) else { return nil }
let assetName = String(fileUrl.dropFirst(Self.bundledImageURLPrefix.count))
return assetName.isEmpty ? nil : assetName
}
}
/// Android `TravelAlbumCreateRequest`

View File

@ -0,0 +1,316 @@
//
// TravelAlbumPhotoPreviewModels.swift
// suixinkan
//
import Foundation
/// AI
struct TravelAlbumAIEditResultState: Equatable, Sendable {
/// AI
enum ThumbnailStatus: Equatable, Sendable {
case none
case processing
case edited
case cover
case failed
}
///
enum DisplayMode: Equatable, Sendable {
case original
case edited
case atmosphere
case cover
}
private(set) var isProcessing = false
private(set) var didFailLastAttempt = false
private(set) var appliedPresetID: String?
private(set) var appliedAtmosphereOptionID: String?
private(set) var appliedCoverTemplateID: String?
private(set) var displayMode: DisplayMode = .original
///
var hasEditedResult: Bool {
appliedPresetID != nil
}
/// AI
var canStartEditing: Bool {
!isProcessing
}
///
var isCover: Bool {
appliedCoverTemplateID != nil
}
///
var hasAtmosphereResult: Bool {
appliedAtmosphereOptionID != nil
}
///
var thumbnailStatus: ThumbnailStatus {
if isProcessing {
return .processing
}
if didFailLastAttempt {
return .failed
}
if isCover {
return .cover
}
return hasEditedResult ? .edited : .none
}
/// 退
mutating func startProcessing() {
guard !isProcessing else { return }
isProcessing = true
didFailLastAttempt = false
}
///
mutating func complete(
presetID: String,
atmosphereOptionID: String? = nil,
coverTemplateID: String? = nil
) {
isProcessing = false
didFailLastAttempt = false
appliedPresetID = presetID
appliedAtmosphereOptionID = atmosphereOptionID
appliedCoverTemplateID = coverTemplateID
displayMode = coverTemplateID == nil ? .edited : .cover
}
///
mutating func failProcessing() {
isProcessing = false
didFailLastAttempt = true
}
///
mutating func restoreOriginal() {
isProcessing = false
didFailLastAttempt = false
appliedPresetID = nil
appliedAtmosphereOptionID = nil
appliedCoverTemplateID = nil
displayMode = .original
}
///
mutating func selectDisplayMode(_ mode: DisplayMode) {
guard hasEditedResult else {
displayMode = .original
return
}
if mode == .atmosphere, !hasAtmosphereResult {
displayMode = .edited
return
}
if mode == .cover, !isCover {
displayMode = .edited
return
}
displayMode = mode
}
}
///
struct TravelAlbumSelectionToolbarState: Equatable, Sendable {
let isSelectionMode: Bool
let selectedCount: Int
///
var showsUpload: Bool {
!isSelectionMode
}
/// AI
var showsSelectionActions: Bool {
isSelectionMode
}
///
var selectionActionsEnabled: Bool {
isSelectionMode && selectedCount > 0
}
}
///
struct TravelAlbumPhotoPreviewState: Equatable, Sendable {
private(set) var materials: [TravelAlbumMaterial]
private(set) var currentIndex: Int
init(materials: [TravelAlbumMaterial], initialIndex: Int) {
self.materials = materials
currentIndex = min(max(initialIndex, 0), max(materials.count - 1, 0))
}
///
var currentMaterial: TravelAlbumMaterial? {
materials.indices.contains(currentIndex) ? materials[currentIndex] : nil
}
///
@discardableResult
mutating func moveNext() -> Bool {
guard currentIndex + 1 < materials.count else { return false }
currentIndex += 1
return true
}
///
@discardableResult
mutating func movePrevious() -> Bool {
guard currentIndex > 0 else { return false }
currentIndex -= 1
return true
}
///
@discardableResult
mutating func removeCurrent() -> Bool {
guard materials.indices.contains(currentIndex) else { return false }
materials.remove(at: currentIndex)
if currentIndex >= materials.count {
currentIndex = max(materials.count - 1, 0)
}
return !materials.isEmpty
}
}
/// AI
struct TravelAlbumEditPreset: Hashable, Sendable, Identifiable {
///
enum Effect: String, Hashable, Sendable {
case original
case portrait
case vintage
case brocade
case distantMountain
case mist
case summer
case rich
}
let id: String
let title: String
let effect: Effect
/// AI
static let defaultOptions: [TravelAlbumEditPreset] = [
TravelAlbumEditPreset(id: "original", title: "还原为原图", effect: .original),
TravelAlbumEditPreset(id: "portrait", title: "写真-简约肖像", effect: .portrait),
TravelAlbumEditPreset(id: "vintage", title: "写真-清冷古风", effect: .vintage),
TravelAlbumEditPreset(id: "brocade", title: "旅拍-锦绣", effect: .brocade),
TravelAlbumEditPreset(id: "distant_mountain", title: "旅拍-远山", effect: .distantMountain),
TravelAlbumEditPreset(id: "mist", title: "旅拍-薄雾", effect: .mist),
TravelAlbumEditPreset(id: "summer", title: "油画-夏日", effect: .summer),
TravelAlbumEditPreset(id: "rich", title: "油画-浓郁", effect: .rich),
]
}
///
struct TravelAlbumAtmosphereEditOption: Hashable, Sendable, Identifiable {
let id: String
let title: String
let subtitle: String
let effect: TravelAlbumEditPreset.Effect
///
static let defaultOptions: [TravelAlbumAtmosphereEditOption] = [
TravelAlbumAtmosphereEditOption(
id: "warm_sun",
title: "日落暖阳",
subtitle: "温暖柔和",
effect: .summer
),
TravelAlbumAtmosphereEditOption(
id: "retro_film",
title: "复古胶片",
subtitle: "怀旧颗粒感",
effect: .vintage
),
TravelAlbumAtmosphereEditOption(
id: "clear_blue",
title: "清透蓝调",
subtitle: "通透轻盈",
effect: .mist
),
TravelAlbumAtmosphereEditOption(
id: "forest_mist",
title: "森系薄雾",
subtitle: "低饱和自然感",
effect: .distantMountain
),
TravelAlbumAtmosphereEditOption(
id: "vivid_story",
title: "浓郁故事",
subtitle: "高饱和电影感",
effect: .rich
),
]
///
static let defaultOption = defaultOptions[0]
}
/// AI 使
struct TravelAlbumCoverTemplate: Hashable, Sendable, Identifiable {
///
enum PreviewStyle: String, Hashable, Sendable {
case travelMemoir
case minimal
case scenicStory
case film
}
///
static let minimumPhotoCount = 4
let id: String
let title: String
let previewStyle: PreviewStyle
/// 0
let quotaCost: Int
///
var isFreeGift: Bool {
quotaCost == 0
}
///
static func shouldShow(for selectedPhotoCount: Int) -> Bool {
selectedPhotoCount >= minimumPhotoCount
}
///
static let defaultOptions: [TravelAlbumCoverTemplate] = [
TravelAlbumCoverTemplate(id: "travel_memoir", title: "旅行纪念册", previewStyle: .travelMemoir, quotaCost: 0),
TravelAlbumCoverTemplate(id: "minimal", title: "简约留白", previewStyle: .minimal, quotaCost: 0),
TravelAlbumCoverTemplate(id: "scenic_story", title: "景区故事", previewStyle: .scenicStory, quotaCost: 0),
TravelAlbumCoverTemplate(id: "film", title: "电影胶片", previewStyle: .film, quotaCost: 0),
]
}
/// AI
struct TravelAlbumAIEditSelection: Equatable, Sendable {
let refinedPreset: TravelAlbumEditPreset
let atmosphereOption: TravelAlbumAtmosphereEditOption?
let coverTemplate: TravelAlbumCoverTemplate?
///
var resultCountPerPhoto: Int {
atmosphereOption == nil ? 1 : 2
}
///
var selectedStyleCount: Int {
1 + (atmosphereOption == nil ? 0 : 1) + (coverTemplate == nil ? 0 : 1)
}
}