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:
2026-06-24 09:43:46 +08:00
parent a48eb76ba4
commit 607130ade0
17 changed files with 2018 additions and 26 deletions

View File

@ -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 {