jsdw_ios/QuickLocation/Section/PigeonMessage/PigeonMessageHistoryVC.swift

217 lines
6.8 KiB
Swift

//
// 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.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) {
guard let url = item.mediaURL else { 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
}
do {
let player = try AVAudioPlayer(data: data)
player.prepareToPlay()
self.audioPlayer = player
self.voiceDuration = max(1, player.duration)
self.voiceElapsed = 0
player.play()
self.isVoicePlaying = true
self.startTimer()
self.refreshVisiblePlayback()
} catch {
DLToast.show(text: "无法播放")
self.playingItemId = nil
}
}
}.resume()
}
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)
}
}