Integrate高德 SDK with simulator-safe build flags, add map views for operating area and punch points, refactor main tabs and key feature screens to UIKit with Diffable lists, and document Swift concurrency defaults in AGENTS.md. Co-authored-by: Cursor <cursoragent@cursor.com>
441 lines
13 KiB
Swift
441 lines
13 KiB
Swift
//
|
||
// LiveModels.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/25.
|
||
//
|
||
|
||
import Foundation
|
||
|
||
/// 直播列表响应实体。
|
||
struct LiveListResponse: Decodable {
|
||
let items: [LiveEntity]
|
||
let total: Int
|
||
let page: Int
|
||
let pageSize: Int
|
||
|
||
/// 枚举,定义相关常量或状态。
|
||
enum CodingKeys: String, CodingKey {
|
||
case items
|
||
case total
|
||
case page
|
||
case pageSize = "page_size"
|
||
}
|
||
|
||
/// 初始化实例。
|
||
init(items: [LiveEntity] = [], total: Int = 0, page: Int = 1, pageSize: Int = 10) {
|
||
self.items = items
|
||
self.total = total
|
||
self.page = page
|
||
self.pageSize = pageSize
|
||
}
|
||
|
||
/// 初始化实例。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
items = (try? container.decode([LiveEntity].self, forKey: .items)) ?? []
|
||
total = try container.liveDecodeLossyInt(forKey: .total) ?? 0
|
||
page = try container.liveDecodeLossyInt(forKey: .page) ?? 1
|
||
pageSize = try container.liveDecodeLossyInt(forKey: .pageSize) ?? 10
|
||
}
|
||
}
|
||
|
||
/// 直播实体,表示一场手动直播。
|
||
struct LiveEntity: Decodable, Identifiable, Equatable {
|
||
let id: Int
|
||
let title: String
|
||
let coverImg: String
|
||
let pushUrl: String
|
||
let playUrl: String
|
||
let pullUrl: String
|
||
let hlsUrl: String
|
||
let flvUrl: String
|
||
let liveUrl: 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 playUrl = "play_url"
|
||
case pullUrl = "pull_url"
|
||
case hlsUrl = "hls_url"
|
||
case flvUrl = "flv_url"
|
||
case liveUrl = "live_url"
|
||
case startTime = "start_time"
|
||
case endTime = "end_time"
|
||
case duration
|
||
case status
|
||
case statusLabel = "status_label"
|
||
case manualPushMode = "manaul_push_mode"
|
||
case manualPushState = "manaul_push_state"
|
||
case viewsCount = "views_count"
|
||
}
|
||
|
||
/// 初始化实例。
|
||
init(
|
||
id: Int = 0,
|
||
title: String = "",
|
||
coverImg: String = "",
|
||
pushUrl: String = "",
|
||
playUrl: String = "",
|
||
pullUrl: String = "",
|
||
hlsUrl: String = "",
|
||
flvUrl: String = "",
|
||
liveUrl: 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.playUrl = playUrl
|
||
self.pullUrl = pullUrl
|
||
self.hlsUrl = hlsUrl
|
||
self.flvUrl = flvUrl
|
||
self.liveUrl = liveUrl
|
||
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.liveDecodeLossyInt(forKey: .id) ?? 0
|
||
title = try container.liveDecodeLossyString(forKey: .title)
|
||
coverImg = try container.liveDecodeLossyString(forKey: .coverImg)
|
||
pushUrl = try container.liveDecodeLossyString(forKey: .pushUrl)
|
||
playUrl = try container.liveDecodeLossyString(forKey: .playUrl)
|
||
pullUrl = try container.liveDecodeLossyString(forKey: .pullUrl)
|
||
hlsUrl = try container.liveDecodeLossyString(forKey: .hlsUrl)
|
||
flvUrl = try container.liveDecodeLossyString(forKey: .flvUrl)
|
||
liveUrl = try container.liveDecodeLossyString(forKey: .liveUrl)
|
||
startTime = Int64(try container.liveDecodeLossyInt(forKey: .startTime) ?? 0)
|
||
endTime = Int64(try container.liveDecodeLossyInt(forKey: .endTime) ?? 0)
|
||
duration = Int64(try container.liveDecodeLossyInt(forKey: .duration) ?? 0)
|
||
status = try container.liveDecodeLossyInt(forKey: .status) ?? 0
|
||
statusLabel = try container.liveDecodeLossyString(forKey: .statusLabel)
|
||
manualPushMode = try container.liveDecodeLossyInt(forKey: .manualPushMode) ?? 1
|
||
manualPushState = try container.liveDecodeLossyInt(forKey: .manualPushState) ?? 0
|
||
viewsCount = try container.liveDecodeLossyInt(forKey: .viewsCount) ?? 0
|
||
}
|
||
|
||
var displayStatus: String {
|
||
statusLabel.liveNonEmpty ?? "状态\(status)"
|
||
}
|
||
|
||
var playbackURLCandidates: [String] {
|
||
[playUrl, pullUrl, hlsUrl, liveUrl, flvUrl]
|
||
}
|
||
}
|
||
|
||
/// 创建直播请求实体。
|
||
struct LiveCreateRequest: Encodable, 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, Equatable {
|
||
let liveId: Int
|
||
|
||
/// 枚举,定义相关常量或状态。
|
||
enum CodingKeys: String, CodingKey {
|
||
case liveId = "live_id"
|
||
}
|
||
}
|
||
|
||
/// 直播推流模式切换请求实体。
|
||
struct LivePushModeRequest: Encodable, Equatable {
|
||
let liveId: Int
|
||
let manualPushMode: Int
|
||
|
||
/// 枚举,定义相关常量或状态。
|
||
enum CodingKeys: String, CodingKey {
|
||
case liveId = "live_id"
|
||
case manualPushMode = "manual_push_mode"
|
||
}
|
||
}
|
||
|
||
/// 直播相册列表响应实体。
|
||
struct LiveAlbumFolderListResponse: Decodable {
|
||
let items: [LiveAlbumFolderItem]
|
||
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: [LiveAlbumFolderItem] = [], 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
|
||
}
|
||
|
||
/// 初始化实例。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
items = (try? container.decode([LiveAlbumFolderItem].self, forKey: .items)) ?? []
|
||
page = try container.liveDecodeLossyInt(forKey: .page) ?? 1
|
||
pageSize = try container.liveDecodeLossyInt(forKey: .pageSize) ?? 10
|
||
total = try container.liveDecodeLossyInt(forKey: .total) ?? 0
|
||
totalPages = try container.liveDecodeLossyInt(forKey: .totalPages) ?? 0
|
||
}
|
||
}
|
||
|
||
/// 直播相册文件夹实体。
|
||
struct LiveAlbumFolderItem: Decodable, Identifiable, Equatable {
|
||
let id: Int
|
||
let albumId: Int
|
||
let name: String
|
||
let creator: String
|
||
let items: [LiveAlbumFileItem]
|
||
|
||
/// 枚举,定义相关常量或状态。
|
||
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: [LiveAlbumFileItem] = []) {
|
||
self.id = id
|
||
self.albumId = albumId
|
||
self.name = name
|
||
self.creator = creator
|
||
self.items = items
|
||
}
|
||
|
||
/// 初始化实例。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
id = try container.liveDecodeLossyInt(forKey: .id) ?? 0
|
||
albumId = try container.liveDecodeLossyInt(forKey: .albumId) ?? 0
|
||
name = try container.liveDecodeLossyString(forKey: .name)
|
||
creator = try container.liveDecodeLossyString(forKey: .creator)
|
||
items = (try? container.decode([LiveAlbumFileItem].self, forKey: .items)) ?? []
|
||
}
|
||
}
|
||
|
||
/// 直播相册素材实体。
|
||
struct LiveAlbumFileItem: Decodable, Identifiable, Hashable {
|
||
let id: Int
|
||
let url: String
|
||
let type: Int
|
||
let size: Int64
|
||
let coverImg: String?
|
||
|
||
/// 枚举,定义相关常量或状态。
|
||
enum CodingKeys: String, CodingKey {
|
||
case id
|
||
case url
|
||
case type
|
||
case size
|
||
case coverImg = "cover_img"
|
||
}
|
||
|
||
/// 初始化实例。
|
||
init(id: Int = 0, url: String = "", type: Int = 1, size: Int64 = 0, coverImg: String? = nil) {
|
||
self.id = id
|
||
self.url = url
|
||
self.type = type
|
||
self.size = size
|
||
self.coverImg = coverImg
|
||
}
|
||
|
||
/// 初始化实例。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
id = try container.liveDecodeLossyInt(forKey: .id) ?? 0
|
||
url = try container.liveDecodeLossyString(forKey: .url)
|
||
type = try container.liveDecodeLossyInt(forKey: .type) ?? 1
|
||
size = Int64(try container.liveDecodeLossyInt(forKey: .size) ?? 0)
|
||
coverImg = try? container.decodeIfPresent(String.self, forKey: .coverImg)
|
||
}
|
||
|
||
var isVideo: Bool { type == 2 }
|
||
|
||
var previewURL: String {
|
||
if let cover = coverImg?.liveTrimmed, !cover.isEmpty {
|
||
return cover
|
||
}
|
||
return url
|
||
}
|
||
}
|
||
|
||
/// 创建直播相册请求实体。
|
||
struct LiveAlbumCreateFolderRequest: Encodable, Equatable {
|
||
let scenicId: String
|
||
let name: String
|
||
let items: [LiveAlbumCreateFileItem]
|
||
|
||
/// 枚举,定义相关常量或状态。
|
||
enum CodingKeys: String, CodingKey {
|
||
case scenicId = "scenic_id"
|
||
case name
|
||
case items
|
||
}
|
||
}
|
||
|
||
/// 创建直播相册素材请求实体。
|
||
struct LiveAlbumCreateFileItem: Encodable, Equatable {
|
||
let url: String
|
||
let type: Int
|
||
let size: Int64
|
||
let coverImg: String?
|
||
|
||
/// 枚举,定义相关常量或状态。
|
||
enum CodingKeys: String, CodingKey {
|
||
case url
|
||
case type
|
||
case size
|
||
case coverImg = "cover_img"
|
||
}
|
||
}
|
||
|
||
/// 删除直播相册请求实体。
|
||
struct LiveAlbumDeleteFolderRequest: Encodable, Equatable {
|
||
let folderId: Int
|
||
|
||
/// 枚举,定义相关常量或状态。
|
||
enum CodingKeys: String, CodingKey {
|
||
case folderId = "folder_id"
|
||
}
|
||
}
|
||
|
||
/// 删除直播相册素材请求实体。
|
||
struct LiveAlbumDeleteFilesRequest: Encodable, Equatable {
|
||
let folderId: Int
|
||
let fileIds: [Int]
|
||
|
||
/// 枚举,定义相关常量或状态。
|
||
enum CodingKeys: String, CodingKey {
|
||
case folderId = "folder_id"
|
||
case fileIds = "file_ids"
|
||
}
|
||
}
|
||
|
||
/// 本地待上传直播相册素材实体。
|
||
struct LiveAlbumLocalUploadFile: Identifiable, Equatable {
|
||
let id: UUID
|
||
let data: Data
|
||
let fileName: String
|
||
let fileType: Int
|
||
let size: Int64
|
||
var uploadedURL: String?
|
||
|
||
/// 初始化实例。
|
||
init(id: UUID = UUID(), data: Data, fileName: String, fileType: Int, size: Int64? = nil, uploadedURL: String? = nil) {
|
||
self.id = id
|
||
self.data = data
|
||
self.fileName = fileName
|
||
self.fileType = fileType
|
||
self.size = size ?? Int64(data.count)
|
||
self.uploadedURL = uploadedURL
|
||
}
|
||
|
||
var isVideo: Bool { fileType == 2 }
|
||
}
|
||
|
||
private extension KeyedDecodingContainer {
|
||
/// 直播解码宽松字符串相关逻辑。
|
||
func liveDecodeLossyString(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)
|
||
}
|
||
if let value = try? decodeIfPresent(Bool.self, forKey: key) {
|
||
return value ? "1" : "0"
|
||
}
|
||
return ""
|
||
}
|
||
|
||
/// 直播解码宽松整数相关逻辑。
|
||
func liveDecodeLossyInt(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(Double.self, forKey: key) {
|
||
return Int(value)
|
||
}
|
||
if let value = try? decodeIfPresent(String.self, forKey: key) {
|
||
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
if let intValue = Int(trimmed) {
|
||
return intValue
|
||
}
|
||
if let doubleValue = Double(trimmed) {
|
||
return Int(doubleValue)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
}
|
||
|
||
extension String {
|
||
/// 直播模块内部使用的去空白字符串。
|
||
var liveTrimmed: String {
|
||
trimmingCharacters(in: .whitespacesAndNewlines)
|
||
}
|
||
|
||
/// 直播模块内部使用的非空字符串兜底。
|
||
var liveNonEmpty: String? {
|
||
let value = liveTrimmed
|
||
return value.isEmpty ? nil : value
|
||
}
|
||
}
|