// // 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") } }