362 lines
14 KiB
Swift
362 lines
14 KiB
Swift
//
|
||
// LiveDetailViewController.swift
|
||
// suixinkan
|
||
//
|
||
|
||
import SnapKit
|
||
import UIKit
|
||
|
||
/// 直播信息页,对齐 Android `AliveDetailsScreen`。
|
||
final class LiveDetailViewController: BaseViewController {
|
||
private let viewModel: LiveDetailViewModel
|
||
private let liveAPI: any LiveServing
|
||
private let scrollView = UIScrollView()
|
||
private let contentStack = UIStackView()
|
||
private let coverImageView = LiveCoverImageView(cornerRadius: 0)
|
||
private let infoCard = LiveInfoCardView()
|
||
private let settingsCard = LiveSettingsCardView()
|
||
private let bottomBar = UIView()
|
||
private let controlButton = LiveActionButton(title: "开始", image: UIImage(systemName: "play.fill"))
|
||
private let finishButton = LiveActionButton(title: "结束", image: UIImage(systemName: "stop.fill"))
|
||
|
||
/// 初始化直播信息页。
|
||
@MainActor
|
||
init(liveId: Int, liveAPI: (any LiveServing)? = nil) {
|
||
viewModel = LiveDetailViewModel(liveId: liveId)
|
||
self.liveAPI = liveAPI ?? NetworkServices.shared.liveAPI
|
||
super.init(nibName: nil, bundle: nil)
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
override func setupNavigationBar() {
|
||
title = "直播信息"
|
||
}
|
||
|
||
override func setupUI() {
|
||
view.backgroundColor = AppColor.pageBackground
|
||
contentStack.axis = .vertical
|
||
contentStack.spacing = 16
|
||
scrollView.backgroundColor = AppColor.pageBackground
|
||
bottomBar.backgroundColor = .white
|
||
finishButton.buttonStyle = .danger
|
||
|
||
view.addSubview(scrollView)
|
||
scrollView.addSubview(contentStack)
|
||
contentStack.addArrangedSubview(coverImageView)
|
||
contentStack.addArrangedSubview(infoCard)
|
||
contentStack.addArrangedSubview(settingsCard)
|
||
view.addSubview(bottomBar)
|
||
bottomBar.addSubview(controlButton)
|
||
bottomBar.addSubview(finishButton)
|
||
}
|
||
|
||
override func setupConstraints() {
|
||
bottomBar.snp.makeConstraints { make in
|
||
make.leading.trailing.bottom.equalToSuperview()
|
||
}
|
||
controlButton.snp.makeConstraints { make in
|
||
make.top.equalToSuperview().offset(16)
|
||
make.leading.equalToSuperview().offset(16)
|
||
make.height.equalTo(48)
|
||
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(16)
|
||
}
|
||
finishButton.snp.makeConstraints { make in
|
||
make.leading.equalTo(controlButton.snp.trailing).offset(8)
|
||
make.trailing.equalToSuperview().inset(16)
|
||
make.top.bottom.width.equalTo(controlButton)
|
||
}
|
||
scrollView.snp.makeConstraints { make in
|
||
make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide)
|
||
make.bottom.equalTo(bottomBar.snp.top)
|
||
}
|
||
contentStack.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview()
|
||
make.width.equalTo(scrollView)
|
||
}
|
||
coverImageView.snp.makeConstraints { make in
|
||
make.height.equalTo(coverImageView.snp.width).multipliedBy(9.0 / 16.0)
|
||
}
|
||
infoCard.snp.makeConstraints { make in
|
||
make.leading.trailing.equalToSuperview().inset(16)
|
||
}
|
||
settingsCard.snp.makeConstraints { make in
|
||
make.leading.trailing.equalToSuperview().inset(16)
|
||
}
|
||
}
|
||
|
||
override func bindActions() {
|
||
viewModel.onStateChange = { [weak self] in
|
||
Task { @MainActor in self?.applyViewModel() }
|
||
}
|
||
viewModel.onShowMessage = { [weak self] message in
|
||
Task { @MainActor in self?.showToast(message) }
|
||
}
|
||
controlButton.addTarget(self, action: #selector(controlTapped), for: .touchUpInside)
|
||
finishButton.addTarget(self, action: #selector(finishTapped), for: .touchUpInside)
|
||
settingsCard.onCopy = { [weak self] text in
|
||
UIPasteboard.general.string = text
|
||
self?.showToast("已复制")
|
||
}
|
||
settingsCard.onModeSelected = { [weak self] mode in
|
||
guard let self else { return }
|
||
Task { await self.viewModel.changeMode(mode, api: self.liveAPI) }
|
||
}
|
||
}
|
||
|
||
override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
Task { await viewModel.load(api: liveAPI) }
|
||
}
|
||
|
||
private func applyViewModel() {
|
||
viewModel.isLoading && viewModel.item == nil ? showLoading() : hideLoading()
|
||
guard let item = viewModel.item else { return }
|
||
coverImageView.setURL(item.coverImg)
|
||
infoCard.apply(item)
|
||
settingsCard.apply(item: item, pushMode: viewModel.pushMode)
|
||
controlButton.setTitle(item.controlTitle, for: .normal)
|
||
controlButton.setImage(UIImage(systemName: item.liveStatus == .living ? "pause.fill" : "play.fill"), for: .normal)
|
||
controlButton.buttonStyle = item.liveStatus == .living ? .warning : .primary
|
||
controlButton.isEnabled = item.liveStatus.isOperable
|
||
finishButton.isEnabled = item.liveStatus.isOperable
|
||
}
|
||
|
||
@objc private func controlTapped() {
|
||
Task { await viewModel.controlLive(api: liveAPI) }
|
||
}
|
||
|
||
@objc private func finishTapped() {
|
||
showAlert(title: "确认结束", message: "是否确认结束该直播?") { [weak self] in
|
||
guard let self else { return }
|
||
Task { await self.viewModel.finishLive(api: self.liveAPI) }
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 直播信息卡片,显示时长和观众数。
|
||
private final class LiveInfoCardView: UIView {
|
||
private let titleLabel = UILabel()
|
||
private let durationRow = LiveIconTextRow(iconName: "clock", text: "")
|
||
private let watchRow = LiveIconTextRow(iconName: "eye", text: "")
|
||
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
setup()
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
/// 应用直播详情。
|
||
func apply(_ item: LiveItem) {
|
||
durationRow.text = "直播时长: \(LiveUIFormatter.duration(item.duration))"
|
||
watchRow.text = "在线观众: \(item.viewsCount)"
|
||
}
|
||
|
||
private func setup() {
|
||
backgroundColor = .white
|
||
layer.cornerRadius = 12
|
||
titleLabel.text = "直播信息"
|
||
titleLabel.font = .systemFont(ofSize: 14, weight: .bold)
|
||
titleLabel.textColor = AppColor.textPrimary
|
||
addSubview(titleLabel)
|
||
addSubview(durationRow)
|
||
addSubview(watchRow)
|
||
titleLabel.snp.makeConstraints { make in
|
||
make.top.leading.trailing.equalToSuperview().inset(16)
|
||
}
|
||
durationRow.snp.makeConstraints { make in
|
||
make.top.equalTo(titleLabel.snp.bottom).offset(6)
|
||
make.leading.trailing.equalToSuperview().inset(16)
|
||
make.height.equalTo(22)
|
||
}
|
||
watchRow.snp.makeConstraints { make in
|
||
make.top.equalTo(durationRow.snp.bottom).offset(4)
|
||
make.leading.trailing.bottom.equalToSuperview().inset(16)
|
||
make.height.equalTo(22)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 直播推流设置卡片,显示推流地址和模式选择。
|
||
private final class LiveSettingsCardView: UIView {
|
||
var onCopy: ((String) -> Void)?
|
||
var onModeSelected: ((LivePushMode) -> Void)?
|
||
|
||
private let titleLabel = UILabel()
|
||
private let addressTitleLabel = UILabel()
|
||
private let addressContainer = UIView()
|
||
private let addressLabel = UILabel()
|
||
private let copyButton = UIButton(type: .system)
|
||
private let modeTitleLabel = UILabel()
|
||
private let clarityButton = UIButton(type: .system)
|
||
private let smoothButton = UIButton(type: .system)
|
||
private var pushURL = ""
|
||
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
setup()
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
/// 应用直播详情与当前推流模式。
|
||
func apply(item: LiveItem, pushMode: LivePushMode) {
|
||
pushURL = item.pushUrl
|
||
addressLabel.text = item.pushUrl
|
||
setMode(pushMode)
|
||
clarityButton.isEnabled = item.liveStatus.isOperable
|
||
smoothButton.isEnabled = item.liveStatus.isOperable
|
||
}
|
||
|
||
private func setup() {
|
||
backgroundColor = .white
|
||
layer.cornerRadius = 12
|
||
titleLabel.text = "推流设置"
|
||
titleLabel.font = .systemFont(ofSize: 14, weight: .bold)
|
||
titleLabel.textColor = AppColor.textPrimary
|
||
addressTitleLabel.text = "推流地址"
|
||
addressTitleLabel.font = .systemFont(ofSize: 14)
|
||
addressTitleLabel.textColor = UIColor(hex: 0x4B5563)
|
||
addressContainer.backgroundColor = AppColor.inputBackground
|
||
addressContainer.layer.borderWidth = 1
|
||
addressContainer.layer.borderColor = AppColor.border.cgColor
|
||
addressContainer.layer.cornerRadius = 4
|
||
addressLabel.font = .systemFont(ofSize: 14)
|
||
addressLabel.textColor = AppColor.textPrimary
|
||
addressLabel.lineBreakMode = .byTruncatingMiddle
|
||
copyButton.setTitle("复制", for: .normal)
|
||
copyButton.titleLabel?.font = .systemFont(ofSize: 14)
|
||
copyButton.backgroundColor = AppColor.primary
|
||
copyButton.setTitleColor(.white, for: .normal)
|
||
copyButton.layer.cornerRadius = 4
|
||
modeTitleLabel.text = "直播模式"
|
||
modeTitleLabel.font = .systemFont(ofSize: 14)
|
||
modeTitleLabel.textColor = UIColor(hex: 0x4B5563)
|
||
[clarityButton, smoothButton].forEach { button in
|
||
button.titleLabel?.font = .systemFont(ofSize: 14)
|
||
button.layer.cornerRadius = 4
|
||
button.contentEdgeInsets = UIEdgeInsets(top: 5, left: 12, bottom: 5, right: 12)
|
||
}
|
||
clarityButton.setTitle(LivePushMode.clarityFirst.title, for: .normal)
|
||
smoothButton.setTitle(LivePushMode.smoothFirst.title, for: .normal)
|
||
|
||
addSubview(titleLabel)
|
||
addSubview(addressTitleLabel)
|
||
addSubview(addressContainer)
|
||
addressContainer.addSubview(addressLabel)
|
||
addSubview(copyButton)
|
||
addSubview(modeTitleLabel)
|
||
addSubview(clarityButton)
|
||
addSubview(smoothButton)
|
||
|
||
titleLabel.snp.makeConstraints { make in
|
||
make.top.leading.trailing.equalToSuperview().inset(16)
|
||
}
|
||
addressTitleLabel.snp.makeConstraints { make in
|
||
make.top.equalTo(titleLabel.snp.bottom).offset(8)
|
||
make.leading.trailing.equalToSuperview().inset(16)
|
||
}
|
||
addressContainer.snp.makeConstraints { make in
|
||
make.top.equalTo(addressTitleLabel.snp.bottom).offset(4)
|
||
make.leading.equalToSuperview().offset(16)
|
||
make.height.equalTo(28)
|
||
}
|
||
addressLabel.snp.makeConstraints { make in
|
||
make.leading.trailing.equalToSuperview().inset(8)
|
||
make.centerY.equalToSuperview()
|
||
}
|
||
copyButton.snp.makeConstraints { make in
|
||
make.leading.equalTo(addressContainer.snp.trailing).offset(4)
|
||
make.trailing.equalToSuperview().inset(16)
|
||
make.top.bottom.equalTo(addressContainer)
|
||
make.width.equalTo(52)
|
||
}
|
||
modeTitleLabel.snp.makeConstraints { make in
|
||
make.top.equalTo(addressContainer.snp.bottom).offset(8)
|
||
make.leading.equalToSuperview().offset(16)
|
||
make.bottom.equalToSuperview().inset(16)
|
||
}
|
||
smoothButton.snp.makeConstraints { make in
|
||
make.trailing.equalToSuperview().inset(16)
|
||
make.centerY.equalTo(modeTitleLabel)
|
||
make.height.equalTo(28)
|
||
}
|
||
clarityButton.snp.makeConstraints { make in
|
||
make.trailing.equalTo(smoothButton.snp.leading).offset(-4)
|
||
make.centerY.height.equalTo(smoothButton)
|
||
}
|
||
|
||
copyButton.addTarget(self, action: #selector(copyTapped), for: .touchUpInside)
|
||
clarityButton.addTarget(self, action: #selector(clarityTapped), for: .touchUpInside)
|
||
smoothButton.addTarget(self, action: #selector(smoothTapped), for: .touchUpInside)
|
||
}
|
||
|
||
private func setMode(_ mode: LivePushMode) {
|
||
updateModeButton(clarityButton, selected: mode == .clarityFirst)
|
||
updateModeButton(smoothButton, selected: mode == .smoothFirst)
|
||
}
|
||
|
||
private func updateModeButton(_ button: UIButton, selected: Bool) {
|
||
button.backgroundColor = selected ? AppColor.primary : AppColor.inputBackground
|
||
button.setTitleColor(selected ? .white : UIColor(hex: 0xB6BECA), for: .normal)
|
||
}
|
||
|
||
@objc private func copyTapped() {
|
||
onCopy?(pushURL)
|
||
}
|
||
|
||
@objc private func clarityTapped() {
|
||
onModeSelected?(.clarityFirst)
|
||
}
|
||
|
||
@objc private func smoothTapped() {
|
||
onModeSelected?(.smoothFirst)
|
||
}
|
||
}
|
||
|
||
/// 图标文字行,用于直播详情信息项。
|
||
private final class LiveIconTextRow: UIView {
|
||
var text: String = "" {
|
||
didSet { label.text = text }
|
||
}
|
||
|
||
private let iconView = UIImageView()
|
||
private let label = UILabel()
|
||
|
||
/// 初始化图标文字行。
|
||
init(iconName: String, text: String) {
|
||
super.init(frame: .zero)
|
||
iconView.image = UIImage(systemName: iconName)
|
||
iconView.tintColor = AppColor.primary
|
||
iconView.contentMode = .scaleAspectFit
|
||
label.text = text
|
||
label.font = .systemFont(ofSize: 14)
|
||
label.textColor = UIColor(hex: 0x4B5563)
|
||
addSubview(iconView)
|
||
addSubview(label)
|
||
iconView.snp.makeConstraints { make in
|
||
make.leading.centerY.equalToSuperview()
|
||
make.size.equalTo(16)
|
||
}
|
||
label.snp.makeConstraints { make in
|
||
make.leading.equalTo(iconView.snp.trailing).offset(8)
|
||
make.trailing.centerY.equalToSuperview()
|
||
}
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
}
|