LNIMChatVoiceInputView.swift 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. //
  2. // LNIMChatVoiceInputView.swift
  3. // Lanu
  4. //
  5. // Created by OneeChan on 2025/12/4.
  6. //
  7. import Foundation
  8. import UIKit
  9. import SnapKit
  10. protocol LNIMChatVoiceInputViewDelegate: AnyObject {
  11. func onVoiceFinishInput()
  12. }
  13. class LNIMChatVoiceInputView: UIView {
  14. private let durationLabel = UILabel()
  15. private let waveView = LNIMChatVoiceWaveView()
  16. private let controlButton = UIButton()
  17. private var recordTaskId: String?
  18. private let maxRecord: Double = 60
  19. weak var delegate: LNIMChatVoiceInputViewDelegate?
  20. weak var viewModel: LNIMChatViewModel?
  21. override init(frame: CGRect) {
  22. super.init(frame: frame)
  23. setupViews()
  24. LNEventDeliver.addObserver(self)
  25. }
  26. required init?(coder: NSCoder) {
  27. fatalError("init(coder:) has not been implemented")
  28. }
  29. }
  30. extension LNIMChatVoiceInputView {
  31. func startRecord() {
  32. LNVoicePlayer.shared.stop()
  33. LNVoiceRecorder.shared.startRecord(maxRecord) { [weak self] taskId in
  34. guard let self else { return }
  35. recordTaskId = taskId
  36. }
  37. }
  38. }
  39. extension LNIMChatVoiceInputView: LNVoiceRecorderNotify {
  40. func onRecordTaskReachMaxDuration(taskId: String, fileUrl: URL?, duration: Double) {
  41. if let fileUrl {
  42. viewModel?.sendVoiceMessage(voicePath: fileUrl.path, duration: duration)
  43. }
  44. }
  45. func onRecordTaskStop(taskId: String) {
  46. guard taskId == recordTaskId else { return }
  47. controlButton.setImage(.icImChatVoiceInputPause, for: .normal)
  48. durationLabel.text = "00:00"
  49. waveView.clear()
  50. delegate?.onVoiceFinishInput()
  51. }
  52. func onRecordTaskPause(taskId: String) {
  53. guard taskId == recordTaskId else { return }
  54. controlButton.setImage(.icImChatVoiceInputContinue, for: .normal)
  55. }
  56. func onRecordTaskDurationChanged(taskId: String, duration: Double, volumeRatio: Double) {
  57. guard taskId == recordTaskId else { return }
  58. durationLabel.text = .init(format: "%02d:%02d", Int(duration) / 60, Int(duration) % 60)
  59. waveView.add(volumeRatio)
  60. }
  61. func onRecordTaskRecording(taskId: String) {
  62. guard taskId == recordTaskId else { return }
  63. controlButton.setImage(.icImChatVoiceInputPause, for: .normal)
  64. }
  65. }
  66. extension LNIMChatVoiceInputView {
  67. private func setupViews() {
  68. backgroundColor = .fill
  69. let infoView = buildVoiceInfoView()
  70. addSubview(infoView)
  71. infoView.snp.makeConstraints { make in
  72. make.horizontalEdges.equalToSuperview()
  73. make.top.equalToSuperview()
  74. }
  75. let controlView = buildVoiceControlView()
  76. addSubview(controlView)
  77. controlView.snp.makeConstraints { make in
  78. make.horizontalEdges.equalToSuperview()
  79. make.bottom.equalToSuperview().offset(-safeBottomInset)
  80. make.top.equalTo(infoView.snp.bottom)
  81. }
  82. }
  83. private func buildVoiceInfoView() -> UIView {
  84. let container = UIView()
  85. container.snp.makeConstraints { make in
  86. make.height.equalTo(40)
  87. }
  88. durationLabel.font = .heading_h2
  89. durationLabel.textColor = .text_5
  90. durationLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
  91. durationLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
  92. container.addSubview(durationLabel)
  93. durationLabel.snp.makeConstraints { make in
  94. make.centerY.equalToSuperview()
  95. make.leading.equalToSuperview().offset(16)
  96. }
  97. container.addSubview(waveView)
  98. waveView.snp.makeConstraints { make in
  99. make.trailing.equalToSuperview().offset(-16)
  100. make.leading.equalToSuperview().offset(82)
  101. make.height.equalTo(28)
  102. make.centerY.equalToSuperview()
  103. }
  104. return container
  105. }
  106. private func buildVoiceControlView() -> UIView {
  107. let container = UIView()
  108. container.snp.makeConstraints { make in
  109. make.height.equalTo(40)
  110. }
  111. let deleteButton = UIButton()
  112. deleteButton.setImage(.icImChatVoiceInputDelete, for: .normal)
  113. deleteButton.addAction(UIAction(handler: { [weak self] _ in
  114. guard let self else { return }
  115. delegate?.onVoiceFinishInput()
  116. LNVoiceRecorder.shared.stopRecord()
  117. waveView.clear()
  118. }), for: .touchUpInside)
  119. container.addSubview(deleteButton)
  120. deleteButton.snp.makeConstraints { make in
  121. make.centerY.equalToSuperview()
  122. make.leading.equalToSuperview().offset(12)
  123. }
  124. controlButton.setImage(.icImChatVoiceInputPause, for: .normal)
  125. controlButton.addAction(UIAction(handler: { [weak self] _ in
  126. guard self != nil else { return }
  127. if LNVoiceRecorder.shared.curState == .recording {
  128. LNVoiceRecorder.shared.pauseRecord()
  129. } else if LNVoiceRecorder.shared.curState == .pausing {
  130. LNVoiceRecorder.shared.continueRecord()
  131. }
  132. }), for: .touchUpInside)
  133. container.addSubview(controlButton)
  134. controlButton.snp.makeConstraints { make in
  135. make.center.equalToSuperview()
  136. }
  137. let sendButton = UIButton()
  138. sendButton.setImage(.icImChatSend, for: .normal)
  139. sendButton.addAction(UIAction(handler: { [weak self] _ in
  140. guard let self else { return }
  141. let (path, duration) = LNVoiceRecorder.shared.stopRecord()
  142. if let path {
  143. viewModel?.sendVoiceMessage(voicePath: path.path, duration: duration)
  144. }
  145. }), for: .touchUpInside)
  146. container.addSubview(sendButton)
  147. sendButton.snp.makeConstraints { make in
  148. make.centerY.equalToSuperview()
  149. make.trailing.equalToSuperview().offset(-12)
  150. }
  151. return container
  152. }
  153. }
  154. #if DEBUG
  155. import SwiftUI
  156. struct LNIMChatVoiceInputViewPreview: UIViewRepresentable {
  157. func makeUIView(context: Context) -> some UIView {
  158. let container = UIView()
  159. container.backgroundColor = .lightGray
  160. let view = LNIMChatVoiceInputView()
  161. container.addSubview(view)
  162. view.snp.makeConstraints { make in
  163. make.leading.trailing.bottom.equalToSuperview()
  164. }
  165. return container
  166. }
  167. func updateUIView(_ uiView: UIViewType, context: Context) { }
  168. }
  169. #Preview(body: {
  170. LNIMChatVoiceInputViewPreview()
  171. })
  172. #endif