// // LiveModels.swift // suixinkan // import Foundation /// 直播状态,对齐 Android `AliveEntity.status`。 enum LiveStatus: Int, Sendable, Equatable { case pending = 1 case living = 2 case paused = 3 case finished = 4 case unknown = 0 /// 是否允许开始、暂停或结束。 var isOperable: Bool { self != .finished } } /// 直播推流模式,对齐 Android `manualPushMode`。 enum LivePushMode: Int, CaseIterable, Sendable, Equatable { case clarityFirst = 1 case smoothFirst = 2 /// 展示文案。 var title: String { switch self { case .clarityFirst: "清晰度优先" case .smoothFirst: "流畅度优先" } } } /// 直播列表与详情实体,对齐 Android `AliveEntity`。 struct LiveItem: Decodable, Sendable, Equatable, Hashable, Identifiable { let id: Int let title: String let coverImg: String let pushUrl: String let startTime: Int64 let endTime: Int64 let duration: Int64 let status: Int let statusLabel: String let manualPushMode: Int let manualPushState: Int let viewsCount: Int enum CodingKeys: String, CodingKey { case id case title case coverImg = "cover_img" case pushUrl = "push_url" case startTime = "start_time" case endTime = "end_time" case duration case status case statusLabel = "status_label" case manualPushMode = "manual_push_mode" case manualPushState = "manual_push_state" case legacyManualPushMode = "manaul_push_mode" case legacyManualPushState = "manaul_push_state" case viewsCount = "views_count" } init( id: Int = 0, title: String = "", coverImg: String = "", pushUrl: String = "", startTime: Int64 = 0, endTime: Int64 = 0, duration: Int64 = 0, status: Int = 0, statusLabel: String = "", manualPushMode: Int = 1, manualPushState: Int = 0, viewsCount: Int = 0 ) { self.id = id self.title = title self.coverImg = coverImg self.pushUrl = pushUrl self.startTime = startTime self.endTime = endTime self.duration = duration self.status = status self.statusLabel = statusLabel self.manualPushMode = manualPushMode self.manualPushState = manualPushState self.viewsCount = viewsCount } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decodeLossyInt(forKey: .id) ?? 0 title = try container.decodeLossyString(forKey: .title) coverImg = try container.decodeLossyString(forKey: .coverImg).unescapedSlashes pushUrl = try container.decodeLossyString(forKey: .pushUrl).unescapedSlashes startTime = try container.decodeLossyInt64(forKey: .startTime) ?? 0 endTime = try container.decodeLossyInt64(forKey: .endTime) ?? 0 duration = try container.decodeLossyInt64(forKey: .duration) ?? 0 status = try container.decodeLossyInt(forKey: .status) ?? 0 statusLabel = try container.decodeLossyString(forKey: .statusLabel) manualPushMode = try container.decodeLossyInt(forKey: .manualPushMode) ?? container.decodeLossyInt(forKey: .legacyManualPushMode) ?? 1 manualPushState = try container.decodeLossyInt(forKey: .manualPushState) ?? container.decodeLossyInt(forKey: .legacyManualPushState) ?? 0 viewsCount = try container.decodeLossyInt(forKey: .viewsCount) ?? 0 } /// 类型化直播状态。 var liveStatus: LiveStatus { LiveStatus(rawValue: status) ?? .unknown } /// 标题兜底。 var displayTitle: String { title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "暂无标题" : title } /// 当前操作按钮标题。 var controlTitle: String { liveStatus == .living ? "暂停" : "开始" } } /// 直播分页响应,对齐 Android `ItemListResponse`。 struct LiveListResponse: Decodable, Sendable, Equatable { let page: Int let pageSize: Int let total: Int let totalPages: Int let items: [LiveItem] enum CodingKeys: String, CodingKey { case page case pageSize = "page_size" case total case totalPages = "total_pages" case items } init(page: Int = 1, pageSize: Int = 10, total: Int = 0, totalPages: Int = 0, items: [LiveItem] = []) { self.page = page self.pageSize = pageSize self.total = total self.totalPages = totalPages self.items = items } } /// 新增直播请求。 struct LiveCreateRequest: Encodable, Sendable, Equatable { let scenicId: String let title: String let coverImg: String enum CodingKeys: String, CodingKey { case scenicId = "scenic_id" case title case coverImg = "cover_img" } } /// 直播控制请求。 struct LiveControlRequest: Encodable, Sendable, Equatable { let liveId: Int enum CodingKeys: String, CodingKey { case liveId = "live_id" } } /// 直播推流模式请求。 struct LivePushModeRequest: Encodable, Sendable, Equatable { let liveId: Int let manualPushMode: Int enum CodingKeys: String, CodingKey { case liveId = "live_id" case manualPushMode = "manual_push_mode" } } /// 直播相册文件实体,对齐 Android `AliveAlbumFileEntity`。 struct LiveAlbumFile: Codable, Sendable, Equatable, Hashable, Identifiable { let url: String let type: Int let size: Int64 let coverImg: String? let id: Int enum CodingKeys: String, CodingKey { case url case type case size case coverImg = "cover_img" case id } init(url: String, type: Int, size: Int64, coverImg: String? = nil, id: Int = 0) { self.url = url.unescapedSlashes self.type = type self.size = size self.coverImg = coverImg?.unescapedSlashes self.id = id } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) url = try container.decodeLossyString(forKey: .url).unescapedSlashes type = try container.decodeLossyInt(forKey: .type) ?? 2 size = try container.decodeLossyInt64(forKey: .size) ?? 0 let cover = try container.decodeIfPresent(String.self, forKey: .coverImg) coverImg = cover?.unescapedSlashes id = try container.decodeLossyInt(forKey: .id) ?? 0 } } /// 直播相册文件夹实体,对齐 Android `AliveAlbumFolderEntity`。 struct LiveAlbumFolder: Decodable, Sendable, Equatable, Hashable, Identifiable { let id: Int let albumId: Int let name: String let creator: String let items: [LiveAlbumFile] enum CodingKeys: String, CodingKey { case id case albumId = "album_id" case name case creator case items } init(id: Int = 0, albumId: Int = 0, name: String = "", creator: String = "", items: [LiveAlbumFile] = []) { self.id = id self.albumId = albumId self.name = name self.creator = creator self.items = items } /// 标题兜底。 var displayName: String { name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "暂无标题" : name } } /// 直播相册列表响应。 struct LiveAlbumFolderListResponse: Decodable, Sendable, Equatable { let items: [LiveAlbumFolder] let page: Int let pageSize: Int let total: Int let totalPages: Int enum CodingKeys: String, CodingKey { case items case page case pageSize = "page_size" case total case totalPages = "total_pages" } init(items: [LiveAlbumFolder] = [], page: Int = 1, pageSize: Int = 10, total: Int = 0, totalPages: Int = 0) { self.items = items self.page = page self.pageSize = pageSize self.total = total self.totalPages = totalPages } } /// 创建直播相册文件夹请求。 struct LiveAlbumFolderCreateRequest: Encodable, Sendable, Equatable { let scenicId: String let name: String let items: [LiveAlbumFile] enum CodingKeys: String, CodingKey { case scenicId = "scenic_id" case name case items } } /// 删除直播相册文件夹请求。 struct LiveAlbumFolderDeleteRequest: Encodable, Sendable, Equatable { let folderId: Int enum CodingKeys: String, CodingKey { case folderId = "folder_id" } } /// 删除直播相册文件请求。 struct LiveAlbumFileDeleteRequest: Encodable, Sendable, Equatable { let folderId: Int let fileIds: [Int] enum CodingKeys: String, CodingKey { case folderId = "folder_id" case fileIds = "file_ids" } } /// 本地待上传直播相册素材。 struct LivePickedMedia: Sendable, Equatable { let data: Data let fileName: String let size: Int64 let width: Int let height: Int } private extension KeyedDecodingContainer { func decodeLossyString(forKey key: Key) throws -> String { if let value = try? decodeIfPresent(String.self, forKey: key) { return value } if let value = try? decodeIfPresent(Int.self, forKey: key) { return String(value) } if let value = try? decodeIfPresent(Int64.self, forKey: key) { return String(value) } if let value = try? decodeIfPresent(Double.self, forKey: key) { return String(value) } return "" } func decodeLossyInt(forKey key: Key) throws -> Int? { if let value = try? decodeIfPresent(Int.self, forKey: key) { return value } if let value = try? decodeIfPresent(Int64.self, forKey: key) { return Int(value) } if let value = try? decodeIfPresent(String.self, forKey: key) { let text = value.trimmingCharacters(in: .whitespacesAndNewlines) if let intValue = Int(text) { return intValue } if let doubleValue = Double(text) { return Int(doubleValue) } } if let value = try? decodeIfPresent(Double.self, forKey: key) { return Int(value) } return nil } func decodeLossyInt64(forKey key: Key) throws -> Int64? { if let value = try? decodeIfPresent(Int64.self, forKey: key) { return value } if let value = try? decodeIfPresent(Int.self, forKey: key) { return Int64(value) } if let value = try? decodeIfPresent(String.self, forKey: key) { let text = value.trimmingCharacters(in: .whitespacesAndNewlines) if let intValue = Int64(text) { return intValue } if let doubleValue = Double(text) { return Int64(doubleValue) } } if let value = try? decodeIfPresent(Double.self, forKey: key) { return Int64(value) } return nil } } private extension String { var unescapedSlashes: String { replacingOccurrences(of: "\\/", with: "/") } }