// // PigeonMessageHistoryVC.swift // QuickLocation // import UIKit import AVFoundation import RxSwift final class PigeonMessageHistoryVC: BaseViewController { private let groupKey: String private var items: [PigeonHistoryItem] = [] private var rootView: PigeonMessageHistoryView! private var playingItemId: String? private var isVoicePlaying = false private var voiceElapsed: TimeInterval = 0 private var voiceDuration: TimeInterval = 0 private var voiceTimer: Timer? private var audioPlayer: AVAudioPlayer? init(groupKey: String) { self.groupKey = groupKey super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func loadView() { rootView = PigeonMessageHistoryView(frame: UIScreen.main.bounds) view = rootView } override func viewDidLoad() { super.viewDidLoad() rootView.tableView.dataSource = self rootView.tableView.delegate = self rootView.tableView.register(PigeonHistoryCell.self, forCellReuseIdentifier: PigeonHistoryCell.reuseId) loadHistory() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) stopVoicePlayback(reset: true) } @objc private func goBack() { navigationController?.popViewController(animated: true) } private func loadHistory() { DLToast.showLoading() PigeonService.sent(groupKey: groupKey) .subscribe(onNext: { [weak self] response in DLToast.dismiss() guard let self else { return } self.items = response.list.map { $0.toHistoryItem() } self.rootView.tableView.reloadData() }, onError: { _ in DLToast.dismiss() }) .disposed(by: disposeBag) } private func togglePlayback(for item: PigeonHistoryItem) { guard item.kind == .voice else { return } guard item.localAudioData != nil || item.mediaURL != nil else { DLToast.show(text: "无法播放") return } if playingItemId == item.id { if isVoicePlaying { pauseVoicePlayback() } else { startVoicePlayback(item: item) } return } stopVoicePlayback(reset: true) startVoicePlayback(item: item) } private func startVoicePlayback(item: PigeonHistoryItem) { playingItemId = item.id if audioPlayer == nil || playingItemId != item.id { preparePlayer(for: item) return } audioPlayer?.play() isVoicePlaying = true startTimer() refreshVisiblePlayback() } private func preparePlayer(for item: PigeonHistoryItem) { if let data = item.localAudioData { startPlayer(data: data, item: item) return } guard let url = item.mediaURL else { DLToast.show(text: "无法播放") playingItemId = nil return } DLToast.showLoading() URLSession.shared.dataTask(with: url) { [weak self] data, _, error in DispatchQueue.main.async { DLToast.dismiss() guard let self, self.playingItemId == item.id else { return } guard let data, error == nil else { DLToast.show(text: "无法播放") self.playingItemId = nil return } self.startPlayer(data: data, item: item) } }.resume() } private func startPlayer(data: Data, item: PigeonHistoryItem) { guard playingItemId == item.id else { return } do { let player = try AVAudioPlayer(data: data) player.prepareToPlay() audioPlayer = player voiceDuration = max(0.01, player.duration) voiceElapsed = 0 player.play() isVoicePlaying = true startTimer() refreshVisiblePlayback() } catch { DLToast.show(text: "无法播放") playingItemId = nil refreshVisiblePlayback() } } private func startTimer() { voiceTimer?.invalidate() let timer = Timer(timeInterval: 0.05, repeats: true) { [weak self] _ in self?.tickVoicePlayback() } RunLoop.main.add(timer, forMode: .common) voiceTimer = timer } private func pauseVoicePlayback() { isVoicePlaying = false audioPlayer?.pause() voiceTimer?.invalidate() voiceTimer = nil refreshVisiblePlayback() } private func stopVoicePlayback(reset: Bool) { isVoicePlaying = false voiceTimer?.invalidate() voiceTimer = nil audioPlayer?.stop() audioPlayer = nil if reset { voiceElapsed = 0 playingItemId = nil } refreshVisiblePlayback() } private func tickVoicePlayback() { if let player = audioPlayer { voiceElapsed = player.currentTime voiceDuration = max(player.duration, voiceElapsed) } else { voiceElapsed = min(voiceDuration, voiceElapsed + 0.05) } refreshVisiblePlayback() guard voiceElapsed >= voiceDuration, voiceDuration > 0 else { return } stopVoicePlayback(reset: true) } private func refreshVisiblePlayback() { for cell in rootView.tableView.visibleCells { guard let historyCell = cell as? PigeonHistoryCell, let indexPath = rootView.tableView.indexPath(for: historyCell) else { continue } let item = items[indexPath.row] let isCurrent = item.id == playingItemId let duration = isCurrent && voiceDuration > 0 ? voiceDuration : item.duration historyCell.applyPlayback( isPlaying: isCurrent && isVoicePlaying, elapsed: isCurrent ? voiceElapsed : 0, duration: duration ) } } } extension PigeonMessageHistoryVC: UITableViewDataSource, UITableViewDelegate { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { items.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell( withIdentifier: PigeonHistoryCell.reuseId, for: indexPath ) as! PigeonHistoryCell let item = items[indexPath.row] let isCurrent = item.id == playingItemId cell.configure( item: item, isPlaying: isCurrent && isVoicePlaying, elapsed: isCurrent ? voiceElapsed : 0 ) cell.onTogglePlay = { [weak self] in self?.togglePlayback(for: item) } return cell } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { guard isVoicePlaying else { return } stopVoicePlayback(reset: true) } }