// // 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) } }