LNProfileFeedItemCell.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. //
  2. // LNProfileFeedItemCell.swift
  3. // Gami
  4. //
  5. // Created by OneeChan on 2026/3/2.
  6. //
  7. import Foundation
  8. import UIKit
  9. import SnapKit
  10. import AVKit
  11. class LNProfileFeedItemCell: UITableViewCell {
  12. private let avatar = UIImageView()
  13. private let nameLabel = UILabel()
  14. private let timeLabel = UILabel()
  15. private let photosView = LNMultiLineStackView()
  16. private let singlePhotoView = UIImageView()
  17. private var videoView = LNVideoPlayerView()
  18. private let durationLabel = UILabel()
  19. private let muteButton = UIButton()
  20. private let contentLabel = UILabel()
  21. private let inputField = UITextField()
  22. private let likeView = LNFeedLikeView()
  23. private let commentView = LNFeedCommentView()
  24. private var curItem: LNFeedItemVO?
  25. var isVideo: Bool {
  26. videoView.superview?.isHidden == false
  27. }
  28. override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
  29. super.init(style: style, reuseIdentifier: reuseIdentifier)
  30. setupViews()
  31. LNEventDeliver.addObserver(self)
  32. }
  33. func update(_ item: LNFeedItemVO) {
  34. avatar.sd_setImage(with: URL(string: item.avatar))
  35. nameLabel.text = item.nickname
  36. timeLabel.text = TimeInterval(item.createdAt / 1_000).tencentIMTimeDesc
  37. contentLabel.text = item.textContent
  38. likeView.update(id: item.id, liked: item.liked, count: item.likeCount)
  39. commentView.update(id: item.id, count: item.commentCount)
  40. videoView.stop()
  41. if item.medias.isEmpty { // 无媒体
  42. photosView.isHidden = true
  43. singlePhotoView.superview?.isHidden = true
  44. videoView.superview?.isHidden = true
  45. } else if let video = item.medias.first(where: { $0.type == .video }) { // 视频消息
  46. photosView.isHidden = true
  47. singlePhotoView.superview?.isHidden = true
  48. videoView.superview?.isHidden = false
  49. videoView.loadVideo(video.url, coverUrl: video.videoCover)
  50. if LNFeedManager.videoMute {
  51. videoView.mute()
  52. } else {
  53. videoView.unmute()
  54. }
  55. let size = video.videoCover.extractSize
  56. videoView.snp.remakeConstraints { make in
  57. make.verticalEdges.equalToSuperview()
  58. if size.width > size.height {
  59. // 横屏
  60. make.horizontalEdges.equalToSuperview()
  61. make.height.equalTo(videoView.snp.width).multipliedBy(167.0/343.0)
  62. } else {
  63. // 竖屏
  64. make.leading.equalToSuperview()
  65. make.width.equalToSuperview().multipliedBy(0.5)
  66. make.height.equalTo(videoView.snp.width).multipliedBy(223.0/167.0)
  67. }
  68. }
  69. } else {
  70. videoView.superview?.isHidden = true
  71. if item.medias.count == 1 { // 单图
  72. photosView.isHidden = true
  73. singlePhotoView.superview?.isHidden = false
  74. singlePhotoView.sd_setImage(with: URL(string: item.medias[0].url))
  75. let size = item.medias[0].url.extractSize
  76. singlePhotoView.snp.remakeConstraints { make in
  77. make.leading.equalToSuperview()
  78. make.verticalEdges.equalToSuperview()
  79. if size.width == size.height {
  80. // 正方形
  81. make.width.equalToSuperview().multipliedBy(0.5)
  82. make.height.equalTo(singlePhotoView.snp.width)
  83. } else if size.width > size.height {
  84. // 横屏
  85. make.width.equalTo(223)
  86. make.height.equalTo(167)
  87. } else {
  88. // 竖屏
  89. make.width.equalToSuperview().multipliedBy(0.5)
  90. make.height.equalTo(singlePhotoView.snp.width).multipliedBy(223.0/167.0)
  91. }
  92. }
  93. } else { // 多图
  94. singlePhotoView.superview?.isHidden = true
  95. photosView.isHidden = false
  96. if item.medias.map({ $0.url }) != curItem?.medias.map({ $0.url }) {
  97. if item.medias.count == 2 {
  98. photosView.columns = 2
  99. } else {
  100. photosView.columns = 3
  101. }
  102. var itemViews: [UIView] = []
  103. for (index, media) in item.medias.enumerated() {
  104. let imageView = buildImageView()
  105. imageView.sd_setImage(with: URL(string: media.url))
  106. imageView.isUserInteractionEnabled = true
  107. imageView.onTap { [weak self] in
  108. guard let self else { return }
  109. presentImagePreview(item.medias.map({ $0.url }), index)
  110. }
  111. imageView.snp.makeConstraints { make in
  112. make.height.equalTo(imageView.snp.width)
  113. }
  114. itemViews.append(imageView)
  115. }
  116. photosView.update(itemViews)
  117. }
  118. }
  119. }
  120. curItem = item
  121. }
  122. func autoPlayVideoIfNeed() {
  123. guard videoView.superview?.isHidden == false else { return }
  124. if videoView.isPlaying { return }
  125. videoView.start()
  126. }
  127. func stopPlayVideoIfNeed() {
  128. guard videoView.superview?.isHidden == false else { return }
  129. if !videoView.isPlaying { return }
  130. videoView.stop()
  131. }
  132. override func didMoveToWindow() {
  133. super.didMoveToWindow()
  134. if window == nil {
  135. stopPlayVideoIfNeed()
  136. }
  137. }
  138. required init?(coder: NSCoder) {
  139. fatalError("init(coder:) has not been implemented")
  140. }
  141. }
  142. extension LNProfileFeedItemCell {
  143. private func toComment() {
  144. guard let curItem else { return }
  145. let panel = LNCommonInputPanel()
  146. panel.maxInput = LNFeedManager.feedCommentMaxInput
  147. panel.handler = { comment in
  148. LNFeedManager.shared.sendFeedComment(id: curItem.id, content: comment)
  149. { success in
  150. guard success else { return }
  151. curItem.commentCount += 1
  152. LNFeedManager.shared.notifyFeedCommentChanged(id: curItem.id, count: curItem.commentCount)
  153. }
  154. }
  155. panel.popup()
  156. }
  157. }
  158. extension LNProfileFeedItemCell: LNVideoPlayerViewDelegate {
  159. func onVideoProgressChanged(view: LNVideoPlayerView, cur: Float64, total: Float64) {
  160. let remain = total - cur
  161. durationLabel.text = TimeInterval(remain).timeCountDisplay
  162. }
  163. func onVideoDidStop(view: LNVideoPlayerView) {
  164. durationLabel.text = view.duration.timeCountDisplay
  165. }
  166. func onVideoDidLoad(view: LNVideoPlayerView) {
  167. durationLabel.text = videoView.duration.timeCountDisplay
  168. }
  169. func onVideoMutedChanged(view: LNVideoPlayerView) {
  170. if videoView.isMuted {
  171. muteButton.setImage(.icVideoVolumnMute.withTintColor(.white), for: .normal)
  172. } else {
  173. muteButton.setImage(.icVideoVolumnNormal.withTintColor(.white), for: .normal)
  174. }
  175. }
  176. }
  177. extension LNProfileFeedItemCell {
  178. private func setupViews() {
  179. let container = UIView()
  180. container.onTap { [weak self] in
  181. guard let self else { return }
  182. guard let curItem else { return }
  183. if curItem.medias.first(where: { $0.type == .video }) != nil {
  184. pushToVideoFeedDetail(id: curItem.id)
  185. } else {
  186. pushToImageFeedDetail(id: curItem.id)
  187. }
  188. }
  189. contentView.addSubview(container)
  190. container.snp.makeConstraints { make in
  191. make.horizontalEdges.equalToSuperview().inset(16)
  192. make.top.equalToSuperview()
  193. make.bottom.equalToSuperview().priority(.low)
  194. }
  195. let userInfo = buildUserInfo()
  196. container.addSubview(userInfo)
  197. userInfo.snp.makeConstraints { make in
  198. make.horizontalEdges.equalToSuperview()
  199. make.top.equalToSuperview().offset(12)
  200. }
  201. let mediaView = buildMeiaView()
  202. container.addSubview(mediaView)
  203. mediaView.snp.makeConstraints { make in
  204. make.horizontalEdges.equalToSuperview()
  205. make.top.equalTo(userInfo.snp.bottom).offset(8)
  206. }
  207. let content = buildTextContent()
  208. container.addSubview(content)
  209. content.snp.makeConstraints { make in
  210. make.horizontalEdges.equalToSuperview()
  211. make.top.equalTo(mediaView.snp.bottom).offset(5)
  212. }
  213. let menu = buildMenuView()
  214. container.addSubview(menu)
  215. menu.snp.makeConstraints { make in
  216. make.horizontalEdges.equalToSuperview()
  217. make.top.equalTo(content.snp.bottom).offset(18)
  218. }
  219. let line = UIView()
  220. line.backgroundColor = .primary_1
  221. container.addSubview(line)
  222. line.snp.makeConstraints { make in
  223. make.horizontalEdges.equalToSuperview()
  224. make.top.equalTo(menu.snp.bottom).offset(16)
  225. make.bottom.equalToSuperview().offset(-4)
  226. make.height.equalTo(1)
  227. }
  228. }
  229. private func buildUserInfo() -> UIView {
  230. let container = UIView()
  231. container.snp.makeConstraints { make in
  232. make.height.equalTo(35)
  233. }
  234. avatar.layer.cornerRadius = 16
  235. avatar.clipsToBounds = true
  236. container.addSubview(avatar)
  237. avatar.snp.makeConstraints { make in
  238. make.leading.equalToSuperview()
  239. make.centerY.equalToSuperview()
  240. make.width.height.equalTo(32)
  241. }
  242. let textContainer = UIView()
  243. container.addSubview(textContainer)
  244. textContainer.snp.makeConstraints { make in
  245. make.centerY.equalToSuperview()
  246. make.trailing.equalToSuperview()
  247. make.leading.equalTo(avatar.snp.trailing).offset(10)
  248. }
  249. nameLabel.text = " "
  250. nameLabel.font = .heading_h4
  251. nameLabel.textColor = .text_5
  252. textContainer.addSubview(nameLabel)
  253. nameLabel.snp.makeConstraints { make in
  254. make.horizontalEdges.equalToSuperview()
  255. make.top.equalToSuperview()
  256. }
  257. timeLabel.text = " "
  258. timeLabel.font = .body_xs
  259. timeLabel.textColor = .text_3
  260. textContainer.addSubview(timeLabel)
  261. timeLabel.snp.makeConstraints { make in
  262. make.horizontalEdges.equalToSuperview()
  263. make.bottom.equalToSuperview()
  264. make.top.equalTo(nameLabel.snp.bottom).offset(4)
  265. }
  266. return container
  267. }
  268. private func buildMeiaView() -> UIView {
  269. let stackView = UIStackView()
  270. stackView.axis = .vertical
  271. photosView.isHidden = true
  272. photosView.columns = 2
  273. photosView.spacing = 6
  274. photosView.itemSpacing = 6
  275. photosView.itemDistribution = .fillEqually
  276. stackView.addArrangedSubview(photosView)
  277. let singleView = UIView()
  278. singleView.isHidden = true
  279. stackView.addArrangedSubview(singleView)
  280. singlePhotoView.layer.cornerRadius = 12
  281. singlePhotoView.clipsToBounds = true
  282. singlePhotoView.contentMode = .scaleAspectFill
  283. singlePhotoView.isUserInteractionEnabled = true
  284. singlePhotoView.onTap { [weak self] in
  285. guard let self else { return }
  286. guard let curItem else { return }
  287. presentImagePreview(curItem.medias.filter({ $0.type == .image }).map({ $0.url }), 0)
  288. }
  289. singleView.addSubview(singlePhotoView)
  290. singlePhotoView.snp.makeConstraints { make in
  291. make.verticalEdges.equalToSuperview()
  292. make.leading.equalToSuperview()
  293. make.width.height.equalTo(0)
  294. }
  295. stackView.addArrangedSubview(buildVideo())
  296. return stackView
  297. }
  298. private func buildVideo() -> UIView {
  299. let container = UIView()
  300. container.isHidden = true
  301. videoView.layer.cornerRadius = 12
  302. videoView.clipsToBounds = true
  303. videoView.delegate = self
  304. videoView.loop = true
  305. videoView.isUserInteractionEnabled = false
  306. videoView.setScaleMode(.resizeAspectFill)
  307. container.addSubview(videoView)
  308. videoView.snp.makeConstraints { make in
  309. make.leading.equalToSuperview()
  310. make.verticalEdges.equalToSuperview()
  311. make.width.height.equalTo(0)
  312. }
  313. let bottomView = UIView()
  314. container.addSubview(bottomView)
  315. bottomView.snp.makeConstraints { make in
  316. make.height.equalTo(36)
  317. make.horizontalEdges.equalTo(videoView)
  318. make.bottom.equalTo(videoView)
  319. }
  320. durationLabel.font = .body_m
  321. durationLabel.textColor = .text_1
  322. bottomView.addSubview(durationLabel)
  323. durationLabel.snp.makeConstraints { make in
  324. make.leading.equalToSuperview().offset(14)
  325. make.centerY.equalToSuperview()
  326. }
  327. muteButton.setImage(.icVideoVolumnNormal.withTintColor(.white), for: .normal)
  328. muteButton.addAction(UIAction(handler: { [weak self] _ in
  329. guard let self else { return }
  330. if videoView.isMuted {
  331. videoView.unmute()
  332. LNFeedManager.videoMute = false
  333. } else {
  334. videoView.mute()
  335. LNFeedManager.videoMute = true
  336. }
  337. }), for: .touchUpInside)
  338. bottomView.addSubview(muteButton)
  339. muteButton.snp.makeConstraints { make in
  340. make.trailing.equalToSuperview().offset(-14)
  341. make.centerY.equalToSuperview()
  342. }
  343. return container
  344. }
  345. private func buildTextContent() -> UIView {
  346. contentLabel.font = .body_m
  347. contentLabel.textColor = .text_5
  348. contentLabel.numberOfLines = 0
  349. return contentLabel
  350. }
  351. private func buildMenuView() -> UIView {
  352. let container = UIView()
  353. container.snp.makeConstraints { make in
  354. make.height.equalTo(30)
  355. }
  356. let comment = buildComment()
  357. container.addSubview(comment)
  358. comment.snp.makeConstraints { make in
  359. make.centerY.equalToSuperview()
  360. make.trailing.equalToSuperview()
  361. }
  362. let like = buildLike()
  363. container.addSubview(like)
  364. like.snp.makeConstraints { make in
  365. make.centerY.equalToSuperview()
  366. make.trailing.equalTo(comment.snp.leading).offset(-20)
  367. }
  368. let input = buildInput()
  369. container.addSubview(input)
  370. input.snp.makeConstraints { make in
  371. make.centerY.equalToSuperview()
  372. make.leading.equalToSuperview()
  373. make.width.equalTo(165)
  374. }
  375. return container
  376. }
  377. private func buildComment() -> UIView {
  378. commentView.uiColor = .text_4
  379. return commentView
  380. }
  381. private func buildLike() -> UIView {
  382. likeView.uiColor = .text_4
  383. return likeView
  384. }
  385. private func buildInput() -> UIView {
  386. let container = UIView()
  387. container.backgroundColor = .fill_2
  388. container.layer.cornerRadius = 15
  389. container.onTap { [weak self] in
  390. guard let self else { return }
  391. toComment()
  392. }
  393. container.snp.makeConstraints { make in
  394. make.height.equalTo(30)
  395. }
  396. let editIc = UIImageView()
  397. editIc.image = .icImChatMenuRemark.withTintColor(.text_2)
  398. container.addSubview(editIc)
  399. editIc.snp.makeConstraints { make in
  400. make.leading.equalToSuperview().offset(10)
  401. make.centerY.equalToSuperview()
  402. make.width.height.equalTo(22)
  403. }
  404. let tipsLabel = UILabel()
  405. tipsLabel.font = .body_xs
  406. tipsLabel.textColor = .text_2
  407. tipsLabel.text = .init(key: "A00300")
  408. container.addSubview(tipsLabel)
  409. tipsLabel.snp.makeConstraints { make in
  410. make.centerY.equalToSuperview()
  411. make.leading.equalTo(editIc.snp.trailing).offset(4)
  412. make.trailing.equalToSuperview().offset(-10)
  413. }
  414. return container
  415. }
  416. private func buildImageView() -> UIImageView {
  417. let imageView = UIImageView()
  418. imageView.layer.cornerRadius = 12
  419. imageView.clipsToBounds = true
  420. imageView.contentMode = .scaleAspectFill
  421. return imageView
  422. }
  423. }
  424. #if DEBUG
  425. import SwiftUI
  426. struct LNProfileFeedItemCellPreview: UIViewRepresentable {
  427. func makeUIView(context: Context) -> some UIView {
  428. let container = UIView()
  429. container.backgroundColor = .lightGray
  430. let view = LNProfileFeedItemCell()
  431. container.addSubview(view)
  432. view.snp.makeConstraints { make in
  433. make.horizontalEdges.equalToSuperview()
  434. make.centerY.equalToSuperview()
  435. make.height.equalTo(72)
  436. }
  437. return container
  438. }
  439. func updateUIView(_ uiView: UIViewType, context: Context) { }
  440. }
  441. #Preview(body: {
  442. LNProfileFeedItemCellPreview()
  443. })
  444. #endif