// // LNVideoFeedDetailViewController.swift // Gami // // Created by OneeChan on 2026/3/3. // import Foundation import UIKit import SnapKit extension UIView { func pushToVideoFeedDetail(id: String) { let vc = LNVideoFeedDetailViewController() vc.loadDetail(id: id) navigationController?.pushViewController(vc, animated: true) } } class LNVideoFeedDetailViewController: LNViewController { private let avatar = UIImageView() private let nameLabel = UILabel() private let contentLabel = UILabel() private let timeLabel = UILabel() private let seekBar = UISlider() private let inputField = UITextField() private let likeView = LNFeedLikeView() private let commentView = LNFeedCommentView() private let videoPlayer = LNVideoPlayerView() private var curDetail: LNFeedDetailVO? override func viewDidLoad() { super.viewDidLoad() setupViews() LNEventDeliver.addObserver(self) } func loadDetail(id: String) { LNFeedManager.shared.getFeedDetail(id: id) { [weak self] detail in guard let self else { return } guard let detail, let video = detail.medias.first(where: { $0.type == .video }) else { navigationController?.popViewController(animated: true) return } curDetail = detail avatar.sd_setImage(with: URL(string: detail.avatar)) nameLabel.text = detail.nickname likeView.update(id: detail.id, liked: detail.liked, count: detail.likeCount) commentView.update(id: detail.id, count: detail.commentCount) contentLabel.text = detail.textContent timeLabel.text = TimeInterval(detail.createdAt / 1_000).tencentIMTimeDesc videoPlayer.loadVideo(video.url, coverUrl: video.videoCover) } } } extension LNVideoFeedDetailViewController { private func toComment() { guard let curDetail else { return } let panel = LNCommonInputPanel() panel.maxInput = LNFeedManager.feedCommentMaxInput panel.handler = { comment in LNFeedManager.shared.sendFeedComment(id: curDetail.id, content: comment) { success in guard success else { return } curDetail.commentCount += 1 LNFeedManager.shared.notifyFeedCommentChanged(id: curDetail.id, count: curDetail.commentCount) } } panel.popup() } } extension LNVideoFeedDetailViewController { @objc private func sliderTouchBegan() { videoPlayer.pause() } @objc private func sliderTouchEnded() { videoPlayer.seekTo(seekBar.value) { [weak self] finished in guard let self, finished else { return } videoPlayer.start() } } } extension LNVideoFeedDetailViewController: LNVideoPlayerViewDelegate { func onVideoDidLoad(view: LNVideoPlayerView) { seekBar.isUserInteractionEnabled = true seekBar.minimumValue = 0 seekBar.maximumValue = Float(view.duration) seekBar.value = 0 view.start() } func onVideoProgressChanged(view: LNVideoPlayerView, cur: Float64, total: Float64) { seekBar.setValue(Float(cur), animated: true) } } extension LNVideoFeedDetailViewController { private func setupViews() { view.backgroundColor = .black setupNavBar() let bottomMenu = buildMenuView() view.addSubview(bottomMenu) bottomMenu.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview() make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom) } videoPlayer.showCover = false videoPlayer.loop = true videoPlayer.delegate = self videoPlayer.backgroundColor = .clear videoPlayer.playButtonSize = 56 view.addSubview(videoPlayer) videoPlayer.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview() make.top.equalTo(fakeNaviBgView) make.bottom.equalTo(bottomMenu.snp.top) } seekBar.isUserInteractionEnabled = false seekBar.addTarget(self, action: #selector(sliderTouchBegan), for: .touchDown) seekBar.addTarget(self, action: #selector(sliderTouchEnded), for: .touchUpInside) seekBar.addTarget(self, action: #selector(sliderTouchEnded), for: .touchUpOutside) seekBar.minimumTrackTintColor = .text_1 seekBar.setThumbImage(UIImage.image(for: .white, size: 6, cornerRadius: 3), for: .normal) view.addSubview(seekBar) seekBar.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview() make.bottom.equalTo(bottomMenu.snp.top) make.height.equalTo(6) } let content = buildContent() view.addSubview(content) content.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview().inset(16) make.bottom.equalTo(seekBar.snp.top).offset(-16) } } private func setupNavBar() { navigationBarColor = .clear backButton.tintColor = .text_1 let menu = buildButtonMenu() setRightButton(menu) let container = UIView() avatar.layer.cornerRadius = 16 avatar.clipsToBounds = true container.addSubview(avatar) avatar.snp.makeConstraints { make in make.leading.equalToSuperview() make.centerY.equalToSuperview() make.width.height.equalTo(32) } nameLabel.font = .heading_h3 nameLabel.textColor = .text_1 nameLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) container.addSubview(nameLabel) nameLabel.snp.makeConstraints { make in make.leading.equalTo(avatar.snp.trailing).offset(12) make.centerY.equalToSuperview() make.trailing.lessThanOrEqualToSuperview().offset(-37) } setTitleView(container) container.snp.makeConstraints { make in make.width.equalTo(view.bounds.width).priority(.medium) make.height.equalTo(44) } } private func buildButtonMenu() -> UIView { let moreButton = UIButton() moreButton.layer.cornerRadius = 16 moreButton.setImage(.icMore.withRenderingMode(.alwaysTemplate), for: .normal) moreButton.tintColor = .text_2 moreButton.snp.makeConstraints { make in make.width.height.equalTo(32) } moreButton.addAction(UIAction(handler: { [weak self] _ in guard let self else { return } guard let curDetail else { return } LNBottomSheetMenu.showFeedMenu(detail: curDetail, view: view) }), for: .touchUpInside) return moreButton } private func buildMenuView() -> UIView { let container = UIView() container.snp.makeConstraints { make in make.height.equalTo(58) } let comment = buildComment() container.addSubview(comment) comment.snp.makeConstraints { make in make.centerY.equalToSuperview() make.trailing.equalToSuperview().offset(-16) } let like = buildLike() container.addSubview(like) like.snp.makeConstraints { make in make.centerY.equalToSuperview() make.trailing.equalTo(comment.snp.leading).offset(-20) } let input = buildInput() container.addSubview(input) input.snp.makeConstraints { make in make.centerY.equalToSuperview() make.leading.equalToSuperview().offset(16) make.width.equalTo(190) } return container } private func buildComment() -> UIView { commentView.uiColor = .text_1 commentView.onTap { [weak self] in guard let self else { return } guard let curDetail else { return } if curDetail.commentCount == 0 { toComment() } else { let panel = LNFeedCommentListPanel() panel.load(curDetail.id) panel.popup() } } return commentView } private func buildLike() -> UIView { likeView.uiColor = .text_1 return likeView } private func buildInput() -> UIView { let container = UIView() container.backgroundColor = .text_5 container.layer.cornerRadius = 19 container.onTap { [weak self] in guard let self else { return } toComment() } container.snp.makeConstraints { make in make.height.equalTo(38) } let editIc = UIImageView() editIc.image = .icImChatMenuRemark.withTintColor(.text_2) container.addSubview(editIc) editIc.snp.makeConstraints { make in make.leading.equalToSuperview().offset(10) make.centerY.equalToSuperview() make.width.height.equalTo(24) } let tipsLabel = UILabel() tipsLabel.font = .body_m tipsLabel.textColor = .text_2 tipsLabel.text = .init(key: "A00300") container.addSubview(tipsLabel) tipsLabel.snp.makeConstraints { make in make.centerY.equalToSuperview() make.leading.equalTo(editIc.snp.trailing).offset(4) make.trailing.equalToSuperview().offset(-10) } return container } private func buildContent() -> UIView { let container = UIView() contentLabel.font = .body_m contentLabel.textColor = .text_1 contentLabel.numberOfLines = 0 container.addSubview(contentLabel) contentLabel.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview() make.top.equalToSuperview() } timeLabel.font = .body_xs timeLabel.textColor = .text_2 container.addSubview(timeLabel) timeLabel.snp.makeConstraints { make in make.leading.equalToSuperview() make.top.equalTo(contentLabel.snp.bottom).offset(8) make.bottom.equalToSuperview() } return container } }