LNVideoFeedDetailViewController.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. //
  2. // LNVideoFeedDetailViewController.swift
  3. // Gami
  4. //
  5. // Created by OneeChan on 2026/3/3.
  6. //
  7. import Foundation
  8. import UIKit
  9. import SnapKit
  10. extension UIView {
  11. func pushToVideoFeedDetail(id: String) {
  12. let vc = LNVideoFeedDetailViewController()
  13. vc.loadDetail(id: id)
  14. navigationController?.pushViewController(vc, animated: true)
  15. }
  16. }
  17. class LNVideoFeedDetailViewController: LNViewController {
  18. private let avatar = UIImageView()
  19. private let nameLabel = UILabel()
  20. private let contentLabel = UILabel()
  21. private let timeLabel = UILabel()
  22. private let seekBar = UISlider()
  23. private let inputField = UITextField()
  24. private let likeView = LNFeedLikeView()
  25. private let commentView = LNFeedCommentView()
  26. private let videoPlayer = LNVideoPlayerView()
  27. private var curDetail: LNFeedDetailVO?
  28. override func viewDidLoad() {
  29. super.viewDidLoad()
  30. setupViews()
  31. LNEventDeliver.addObserver(self)
  32. }
  33. func loadDetail(id: String) {
  34. LNFeedManager.shared.getFeedDetail(id: id) { [weak self] detail in
  35. guard let self else { return }
  36. guard let detail, let video = detail.medias.first(where: { $0.type == .video }) else {
  37. navigationController?.popViewController(animated: true)
  38. return
  39. }
  40. curDetail = detail
  41. avatar.sd_setImage(with: URL(string: detail.avatar))
  42. nameLabel.text = detail.nickname
  43. likeView.update(id: detail.id, liked: detail.liked, count: detail.likeCount)
  44. commentView.update(id: detail.id, count: detail.commentCount)
  45. contentLabel.text = detail.textContent
  46. timeLabel.text = TimeInterval(detail.createdAt / 1_000).tencentIMTimeDesc
  47. videoPlayer.loadVideo(video.url, coverUrl: video.videoCover)
  48. }
  49. }
  50. }
  51. extension LNVideoFeedDetailViewController {
  52. private func toComment() {
  53. guard let curDetail else { return }
  54. let panel = LNCommonInputPanel()
  55. panel.maxInput = LNFeedManager.feedCommentMaxInput
  56. panel.handler = { comment in
  57. LNFeedManager.shared.sendFeedComment(id: curDetail.id, content: comment)
  58. { success in
  59. guard success else { return }
  60. curDetail.commentCount += 1
  61. LNFeedManager.shared.notifyFeedCommentChanged(id: curDetail.id, count: curDetail.commentCount)
  62. }
  63. }
  64. panel.popup()
  65. }
  66. }
  67. extension LNVideoFeedDetailViewController {
  68. @objc private func sliderTouchBegan() {
  69. videoPlayer.pause()
  70. }
  71. @objc private func sliderTouchEnded() {
  72. videoPlayer.seekTo(seekBar.value) { [weak self] finished in
  73. guard let self, finished else { return }
  74. videoPlayer.start()
  75. }
  76. }
  77. }
  78. extension LNVideoFeedDetailViewController: LNVideoPlayerViewDelegate {
  79. func onVideoDidLoad(view: LNVideoPlayerView) {
  80. seekBar.isUserInteractionEnabled = true
  81. seekBar.minimumValue = 0
  82. seekBar.maximumValue = Float(view.duration)
  83. seekBar.value = 0
  84. view.start()
  85. }
  86. func onVideoProgressChanged(view: LNVideoPlayerView, cur: Float64, total: Float64) {
  87. seekBar.setValue(Float(cur), animated: true)
  88. }
  89. }
  90. extension LNVideoFeedDetailViewController {
  91. private func setupViews() {
  92. view.backgroundColor = .black
  93. setupNavBar()
  94. let bottomMenu = buildMenuView()
  95. view.addSubview(bottomMenu)
  96. bottomMenu.snp.makeConstraints { make in
  97. make.horizontalEdges.equalToSuperview()
  98. make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom)
  99. }
  100. videoPlayer.showCover = false
  101. videoPlayer.loop = true
  102. videoPlayer.delegate = self
  103. videoPlayer.backgroundColor = .clear
  104. videoPlayer.playButtonSize = 56
  105. view.addSubview(videoPlayer)
  106. videoPlayer.snp.makeConstraints { make in
  107. make.horizontalEdges.equalToSuperview()
  108. make.top.equalTo(fakeNaviBgView)
  109. make.bottom.equalTo(bottomMenu.snp.top)
  110. }
  111. seekBar.isUserInteractionEnabled = false
  112. seekBar.addTarget(self, action: #selector(sliderTouchBegan), for: .touchDown)
  113. seekBar.addTarget(self, action: #selector(sliderTouchEnded), for: .touchUpInside)
  114. seekBar.addTarget(self, action: #selector(sliderTouchEnded), for: .touchUpOutside)
  115. seekBar.minimumTrackTintColor = .text_1
  116. seekBar.setThumbImage(UIImage.image(for: .white, size: 6, cornerRadius: 3), for: .normal)
  117. view.addSubview(seekBar)
  118. seekBar.snp.makeConstraints { make in
  119. make.horizontalEdges.equalToSuperview()
  120. make.bottom.equalTo(bottomMenu.snp.top)
  121. make.height.equalTo(6)
  122. }
  123. let content = buildContent()
  124. view.addSubview(content)
  125. content.snp.makeConstraints { make in
  126. make.horizontalEdges.equalToSuperview().inset(16)
  127. make.bottom.equalTo(seekBar.snp.top).offset(-16)
  128. }
  129. }
  130. private func setupNavBar() {
  131. navigationBarColor = .clear
  132. backButton.tintColor = .text_1
  133. let menu = buildButtonMenu()
  134. setRightButton(menu)
  135. let container = UIView()
  136. avatar.layer.cornerRadius = 16
  137. avatar.clipsToBounds = true
  138. container.addSubview(avatar)
  139. avatar.snp.makeConstraints { make in
  140. make.leading.equalToSuperview()
  141. make.centerY.equalToSuperview()
  142. make.width.height.equalTo(32)
  143. }
  144. nameLabel.font = .heading_h3
  145. nameLabel.textColor = .text_1
  146. nameLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
  147. nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
  148. container.addSubview(nameLabel)
  149. nameLabel.snp.makeConstraints { make in
  150. make.leading.equalTo(avatar.snp.trailing).offset(12)
  151. make.centerY.equalToSuperview()
  152. make.trailing.lessThanOrEqualToSuperview().offset(-37)
  153. }
  154. setTitleView(container)
  155. container.snp.makeConstraints { make in
  156. make.width.equalTo(view.bounds.width).priority(.medium)
  157. make.height.equalTo(44)
  158. }
  159. }
  160. private func buildButtonMenu() -> UIView {
  161. let moreButton = UIButton()
  162. moreButton.layer.cornerRadius = 16
  163. moreButton.setImage(.icMore.withRenderingMode(.alwaysTemplate), for: .normal)
  164. moreButton.tintColor = .text_2
  165. moreButton.snp.makeConstraints { make in
  166. make.width.height.equalTo(32)
  167. }
  168. moreButton.addAction(UIAction(handler: { [weak self] _ in
  169. guard let self else { return }
  170. guard let curDetail else { return }
  171. LNBottomSheetMenu.showFeedMenu(detail: curDetail, view: view)
  172. }), for: .touchUpInside)
  173. return moreButton
  174. }
  175. private func buildMenuView() -> UIView {
  176. let container = UIView()
  177. container.snp.makeConstraints { make in
  178. make.height.equalTo(58)
  179. }
  180. let comment = buildComment()
  181. container.addSubview(comment)
  182. comment.snp.makeConstraints { make in
  183. make.centerY.equalToSuperview()
  184. make.trailing.equalToSuperview().offset(-16)
  185. }
  186. let like = buildLike()
  187. container.addSubview(like)
  188. like.snp.makeConstraints { make in
  189. make.centerY.equalToSuperview()
  190. make.trailing.equalTo(comment.snp.leading).offset(-20)
  191. }
  192. let input = buildInput()
  193. container.addSubview(input)
  194. input.snp.makeConstraints { make in
  195. make.centerY.equalToSuperview()
  196. make.leading.equalToSuperview().offset(16)
  197. make.width.equalTo(190)
  198. }
  199. return container
  200. }
  201. private func buildComment() -> UIView {
  202. commentView.uiColor = .text_1
  203. commentView.onTap { [weak self] in
  204. guard let self else { return }
  205. guard let curDetail else { return }
  206. if curDetail.commentCount == 0 {
  207. toComment()
  208. } else {
  209. let panel = LNFeedCommentListPanel()
  210. panel.load(curDetail.id)
  211. panel.popup()
  212. }
  213. }
  214. return commentView
  215. }
  216. private func buildLike() -> UIView {
  217. likeView.uiColor = .text_1
  218. return likeView
  219. }
  220. private func buildInput() -> UIView {
  221. let container = UIView()
  222. container.backgroundColor = .text_5
  223. container.layer.cornerRadius = 19
  224. container.onTap { [weak self] in
  225. guard let self else { return }
  226. toComment()
  227. }
  228. container.snp.makeConstraints { make in
  229. make.height.equalTo(38)
  230. }
  231. let editIc = UIImageView()
  232. editIc.image = .icImChatMenuRemark.withTintColor(.text_2)
  233. container.addSubview(editIc)
  234. editIc.snp.makeConstraints { make in
  235. make.leading.equalToSuperview().offset(10)
  236. make.centerY.equalToSuperview()
  237. make.width.height.equalTo(24)
  238. }
  239. let tipsLabel = UILabel()
  240. tipsLabel.font = .body_m
  241. tipsLabel.textColor = .text_2
  242. tipsLabel.text = .init(key: "A00300")
  243. container.addSubview(tipsLabel)
  244. tipsLabel.snp.makeConstraints { make in
  245. make.centerY.equalToSuperview()
  246. make.leading.equalTo(editIc.snp.trailing).offset(4)
  247. make.trailing.equalToSuperview().offset(-10)
  248. }
  249. return container
  250. }
  251. private func buildContent() -> UIView {
  252. let container = UIView()
  253. contentLabel.font = .body_m
  254. contentLabel.textColor = .text_1
  255. contentLabel.numberOfLines = 0
  256. container.addSubview(contentLabel)
  257. contentLabel.snp.makeConstraints { make in
  258. make.horizontalEdges.equalToSuperview()
  259. make.top.equalToSuperview()
  260. }
  261. timeLabel.font = .body_xs
  262. timeLabel.textColor = .text_2
  263. container.addSubview(timeLabel)
  264. timeLabel.snp.makeConstraints { make in
  265. make.leading.equalToSuperview()
  266. make.top.equalTo(contentLabel.snp.bottom).offset(8)
  267. make.bottom.equalToSuperview()
  268. }
  269. return container
  270. }
  271. }