Add album management and trailer upload to Assets module.
Wire home routing for album_list and album_trailer, add a DEBUG home menu preview in Profile, and expand Assets tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -675,6 +675,215 @@ struct MediaLocalUploadFile: Identifiable, Equatable {
|
||||
let height: Int
|
||||
}
|
||||
|
||||
/// 相册文件夹实体,表示相册管理列表中的一个相册。
|
||||
struct AlbumFolderItem: Decodable, Identifiable, Hashable {
|
||||
let id: Int
|
||||
let name: String
|
||||
let coverFileId: Int
|
||||
let countVideo: Int
|
||||
let countImage: Int
|
||||
let coverFile: AlbumFileItem?
|
||||
let createTime: String
|
||||
let remark: String
|
||||
|
||||
/// 相册封面地址,优先使用封面文件的预览地址。
|
||||
var coverURLString: String {
|
||||
coverFile?.previewURLString ?? ""
|
||||
}
|
||||
|
||||
/// 相册内文件总数。
|
||||
var totalCount: Int {
|
||||
countVideo + countImage
|
||||
}
|
||||
|
||||
/// 字段映射,兼容旧接口下划线命名。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case name
|
||||
case coverFileId = "cover_file_id"
|
||||
case countVideo = "count_video"
|
||||
case countImage = "count_image"
|
||||
case coverFile = "cover"
|
||||
case createTime = "created_at"
|
||||
case remark
|
||||
}
|
||||
|
||||
/// 宽松解码相册文件夹字段。
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decodeLossyInt(forKey: .id) ?? 0
|
||||
name = try container.decodeLossyString(forKey: .name)
|
||||
coverFileId = try container.decodeLossyInt(forKey: .coverFileId) ?? 0
|
||||
countVideo = try container.decodeLossyInt(forKey: .countVideo) ?? 0
|
||||
countImage = try container.decodeLossyInt(forKey: .countImage) ?? 0
|
||||
coverFile = try? container.decodeIfPresent(AlbumFileItem.self, forKey: .coverFile)
|
||||
createTime = try container.decodeLossyString(forKey: .createTime)
|
||||
remark = try container.decodeLossyString(forKey: .remark)
|
||||
}
|
||||
}
|
||||
|
||||
/// 相册文件实体,表示相册详情中的图片或视频。
|
||||
struct AlbumFileItem: Decodable, Identifiable, Hashable {
|
||||
let id: Int
|
||||
let fileName: String
|
||||
let fileType: Int
|
||||
let fileUrl: String
|
||||
let coverUrl: String
|
||||
let fileSize: Int64
|
||||
let fileSizeHuman: String
|
||||
let remark: String
|
||||
|
||||
/// 判断当前文件是否为视频。
|
||||
var isVideo: Bool {
|
||||
fileType == 1 || Self.hasVideoExtension(fileName) || Self.hasVideoExtension(fileUrl)
|
||||
}
|
||||
|
||||
/// 判断当前文件是否为图片。
|
||||
var isImage: Bool {
|
||||
fileType == 2 || Self.hasImageExtension(fileName) || Self.hasImageExtension(fileUrl)
|
||||
}
|
||||
|
||||
/// 返回图片或视频封面的预览地址。
|
||||
var previewURLString: String {
|
||||
coverUrl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? fileUrl : coverUrl
|
||||
}
|
||||
|
||||
/// 返回文件大小展示文案。
|
||||
var displayFileSize: String {
|
||||
if !fileSizeHuman.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
return fileSizeHuman
|
||||
}
|
||||
return fileSize.assetsFormattedFileSize
|
||||
}
|
||||
|
||||
/// 根据文件扩展名判断是否为视频。
|
||||
private static func hasVideoExtension(_ value: String) -> Bool {
|
||||
["mp4", "mov", "m4v", "avi"].contains(pathExtension(for: value))
|
||||
}
|
||||
|
||||
/// 根据文件扩展名判断是否为图片。
|
||||
private static func hasImageExtension(_ value: String) -> Bool {
|
||||
["png", "jpg", "jpeg", "heic", "heif", "webp"].contains(pathExtension(for: value))
|
||||
}
|
||||
|
||||
/// 提取 URL 或文件路径中的扩展名。
|
||||
private static func pathExtension(for value: String) -> String {
|
||||
URL(string: value)?.pathExtension.lowercased() ?? URL(fileURLWithPath: value).pathExtension.lowercased()
|
||||
}
|
||||
|
||||
/// 字段映射,兼容旧接口下划线命名。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case fileName = "file_name"
|
||||
case fileType = "file_type"
|
||||
case fileUrl = "file_url"
|
||||
case coverUrl = "cover_url"
|
||||
case fileSize = "file_size"
|
||||
case fileSizeHuman = "file_size_human"
|
||||
case remark
|
||||
}
|
||||
|
||||
/// 宽松解码相册文件字段。
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decodeLossyInt(forKey: .id) ?? 0
|
||||
fileName = try container.decodeLossyString(forKey: .fileName)
|
||||
fileType = try container.decodeLossyInt(forKey: .fileType) ?? 0
|
||||
fileUrl = try container.decodeLossyString(forKey: .fileUrl)
|
||||
coverUrl = try container.decodeLossyString(forKey: .coverUrl)
|
||||
fileSize = try container.decodeLossyInt64(forKey: .fileSize) ?? 0
|
||||
fileSizeHuman = try container.decodeLossyString(forKey: .fileSizeHuman)
|
||||
remark = try container.decodeLossyString(forKey: .remark)
|
||||
}
|
||||
}
|
||||
|
||||
/// 相册文件类型筛选实体,表示相册详情中的图片或视频 Tab。
|
||||
enum AlbumFileTab: Int, CaseIterable, Identifiable {
|
||||
case image = 2
|
||||
case video = 1
|
||||
|
||||
var id: Int { rawValue }
|
||||
|
||||
/// 返回页面展示标题。
|
||||
var title: String {
|
||||
switch self {
|
||||
case .image:
|
||||
"图片"
|
||||
case .video:
|
||||
"视频"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 新建相册请求实体。
|
||||
struct AlbumFolderAddRequest: Encodable, Equatable {
|
||||
let scenicId: String
|
||||
let name: String
|
||||
let remark: String
|
||||
let cloudFolderType: Int
|
||||
|
||||
/// 请求字段映射,兼容旧接口下划线命名。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case scenicId = "scenic_id"
|
||||
case name
|
||||
case remark
|
||||
case cloudFolderType = "cloud_folder_type"
|
||||
}
|
||||
}
|
||||
|
||||
/// 编辑相册请求实体,用于改名、备注和封面。
|
||||
struct AlbumFolderEditRequest: Encodable, Equatable {
|
||||
let folderId: Int
|
||||
let coverFileId: Int?
|
||||
let name: String?
|
||||
let remark: String?
|
||||
|
||||
/// 请求字段映射,兼容旧接口下划线命名。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case folderId = "id"
|
||||
case coverFileId = "cover_file_id"
|
||||
case name
|
||||
case remark
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除相册文件请求实体。
|
||||
struct AlbumFileDeleteRequest: Encodable, Equatable {
|
||||
let idList: [Int]
|
||||
let folderId: Int
|
||||
|
||||
/// 请求字段映射,兼容旧接口下划线命名。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case idList = "id_list"
|
||||
case folderId = "folder_id"
|
||||
}
|
||||
}
|
||||
|
||||
/// 相册本地待上传文件实体,表示 PhotosPicker 读取后的图片或视频。
|
||||
struct AlbumLocalUploadFile: Identifiable, Equatable {
|
||||
let id = UUID()
|
||||
let data: Data
|
||||
let fileName: String
|
||||
let fileType: Int
|
||||
}
|
||||
|
||||
private extension Int64 {
|
||||
/// 返回资产模块通用文件大小展示文案。
|
||||
var assetsFormattedFileSize: String {
|
||||
let value = Double(self)
|
||||
if value >= 1_073_741_824 {
|
||||
return String(format: "%.1fGB", value / 1_073_741_824)
|
||||
}
|
||||
if value >= 1_048_576 {
|
||||
return String(format: "%.1fMB", value / 1_048_576)
|
||||
}
|
||||
if value >= 1024 {
|
||||
return String(format: "%.1fKB", value / 1024)
|
||||
}
|
||||
return "\(self)B"
|
||||
}
|
||||
}
|
||||
|
||||
private extension KeyedDecodingContainer {
|
||||
/// 将 String、数字和 Bool 宽松解码为字符串。
|
||||
func decodeLossyString(forKey key: Key) throws -> String {
|
||||
|
||||
Reference in New Issue
Block a user