371 lines
11 KiB
Swift
371 lines
11 KiB
Swift
//
|
||
// 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<AliveEntity>`。
|
||
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: "/")
|
||
}
|
||
}
|