LNProfileFeedItemCell.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  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.attributedText = item.textContent.getEmojiString(with: .body_m)
  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.onTap { [weak self] in
  107. guard let self else { return }
  108. presentImagePreview(item.medias.map({ $0.url }), index)
  109. }
  110. imageView.snp.makeConstraints { make in
  111. make.height.equalTo(imageView.snp.width)
  112. }
  113. itemViews.append(imageView)
  114. }
  115. photosView.update(itemViews)
  116. }
  117. }
  118. }
  119. curItem = item
  120. }
  121. func autoPlayVideoIfNeed() {
  122. guard videoView.superview?.isHidden == false else { return }
  123. if videoView.isPlaying { return }
  124. videoView.start()
  125. }
  126. func stopPlayVideoIfNeed() {
  127. guard videoView.superview?.isHidden == false else { return }
  128. if !videoView.isPlaying { return }
  129. videoView.stop()
  130. }
  131. override func didMoveToWindow() {
  132. super.didMoveToWindow()
  133. if window == nil {
  134. stopPlayVideoIfNeed()
  135. }
  136. }
  137. required init?(coder: NSCoder) {
  138. fatalError("init(coder:) has not been implemented")
  139. }
  140. }
  141. extension LNProfileFeedItemCell {
  142. private func toComment() {
  143. guard let curItem else { return }
  144. let panel = LNCommonInputPanel()
  145. panel.maxInput = LNFeedManager.feedCommentMaxInput
  146. panel.handler = { comment in
  147. LNFeedManager.shared.sendFeedComment(id: curItem.id, content: comment)
  148. { success in
  149. guard success else { return }
  150. curItem.commentCount += 1
  151. LNFeedManager.shared.notifyFeedCommentChanged(id: curItem.id, count: curItem.commentCount)
  152. }
  153. }
  154. panel.popup()
  155. }
  156. }
  157. extension LNProfileFeedItemCell: LNVideoPlayerViewDelegate {
  158. func onVideoProgressChanged(view: LNVideoPlayerView, cur: Float64, total: Float64) {
  159. let remain = total - cur
  160. durationLabel.text = TimeInterval(remain).timeCountDisplay
  161. }
  162. func onVideoDidStop(view: LNVideoPlayerView) {
  163. durationLabel.text = view.duration.timeCountDisplay
  164. }
  165. func onVideoDidLoad(view: LNVideoPlayerView) {
  166. durationLabel.text = videoView.duration.timeCountDisplay
  167. }
  168. func onVideoMutedChanged(view: LNVideoPlayerView) {
  169. if videoView.isMuted {
  170. muteButton.setImage(.icVideoVolumnMute.withTintColor(.white), for: .normal)
  171. } else {
  172. muteButton.setImage(.icVideoVolumnNormal.withTintColor(.white), for: .normal)
  173. }
  174. }
  175. }
  176. extension LNProfileFeedItemCell {
  177. private func setupViews() {
  178. let container = UIView()
  179. container.onTap { [weak self] in
  180. guard let self else { return }
  181. guard let curItem else { return }
  182. if curItem.medias.first(where: { $0.type == .video }) != nil {
  183. pushToVideoFeedDetail(id: curItem.id)
  184. } else {
  185. pushToImageFeedDetail(id: curItem.id)
  186. }
  187. }
  188. contentView.addSubview(container)
  189. container.snp.makeConstraints { make in
  190. make.horizontalEdges.equalToSuperview().inset(16)
  191. make.top.equalToSuperview()
  192. make.bottom.equalToSuperview().priority(.low)
  193. }
  194. let userInfo = buildUserInfo()
  195. container.addSubview(userInfo)
  196. userInfo.snp.makeConstraints { make in
  197. make.horizontalEdges.equalToSuperview()
  198. make.top.equalToSuperview().offset(12)
  199. }
  200. let mediaView = buildMeiaView()
  201. container.addSubview(mediaView)
  202. mediaView.snp.makeConstraints { make in
  203. make.horizontalEdges.equalToSuperview()
  204. make.top.equalTo(userInfo.snp.bottom).offset(8)
  205. }
  206. let content = buildTextContent()
  207. container.addSubview(content)
  208. content.snp.makeConstraints { make in
  209. make.horizontalEdges.equalToSuperview()
  210. make.top.equalTo(mediaView.snp.bottom).offset(5)
  211. }
  212. let menu = buildMenuView()
  213. container.addSubview(menu)
  214. menu.snp.makeConstraints { make in
  215. make.horizontalEdges.equalToSuperview()
  216. make.top.equalTo(content.snp.bottom).offset(18)
  217. }
  218. let line = UIView()
  219. line.backgroundColor = .primary_1
  220. container.addSubview(line)
  221. line.snp.makeConstraints { make in
  222. make.horizontalEdges.equalToSuperview()
  223. make.top.equalTo(menu.snp.bottom).offset(16)
  224. make.bottom.equalToSuperview().offset(-4)
  225. make.height.equalTo(1)
  226. }
  227. }
  228. private func buildUserInfo() -> UIView {
  229. let container = UIView()
  230. container.snp.makeConstraints { make in
  231. make.height.equalTo(35)
  232. }
  233. avatar.layer.cornerRadius = 16
  234. avatar.clipsToBounds = true
  235. container.addSubview(avatar)
  236. avatar.snp.makeConstraints { make in
  237. make.leading.equalToSuperview()
  238. make.centerY.equalToSuperview()
  239. make.width.height.equalTo(32)
  240. }
  241. let textContainer = UIView()
  242. container.addSubview(textContainer)
  243. textContainer.snp.makeConstraints { make in
  244. make.centerY.equalToSuperview()
  245. make.trailing.equalToSuperview()
  246. make.leading.equalTo(avatar.snp.trailing).offset(10)
  247. }
  248. nameLabel.text = " "
  249. nameLabel.font = .heading_h4
  250. nameLabel.textColor = .text_5
  251. textContainer.addSubview(nameLabel)
  252. nameLabel.snp.makeConstraints { make in
  253. make.horizontalEdges.equalToSuperview()
  254. make.top.equalToSuperview()
  255. }
  256. timeLabel.text = " "
  257. timeLabel.font = .body_xs
  258. timeLabel.textColor = .text_3
  259. textContainer.addSubview(timeLabel)
  260. timeLabel.snp.makeConstraints { make in
  261. make.horizontalEdges.equalToSuperview()
  262. make.bottom.equalToSuperview()
  263. make.top.equalTo(nameLabel.snp.bottom).offset(4)
  264. }
  265. return container
  266. }
  267. private func buildMeiaView() -> UIView {
  268. let stackView = UIStackView()
  269. stackView.axis = .vertical
  270. photosView.isHidden = true
  271. photosView.columns = 2
  272. photosView.spacing = 6
  273. photosView.itemSpacing = 6
  274. photosView.itemDistribution = .fillEqually
  275. stackView.addArrangedSubview(photosView)
  276. let singleView = UIView()
  277. singleView.isHidden = true
  278. stackView.addArrangedSubview(singleView)
  279. singlePhotoView.layer.cornerRadius = 12
  280. singlePhotoView.clipsToBounds = true
  281. singlePhotoView.contentMode = .scaleAspectFill
  282. singlePhotoView.onTap { [weak self] in
  283. guard let self else { return }
  284. guard let curItem else { return }
  285. presentImagePreview(curItem.medias.filter({ $0.type == .image }).map({ $0.url }), 0)
  286. }
  287. singleView.addSubview(singlePhotoView)
  288. singlePhotoView.snp.makeConstraints { make in
  289. make.verticalEdges.equalToSuperview()
  290. make.leading.equalToSuperview()
  291. make.width.height.equalTo(0)
  292. }
  293. stackView.addArrangedSubview(buildVideo())
  294. return stackView
  295. }
  296. private func buildVideo() -> UIView {
  297. let container = UIView()
  298. container.isHidden = true
  299. videoView.layer.cornerRadius = 12
  300. videoView.clipsToBounds = true
  301. videoView.delegate = self
  302. videoView.loop = true
  303. videoView.isUserInteractionEnabled = false
  304. videoView.setScaleMode(.resizeAspectFill)
  305. container.addSubview(videoView)
  306. videoView.snp.makeConstraints { make in
  307. make.leading.equalToSuperview()
  308. make.verticalEdges.equalToSuperview()
  309. make.width.height.equalTo(0)
  310. }
  311. let bottomView = UIView()
  312. container.addSubview(bottomView)
  313. bottomView.snp.makeConstraints { make in
  314. make.height.equalTo(36)
  315. make.horizontalEdges.equalTo(videoView)
  316. make.bottom.equalTo(videoView)
  317. }
  318. durationLabel.font = .body_m
  319. durationLabel.textColor = .text_1
  320. bottomView.addSubview(durationLabel)
  321. durationLabel.snp.makeConstraints { make in
  322. make.leading.equalToSuperview().offset(14)
  323. make.centerY.equalToSuperview()
  324. }
  325. muteButton.setImage(.icVideoVolumnNormal.withTintColor(.white), for: .normal)
  326. muteButton.addAction(UIAction(handler: { [weak self] _ in
  327. guard let self else { return }
  328. if videoView.isMuted {
  329. videoView.unmute()
  330. LNFeedManager.videoMute = false
  331. } else {
  332. videoView.mute()
  333. LNFeedManager.videoMute = true
  334. }
  335. }), for: .touchUpInside)
  336. bottomView.addSubview(muteButton)
  337. muteButton.snp.makeConstraints { make in
  338. make.trailing.equalToSuperview().offset(-14)
  339. make.centerY.equalToSuperview()
  340. }
  341. return container
  342. }
  343. private func buildTextContent() -> UIView {
  344. contentLabel.font = .body_m
  345. contentLabel.textColor = .text_5
  346. contentLabel.numberOfLines = 0
  347. return contentLabel
  348. }
  349. private func buildMenuView() -> UIView {
  350. let container = UIView()
  351. container.snp.makeConstraints { make in
  352. make.height.equalTo(30)
  353. }
  354. let comment = buildComment()
  355. container.addSubview(comment)
  356. comment.snp.makeConstraints { make in
  357. make.centerY.equalToSuperview()
  358. make.trailing.equalToSuperview()
  359. }
  360. let like = buildLike()
  361. container.addSubview(like)
  362. like.snp.makeConstraints { make in
  363. make.centerY.equalToSuperview()
  364. make.trailing.equalTo(comment.snp.leading).offset(-20)
  365. }
  366. let input = buildInput()
  367. container.addSubview(input)
  368. input.snp.makeConstraints { make in
  369. make.centerY.equalToSuperview()
  370. make.leading.equalToSuperview()
  371. make.width.equalTo(165)
  372. }
  373. return container
  374. }
  375. private func buildComment() -> UIView {
  376. commentView.uiColor = .text_4
  377. return commentView
  378. }
  379. private func buildLike() -> UIView {
  380. likeView.uiColor = .text_4
  381. return likeView
  382. }
  383. private func buildInput() -> UIView {
  384. let container = UIView()
  385. container.backgroundColor = .fill_2
  386. container.layer.cornerRadius = 15
  387. container.onTap { [weak self] in
  388. guard let self else { return }
  389. toComment()
  390. }
  391. container.snp.makeConstraints { make in
  392. make.height.equalTo(30)
  393. }
  394. let editIc = UIImageView()
  395. editIc.image = .icImChatMenuRemark.withTintColor(.text_2)
  396. container.addSubview(editIc)
  397. editIc.snp.makeConstraints { make in
  398. make.leading.equalToSuperview().offset(10)
  399. make.centerY.equalToSuperview()
  400. make.width.height.equalTo(22)
  401. }
  402. let tipsLabel = UILabel()
  403. tipsLabel.font = .body_xs
  404. tipsLabel.textColor = .text_2
  405. tipsLabel.text = .init(key: "A00300")
  406. container.addSubview(tipsLabel)
  407. tipsLabel.snp.makeConstraints { make in
  408. make.centerY.equalToSuperview()
  409. make.leading.equalTo(editIc.snp.trailing).offset(4)
  410. make.trailing.equalToSuperview().offset(-10)
  411. }
  412. return container
  413. }
  414. private func buildImageView() -> UIImageView {
  415. let imageView = UIImageView()
  416. imageView.layer.cornerRadius = 12
  417. imageView.clipsToBounds = true
  418. imageView.contentMode = .scaleAspectFill
  419. return imageView
  420. }
  421. }
  422. #if DEBUG
  423. import SwiftUI
  424. struct LNProfileFeedItemCellPreview: UIViewRepresentable {
  425. func makeUIView(context: Context) -> some UIView {
  426. let container = UIView()
  427. container.backgroundColor = .lightGray
  428. let view = LNProfileFeedItemCell()
  429. container.addSubview(view)
  430. view.snp.makeConstraints { make in
  431. make.horizontalEdges.equalToSuperview()
  432. make.centerY.equalToSuperview()
  433. make.height.equalTo(72)
  434. }
  435. return container
  436. }
  437. func updateUIView(_ uiView: UIViewType, context: Context) { }
  438. }
  439. #Preview(body: {
  440. LNProfileFeedItemCellPreview()
  441. })
  442. #endif