| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520 |
- //
- // LNProfileFeedItemCell.swift
- // Gami
- //
- // Created by OneeChan on 2026/3/2.
- //
- import Foundation
- import UIKit
- import SnapKit
- import AVKit
- class LNProfileFeedItemCell: UITableViewCell {
- private let avatar = UIImageView()
- private let nameLabel = UILabel()
- private let timeLabel = UILabel()
-
- private let photosView = LNMultiLineStackView()
- private let singlePhotoView = UIImageView()
-
- private var videoView = LNVideoPlayerView()
- private let durationLabel = UILabel()
- private let muteButton = UIButton()
-
- private let contentLabel = UILabel()
-
- private let inputField = UITextField()
-
- private let likeView = LNFeedLikeView()
- private let commentView = LNFeedCommentView()
-
- private var curItem: LNFeedItemVO?
-
- var isVideo: Bool {
- videoView.superview?.isHidden == false
- }
-
- override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
- super.init(style: style, reuseIdentifier: reuseIdentifier)
-
- setupViews()
- LNEventDeliver.addObserver(self)
- }
-
- func update(_ item: LNFeedItemVO) {
- avatar.sd_setImage(with: URL(string: item.avatar))
- nameLabel.text = item.nickname
- timeLabel.text = TimeInterval(item.createdAt / 1_000).tencentIMTimeDesc
- contentLabel.text = item.textContent
- likeView.update(id: item.id, liked: item.liked, count: item.likeCount)
- commentView.update(id: item.id, count: item.commentCount)
- videoView.stop()
-
- if item.medias.isEmpty { // 无媒体
- photosView.isHidden = true
- singlePhotoView.superview?.isHidden = true
- videoView.superview?.isHidden = true
- } else if let video = item.medias.first(where: { $0.type == .video }) { // 视频消息
- photosView.isHidden = true
- singlePhotoView.superview?.isHidden = true
-
- videoView.superview?.isHidden = false
- videoView.loadVideo(video.url, coverUrl: video.videoCover)
- if LNFeedManager.videoMute {
- videoView.mute()
- } else {
- videoView.unmute()
- }
- let size = video.videoCover.extractSize
- videoView.snp.remakeConstraints { make in
- make.verticalEdges.equalToSuperview()
- if size.width > size.height {
- // 横屏
- make.horizontalEdges.equalToSuperview()
- make.height.equalTo(videoView.snp.width).multipliedBy(167.0/343.0)
- } else {
- // 竖屏
- make.leading.equalToSuperview()
- make.width.equalToSuperview().multipliedBy(0.5)
- make.height.equalTo(videoView.snp.width).multipliedBy(223.0/167.0)
- }
- }
- } else {
- videoView.superview?.isHidden = true
-
- if item.medias.count == 1 { // 单图
- photosView.isHidden = true
- singlePhotoView.superview?.isHidden = false
- singlePhotoView.sd_setImage(with: URL(string: item.medias[0].url))
- let size = item.medias[0].url.extractSize
- singlePhotoView.snp.remakeConstraints { make in
- make.leading.equalToSuperview()
- make.verticalEdges.equalToSuperview()
- if size.width == size.height {
- // 正方形
- make.width.equalToSuperview().multipliedBy(0.5)
- make.height.equalTo(singlePhotoView.snp.width)
- } else if size.width > size.height {
- // 横屏
- make.width.equalTo(223)
- make.height.equalTo(167)
- } else {
- // 竖屏
- make.width.equalToSuperview().multipliedBy(0.5)
- make.height.equalTo(singlePhotoView.snp.width).multipliedBy(223.0/167.0)
- }
- }
- } else { // 多图
- singlePhotoView.superview?.isHidden = true
-
- photosView.isHidden = false
- if item.medias.map({ $0.url }) != curItem?.medias.map({ $0.url }) {
- if item.medias.count == 2 {
- photosView.columns = 2
- } else {
- photosView.columns = 3
- }
- var itemViews: [UIView] = []
- for (index, media) in item.medias.enumerated() {
- let imageView = buildImageView()
- imageView.sd_setImage(with: URL(string: media.url))
- imageView.isUserInteractionEnabled = true
- imageView.onTap { [weak self] in
- guard let self else { return }
- presentImagePreview(item.medias.map({ $0.url }), index)
- }
- imageView.snp.makeConstraints { make in
- make.height.equalTo(imageView.snp.width)
- }
- itemViews.append(imageView)
- }
- photosView.update(itemViews)
- }
- }
- }
-
- curItem = item
- }
-
- func autoPlayVideoIfNeed() {
- guard videoView.superview?.isHidden == false else { return }
- if videoView.isPlaying { return }
- videoView.start()
- }
-
- func stopPlayVideoIfNeed() {
- guard videoView.superview?.isHidden == false else { return }
- if !videoView.isPlaying { return }
- videoView.stop()
- }
-
- override func didMoveToWindow() {
- super.didMoveToWindow()
- if window == nil {
- stopPlayVideoIfNeed()
- }
- }
-
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
- }
- extension LNProfileFeedItemCell {
- private func toComment() {
- guard let curItem else { return }
- let panel = LNCommonInputPanel()
- panel.maxInput = LNFeedManager.feedCommentMaxInput
- panel.handler = { comment in
- LNFeedManager.shared.sendFeedComment(id: curItem.id, content: comment)
- { success in
- guard success else { return }
- curItem.commentCount += 1
- LNFeedManager.shared.notifyFeedCommentChanged(id: curItem.id, count: curItem.commentCount)
- }
- }
- panel.popup()
- }
- }
- extension LNProfileFeedItemCell: LNVideoPlayerViewDelegate {
- func onVideoProgressChanged(view: LNVideoPlayerView, cur: Float64, total: Float64) {
- let remain = total - cur
- durationLabel.text = TimeInterval(remain).timeCountDisplay
- }
-
- func onVideoDidStop(view: LNVideoPlayerView) {
- durationLabel.text = view.duration.timeCountDisplay
- }
-
- func onVideoDidLoad(view: LNVideoPlayerView) {
- durationLabel.text = videoView.duration.timeCountDisplay
- }
-
- func onVideoMutedChanged(view: LNVideoPlayerView) {
- if videoView.isMuted {
- muteButton.setImage(.icVideoVolumnMute.withTintColor(.white), for: .normal)
- } else {
- muteButton.setImage(.icVideoVolumnNormal.withTintColor(.white), for: .normal)
- }
- }
- }
- extension LNProfileFeedItemCell {
- private func setupViews() {
- let container = UIView()
- container.onTap { [weak self] in
- guard let self else { return }
- guard let curItem else { return }
- if curItem.medias.first(where: { $0.type == .video }) != nil {
- pushToVideoFeedDetail(id: curItem.id)
- } else {
- pushToImageFeedDetail(id: curItem.id)
- }
- }
- contentView.addSubview(container)
- container.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview().inset(16)
- make.top.equalToSuperview()
- make.bottom.equalToSuperview().priority(.low)
- }
-
- let userInfo = buildUserInfo()
- container.addSubview(userInfo)
- userInfo.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.top.equalToSuperview().offset(12)
- }
-
- let mediaView = buildMeiaView()
- container.addSubview(mediaView)
- mediaView.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.top.equalTo(userInfo.snp.bottom).offset(8)
- }
-
- let content = buildTextContent()
- container.addSubview(content)
- content.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.top.equalTo(mediaView.snp.bottom).offset(5)
- }
-
- let menu = buildMenuView()
- container.addSubview(menu)
- menu.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.top.equalTo(content.snp.bottom).offset(18)
- }
-
- let line = UIView()
- line.backgroundColor = .primary_1
- container.addSubview(line)
- line.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.top.equalTo(menu.snp.bottom).offset(16)
- make.bottom.equalToSuperview().offset(-4)
- make.height.equalTo(1)
- }
- }
-
- private func buildUserInfo() -> UIView {
- let container = UIView()
- container.snp.makeConstraints { make in
- make.height.equalTo(35)
- }
-
- 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)
- }
-
- let textContainer = UIView()
- container.addSubview(textContainer)
- textContainer.snp.makeConstraints { make in
- make.centerY.equalToSuperview()
- make.trailing.equalToSuperview()
- make.leading.equalTo(avatar.snp.trailing).offset(10)
- }
-
- nameLabel.text = " "
- nameLabel.font = .heading_h4
- nameLabel.textColor = .text_5
- textContainer.addSubview(nameLabel)
- nameLabel.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.top.equalToSuperview()
- }
-
- timeLabel.text = " "
- timeLabel.font = .body_xs
- timeLabel.textColor = .text_3
- textContainer.addSubview(timeLabel)
- timeLabel.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.bottom.equalToSuperview()
- make.top.equalTo(nameLabel.snp.bottom).offset(4)
- }
-
- return container
- }
-
- private func buildMeiaView() -> UIView {
- let stackView = UIStackView()
- stackView.axis = .vertical
-
- photosView.isHidden = true
- photosView.columns = 2
- photosView.spacing = 6
- photosView.itemSpacing = 6
- photosView.itemDistribution = .fillEqually
- stackView.addArrangedSubview(photosView)
-
- let singleView = UIView()
- singleView.isHidden = true
- stackView.addArrangedSubview(singleView)
-
- singlePhotoView.layer.cornerRadius = 12
- singlePhotoView.clipsToBounds = true
- singlePhotoView.contentMode = .scaleAspectFill
- singlePhotoView.isUserInteractionEnabled = true
- singlePhotoView.onTap { [weak self] in
- guard let self else { return }
- guard let curItem else { return }
- presentImagePreview(curItem.medias.filter({ $0.type == .image }).map({ $0.url }), 0)
- }
- singleView.addSubview(singlePhotoView)
- singlePhotoView.snp.makeConstraints { make in
- make.verticalEdges.equalToSuperview()
- make.leading.equalToSuperview()
- make.width.height.equalTo(0)
- }
-
- stackView.addArrangedSubview(buildVideo())
-
- return stackView
- }
-
- private func buildVideo() -> UIView {
- let container = UIView()
- container.isHidden = true
-
- videoView.layer.cornerRadius = 12
- videoView.clipsToBounds = true
- videoView.delegate = self
- videoView.loop = true
- videoView.isUserInteractionEnabled = false
- videoView.setScaleMode(.resizeAspectFill)
- container.addSubview(videoView)
- videoView.snp.makeConstraints { make in
- make.leading.equalToSuperview()
- make.verticalEdges.equalToSuperview()
- make.width.height.equalTo(0)
- }
-
- let bottomView = UIView()
- container.addSubview(bottomView)
- bottomView.snp.makeConstraints { make in
- make.height.equalTo(36)
- make.horizontalEdges.equalTo(videoView)
- make.bottom.equalTo(videoView)
- }
-
- durationLabel.font = .body_m
- durationLabel.textColor = .text_1
- bottomView.addSubview(durationLabel)
- durationLabel.snp.makeConstraints { make in
- make.leading.equalToSuperview().offset(14)
- make.centerY.equalToSuperview()
- }
-
- muteButton.setImage(.icVideoVolumnNormal.withTintColor(.white), for: .normal)
- muteButton.addAction(UIAction(handler: { [weak self] _ in
- guard let self else { return }
- if videoView.isMuted {
- videoView.unmute()
- LNFeedManager.videoMute = false
- } else {
- videoView.mute()
- LNFeedManager.videoMute = true
- }
- }), for: .touchUpInside)
- bottomView.addSubview(muteButton)
- muteButton.snp.makeConstraints { make in
- make.trailing.equalToSuperview().offset(-14)
- make.centerY.equalToSuperview()
- }
-
- return container
- }
-
- private func buildTextContent() -> UIView {
- contentLabel.font = .body_m
- contentLabel.textColor = .text_5
- contentLabel.numberOfLines = 0
-
- return contentLabel
- }
-
- private func buildMenuView() -> UIView {
- let container = UIView()
- container.snp.makeConstraints { make in
- make.height.equalTo(30)
- }
-
- let comment = buildComment()
- container.addSubview(comment)
- comment.snp.makeConstraints { make in
- make.centerY.equalToSuperview()
- make.trailing.equalToSuperview()
- }
-
- 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()
- make.width.equalTo(165)
- }
-
- return container
- }
-
- private func buildComment() -> UIView {
- commentView.uiColor = .text_4
-
- return commentView
- }
-
- private func buildLike() -> UIView {
- likeView.uiColor = .text_4
-
- return likeView
- }
-
- private func buildInput() -> UIView {
- let container = UIView()
- container.backgroundColor = .fill_2
- container.layer.cornerRadius = 15
- container.onTap { [weak self] in
- guard let self else { return }
- toComment()
- }
- container.snp.makeConstraints { make in
- make.height.equalTo(30)
- }
-
- 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(22)
- }
-
- let tipsLabel = UILabel()
- tipsLabel.font = .body_xs
- 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 buildImageView() -> UIImageView {
- let imageView = UIImageView()
- imageView.layer.cornerRadius = 12
- imageView.clipsToBounds = true
- imageView.contentMode = .scaleAspectFill
-
- return imageView
- }
- }
- #if DEBUG
- import SwiftUI
- struct LNProfileFeedItemCellPreview: UIViewRepresentable {
- func makeUIView(context: Context) -> some UIView {
- let container = UIView()
- container.backgroundColor = .lightGray
-
- let view = LNProfileFeedItemCell()
- container.addSubview(view)
- view.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.centerY.equalToSuperview()
- make.height.equalTo(72)
- }
-
- return container
- }
-
- func updateUIView(_ uiView: UIViewType, context: Context) { }
- }
- #Preview(body: {
- LNProfileFeedItemCellPreview()
- })
- #endif
|