// // LNIMChatVoiceMessageCell.swift // Lanu // // Created by OneeChan on 2025/12/5. // import Foundation import UIKit import SnapKit class LNIMChatVoiceMessageCell: LNIMChatBaseMessageCell { private let voiceView = UIStackView() private let controlIc = UIImageView() private let waveBg = UIImageView() private let indicator = UIView() private let waveBlack = UIImageView() private let waveProgress = UIView() private let durationLabel = UILabel() private let speedView = UIView() private let speedLabel = UILabel() private var curItem: LNIMMessageData? private var isPlaying: Bool { if let content = curItem?.content, content.lowercased().starts(with: "http") { return LNVoicePlayer.shared.playingUrl == content } guard let playingUrl = LNVoicePlayer.shared.playingUrl else { return false } return playingUrl == curItem?.imMessage.soundElem?.uuid } override func update(_ data: LNIMMessageData, viewModel: LNIMChatViewModel) { super.update(data, viewModel: viewModel) curItem = data if data.imMessage.isSelf { waveBg.image = .icImChatMyVoice waveBlack.image = .icImChatMyVoiceBlack } else { waveBg.image = .icImChatPeerVoice waveBlack.image = .icImChatPeerVoiceBlack } if isPlaying { updatePlayUI() } else { updateStopUI() } LNEventDeliver.addObserver(self) } private func updatePlayUI(animated: Bool = false) { guard let curItem else { return } controlIc.image = .icImChatVoicePause let currentTime = LNVoicePlayer.shared.currentTime + 0.25 let duration = LNVoicePlayer.shared.duration durationLabel.text = .init(format: "%02d:%02d", Int(currentTime) / 60, Int(currentTime) % 60) if curItem.imMessage.isSelf { indicator.backgroundColor = .text_3 } else { indicator.backgroundColor = .primary_3 } waveProgress.snp.remakeConstraints { make in make.leading.verticalEdges.equalTo(waveBg) if duration == 0 || currentTime == 0 { make.width.equalTo(0) } else { make.width.equalTo(waveBg).multipliedBy(currentTime / Double(duration)) } } if speedView.superview == nil { voiceView.addArrangedSubview(speedView) } speedLabel.text = "\(Int(LNVoicePlayer.shared.curSpeed))x" if animated { UIView.animate(withDuration: 0.25) { [weak self] in guard let self else { return } waveProgress.superview?.layoutIfNeeded() } } } private func updateStopUI() { guard let curItem else { return } controlIc.image = .icImChatVoicePlay durationLabel.text = .init(format: "%02d:%02d", curItem.voiceDuration / 60, curItem.voiceDuration % 60) if curItem.imMessage.isSelf { indicator.backgroundColor = .text_3 } else { indicator.backgroundColor = .init(hex: "#1789FF") } waveProgress.snp.remakeConstraints { make in make.leading.verticalEdges.equalTo(waveBg) make.width.equalTo(0) } voiceView.removeArrangedSubview(speedView) speedView.removeFromSuperview() } override func setupViews() { LNEventDeliver.addObserver(self) super.setupViews() voiceView.axis = .horizontal voiceView.spacing = 8 container.addSubview(voiceView) voiceView.snp.makeConstraints { make in make.edges.equalToSuperview() } controlIc.isUserInteractionEnabled = false voiceView.addArrangedSubview(controlIc) voiceView.addArrangedSubview(waveBg) waveProgress.clipsToBounds = true waveBg.addSubview(waveProgress) waveProgress.snp.makeConstraints { make in make.leading.verticalEdges.equalToSuperview() make.width.equalTo(0) } waveProgress.addSubview(waveBlack) waveBlack.snp.makeConstraints { make in make.leading.verticalEdges.equalToSuperview() } indicator.backgroundColor = .text_3 indicator.layer.cornerRadius = 5 waveBg.addSubview(indicator) indicator.snp.makeConstraints { make in make.centerX.equalTo(waveProgress.snp.trailing) make.centerY.equalToSuperview() make.width.height.equalTo(10) } durationLabel.font = .body_xs durationLabel.textColor = .text_3 container.addSubview(durationLabel) durationLabel.snp.makeConstraints { make in make.leading.equalTo(waveBg) make.top.equalTo(container.snp.bottom).offset(4) } speedView.backgroundColor = .text_5.withAlphaComponent(0.4) speedView.layer.cornerRadius = 14 speedView.snp.makeConstraints { make in make.height.equalTo(28) } speedLabel.font = .body_m speedLabel.textColor = .text_1 speedView.addSubview(speedLabel) speedLabel.snp.makeConstraints { make in make.center.equalToSuperview() make.leading.greaterThanOrEqualToSuperview().offset(17.5) } speedView.onTap { [weak self] in guard self != nil else { return } if LNVoicePlayer.shared.curSpeed == 1.0 { LNVoicePlayer.shared.setSpeed(speed: 2.0) } else { LNVoicePlayer.shared.setSpeed(speed: 1.0) } } container.onTap { [weak self] in guard let self else { return } guard let curItem else { return } if let content = curItem.content, content.lowercased().starts(with: "http") { if LNVoicePlayer.shared.playingUrl == content { LNVoicePlayer.shared.stop() } else { LNVoicePlayer.shared.play(content) } return } if LNVoicePlayer.shared.playingUrl == curItem.imMessage.soundElem?.uuid { LNVoicePlayer.shared.stop() return } LNVoicePlayer.shared.playVoiceMessage(message: curItem) } } } extension LNIMChatVoiceMessageCell: LNVoicePlayerNotify { func onAudioStartPlay(path: String) { guard isPlaying else { return } updatePlayUI(animated: true) } func onAudioUpdateDuration(path: String, cur: TimeInterval, total: TimeInterval) { guard isPlaying else { return } updatePlayUI(animated: true) } func onAudioStopPlay(path: String) { guard isPlaying else { return } updateStopUI() } }