Files
suixinkan_uikit/suixinkan/Features/Live/ViewModels/LiveViewModels.swift

583 lines
19 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// LiveViewModels.swift
// suixinkan
//
import Foundation
/// OSS 便 ViewModel
@MainActor
protocol LiveOSSUploading {
/// OSS URL
func uploadLiveCover(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String
/// OSS URL
func uploadLiveAlbumFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String
}
extension OSSUploadService: LiveOSSUploading {}
/// ViewModel
final class LiveManageViewModel {
private(set) var items: [LiveItem] = []
private(set) var isLoading = false
private(set) var isRefreshing = false
private(set) var canLoadMore = false
var onStateChange: (() -> Void)?
var onShowMessage: ((String) -> Void)?
var onLiveCreated: (() -> Void)?
private let currentScenicIdProvider: () -> Int
private var currentPage = 1
private var totalCount = 0
private let pageSize = 10
/// ViewModel
init(currentScenicIdProvider: @escaping () -> Int = { AppStore.shared.currentScenicId }) {
self.currentScenicIdProvider = currentScenicIdProvider
}
///
func loadInitial(api: any LiveServing) async {
await load(reset: true, showLoading: true, api: api)
}
///
func refresh(api: any LiveServing) async {
isRefreshing = true
notifyStateChange()
await load(reset: true, showLoading: false, api: api)
}
///
func loadMoreIfNeeded(lastVisibleIndex: Int, api: any LiveServing) async {
guard lastVisibleIndex >= items.count - 3 else { return }
await load(reset: false, showLoading: false, api: api)
}
///
func createLive(title: String, coverUrl: String, api: any LiveServing) async {
let name = title.trimmingCharacters(in: .whitespacesAndNewlines)
guard !name.isEmpty else {
onShowMessage?("请输入直播标题")
return
}
guard !coverUrl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
onShowMessage?("请上传封面图片")
return
}
guard let scenicId = scenicIdText() else { return }
isLoading = true
notifyStateChange()
defer {
isLoading = false
notifyStateChange()
}
do {
try await api.createLive(scenicId: scenicId, title: name, coverImg: coverUrl)
onLiveCreated?()
await load(reset: true, showLoading: true, api: api)
} catch is CancellationError {
return
} catch {
onShowMessage?(error.localizedDescription.isEmpty ? "新增直播失败" : error.localizedDescription)
}
}
///
func copyPushURL(_ item: LiveItem) -> String {
item.pushUrl
}
///
func controlLive(_ item: LiveItem, api: any LiveServing) async {
guard item.liveStatus.isOperable else { return }
await performAndReload(api: api) {
if item.liveStatus == .living {
try await api.stopLive(liveId: item.id)
} else if item.liveStatus == .pending || item.liveStatus == .paused {
try await api.startLive(liveId: item.id)
}
}
}
///
func finishLive(_ item: LiveItem, api: any LiveServing) async {
guard item.liveStatus.isOperable else { return }
await performAndReload(api: api) {
try await api.finishLive(liveId: item.id)
}
}
private func load(reset: Bool, showLoading: Bool, api: any LiveServing) async {
guard let scenicId = scenicIdText() else { return }
if reset {
currentPage = 1
canLoadMore = false
} else {
guard canLoadMore, !isLoading else { return }
currentPage += 1
}
isLoading = showLoading
notifyStateChange()
defer {
isLoading = false
isRefreshing = false
notifyStateChange()
}
do {
let response = try await api.liveList(scenicId: scenicId, page: currentPage, pageSize: pageSize)
totalCount = response.total
items = reset ? response.items : items + response.items
canLoadMore = items.count < totalCount
} catch is CancellationError {
return
} catch {
if !reset, currentPage > 1 { currentPage -= 1 }
if reset {
items = []
totalCount = 0
canLoadMore = false
}
onShowMessage?(error.localizedDescription.isEmpty ? "获取直播列表失败" : error.localizedDescription)
}
}
private func performAndReload(api: any LiveServing, operation: () async throws -> Void) async {
isLoading = true
notifyStateChange()
defer {
isLoading = false
notifyStateChange()
}
do {
try await operation()
await load(reset: true, showLoading: true, api: api)
} catch is CancellationError {
return
} catch {
onShowMessage?(error.localizedDescription.isEmpty ? "操作失败" : error.localizedDescription)
}
}
private func scenicIdText() -> String? {
let scenicId = currentScenicIdProvider()
guard scenicId > 0 else {
onShowMessage?("请先选择景区")
return nil
}
return String(scenicId)
}
private func notifyStateChange() {
onStateChange?()
}
}
/// ViewModel
final class LiveDetailViewModel {
private(set) var item: LiveItem?
private(set) var pushMode: LivePushMode = .clarityFirst
private(set) var isLoading = false
var onStateChange: (() -> Void)?
var onShowMessage: ((String) -> Void)?
let liveId: Int
/// ViewModel
init(liveId: Int) {
self.liveId = liveId
}
///
func load(api: any LiveServing) async {
guard liveId > 0 else {
onShowMessage?("直播ID无效")
return
}
isLoading = true
notifyStateChange()
defer {
isLoading = false
notifyStateChange()
}
do {
let detail = try await api.liveDetail(liveId: liveId)
item = detail
pushMode = LivePushMode(rawValue: detail.manualPushMode) ?? .clarityFirst
} catch is CancellationError {
return
} catch {
onShowMessage?(error.localizedDescription.isEmpty ? "获取直播信息失败" : error.localizedDescription)
}
}
///
func changeMode(_ mode: LivePushMode, api: any LiveServing) async {
guard pushMode != mode, item?.liveStatus.isOperable == true else { return }
await performAndReload(api: api) {
try await api.setPushMode(liveId: liveId, mode: mode.rawValue)
}
}
///
func controlLive(api: any LiveServing) async {
guard let item, item.liveStatus.isOperable else { return }
await performAndReload(api: api) {
if item.liveStatus == .living {
try await api.stopLive(liveId: item.id)
} else if item.liveStatus == .pending || item.liveStatus == .paused {
try await api.startLive(liveId: item.id)
}
}
}
///
func finishLive(api: any LiveServing) async {
guard let item, item.liveStatus.isOperable else { return }
await performAndReload(api: api) {
try await api.finishLive(liveId: item.id)
}
}
private func performAndReload(api: any LiveServing, operation: () async throws -> Void) async {
isLoading = true
notifyStateChange()
defer {
isLoading = false
notifyStateChange()
}
do {
try await operation()
await load(api: api)
} catch is CancellationError {
return
} catch {
onShowMessage?(error.localizedDescription.isEmpty ? "操作失败" : error.localizedDescription)
}
}
private func notifyStateChange() {
onStateChange?()
}
}
/// ViewModel
final class LiveAlbumListViewModel {
private(set) var folders: [LiveAlbumFolder] = []
private(set) var startDate: Date?
private(set) var endDate: Date?
private(set) var isLoading = false
private(set) var isRefreshing = false
private(set) var canLoadMore = false
var onStateChange: (() -> Void)?
var onShowMessage: ((String) -> Void)?
private let currentScenicIdProvider: () -> Int
private var currentPage = 1
private var totalCount = 0
private let pageSize = 10
/// ViewModel
init(currentScenicIdProvider: @escaping () -> Int = { AppStore.shared.currentScenicId }) {
self.currentScenicIdProvider = currentScenicIdProvider
}
///
func loadInitial(api: any LiveServing) async {
await load(reset: true, showLoading: true, api: api)
}
///
func refresh(api: any LiveServing) async {
isRefreshing = true
notifyStateChange()
await load(reset: true, showLoading: false, api: api)
}
///
func loadMoreIfNeeded(lastVisibleIndex: Int, api: any LiveServing) async {
guard lastVisibleIndex >= folders.count - 3 else { return }
await load(reset: false, showLoading: false, api: api)
}
///
func setStartDate(_ date: Date, api: any LiveServing) async {
if let endDate, date.startOfDay > endDate.startOfDay {
onShowMessage?("开始时间不能大于结束时间")
return
}
startDate = date.startOfDay
notifyStateChange()
await load(reset: true, showLoading: false, api: api)
}
///
func setEndDate(_ date: Date, api: any LiveServing) async {
if let startDate, date.startOfDay < startDate.startOfDay {
onShowMessage?("结束时间不能小于开始时间")
return
}
endDate = date.startOfDay
notifyStateChange()
await load(reset: true, showLoading: false, api: api)
}
///
func deleteFolder(_ folder: LiveAlbumFolder, api: any LiveServing) async {
isLoading = true
notifyStateChange()
defer {
isLoading = false
notifyStateChange()
}
do {
try await api.deleteAlbumFolder(folderId: folder.id)
await load(reset: true, showLoading: true, api: api)
} catch is CancellationError {
return
} catch {
onShowMessage?(error.localizedDescription.isEmpty ? "删除相册失败" : error.localizedDescription)
}
}
private func load(reset: Bool, showLoading: Bool, api: any LiveServing) async {
guard let scenicId = scenicIdText() else { return }
if reset {
currentPage = 1
canLoadMore = false
} else {
guard canLoadMore, !isLoading else { return }
currentPage += 1
}
isLoading = showLoading
notifyStateChange()
defer {
isLoading = false
isRefreshing = false
notifyStateChange()
}
do {
let response = try await api.albumList(
scenicId: scenicId,
startTime: startDate.map(Self.dateString),
endTime: endDate.map(Self.dateString),
page: currentPage,
pageSize: pageSize
)
totalCount = response.total
folders = reset ? response.items : folders + response.items
canLoadMore = folders.count < totalCount
} catch is CancellationError {
return
} catch {
if !reset, currentPage > 1 { currentPage -= 1 }
if reset {
folders = []
totalCount = 0
canLoadMore = false
}
onShowMessage?(error.localizedDescription.isEmpty ? "获取直播相册失败" : error.localizedDescription)
}
}
private func scenicIdText() -> String? {
let scenicId = currentScenicIdProvider()
guard scenicId > 0 else {
onShowMessage?("请先选择景区")
return nil
}
return String(scenicId)
}
static func dateString(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .gregorian)
formatter.locale = Locale(identifier: "zh_CN")
formatter.dateFormat = "yyyy-MM-dd"
return formatter.string(from: date)
}
private func notifyStateChange() {
onStateChange?()
}
}
/// ViewModel
final class LiveAlbumAddViewModel {
private(set) var title = ""
private(set) var files: [LiveAlbumFile] = []
private(set) var isLoading = false
var onStateChange: (() -> Void)?
var onShowMessage: ((String) -> Void)?
var onCreateSuccess: (() -> Void)?
private let currentScenicIdProvider: () -> Int
/// ViewModel
init(currentScenicIdProvider: @escaping () -> Int = { AppStore.shared.currentScenicId }) {
self.currentScenicIdProvider = currentScenicIdProvider
}
/// 20
func updateTitle(_ title: String) {
self.title = String(title.prefix(20))
notifyStateChange()
}
///
func addLocalMedia(_ media: LivePickedMedia, uploader: any LiveOSSUploading) async {
guard let scenicId = scenicId() else { return }
isLoading = true
notifyStateChange()
defer {
isLoading = false
notifyStateChange()
}
do {
let url = try await uploader.uploadLiveAlbumFile(
data: media.data,
fileName: media.fileName,
fileType: 2,
scenicId: scenicId,
onProgress: { _ in }
)
files.append(LiveAlbumFile(url: url, type: 2, size: media.size, coverImg: ""))
} catch is CancellationError {
return
} catch {
onShowMessage?(error.localizedDescription.isEmpty ? "上传失败" : error.localizedDescription)
}
}
///
func deleteLocalFile(at index: Int) {
guard files.indices.contains(index) else { return }
files.remove(at: index)
notifyStateChange()
}
///
func create(api: any LiveServing) async {
let name = title.trimmingCharacters(in: .whitespacesAndNewlines)
guard !name.isEmpty else {
onShowMessage?("请输入作品名称")
return
}
guard !files.isEmpty else {
onShowMessage?("请上传素材")
return
}
guard let scenicId = scenicId().map(String.init) else { return }
isLoading = true
notifyStateChange()
defer {
isLoading = false
notifyStateChange()
}
do {
try await api.createAlbumFolder(scenicId: scenicId, name: name, items: files)
onCreateSuccess?()
} catch is CancellationError {
return
} catch {
onShowMessage?(error.localizedDescription.isEmpty ? "创建相册失败" : error.localizedDescription)
}
}
private func scenicId() -> Int? {
let scenicId = currentScenicIdProvider()
guard scenicId > 0 else {
onShowMessage?("请先选择景区")
return nil
}
return scenicId
}
private func notifyStateChange() {
onStateChange?()
}
}
/// ViewModel
final class LiveAlbumPreviewViewModel {
private(set) var files: [LiveAlbumFile] = []
private(set) var currentIndex: Int
private(set) var isLoading = false
var onStateChange: (() -> Void)?
var onShowMessage: ((String) -> Void)?
let folderId: Int
/// ViewModel
init(folderId: Int, startIndex: Int) {
self.folderId = folderId
self.currentIndex = max(startIndex, 0)
}
///
func load(api: any LiveServing) async {
guard folderId > 0 else {
onShowMessage?("相册ID无效")
return
}
isLoading = true
notifyStateChange()
defer {
isLoading = false
notifyStateChange()
}
do {
let detail = try await api.albumFolderDetail(folderId: folderId)
files = detail.items
currentIndex = files.isEmpty ? 0 : min(currentIndex, files.count - 1)
} catch is CancellationError {
return
} catch {
onShowMessage?(error.localizedDescription.isEmpty ? "获取相册详情失败" : error.localizedDescription)
}
}
///
func updateCurrentIndex(_ index: Int) {
currentIndex = max(0, min(index, max(files.count - 1, 0)))
notifyStateChange()
}
///
func deleteCurrentFile(api: any LiveServing) async {
guard files.indices.contains(currentIndex) else { return }
let file = files[currentIndex]
isLoading = true
notifyStateChange()
defer {
isLoading = false
notifyStateChange()
}
do {
try await api.deleteAlbumFile(folderId: folderId, fileId: file.id)
currentIndex = 0
await load(api: api)
} catch is CancellationError {
return
} catch {
onShowMessage?(error.localizedDescription.isEmpty ? "删除素材失败" : error.localizedDescription)
}
}
private func notifyStateChange() {
onStateChange?()
}
}
private extension Date {
var startOfDay: Date {
Calendar.current.startOfDay(for: self)
}
}