LNVoiceEditView.swift 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. //
  2. // LNVoiceEditView.swift
  3. // Gami
  4. //
  5. // Created by OneeChan on 2026/3/10.
  6. //
  7. import Foundation
  8. import UIKit
  9. import SnapKit
  10. protocol LNVoiceEditViewDelegate: NSObject {
  11. func onVoiceChanged()
  12. }
  13. class LNVoiceEditView: UIView {
  14. private var minDuration: Double?
  15. private var maxDuration: Double?
  16. private let recordView = UIView()
  17. private let recordButton = UIButton()
  18. private let recordDurationLabel = UILabel()
  19. private let recordText = UILabel()
  20. private var recordTaskId: String?
  21. private let displayView = UIView()
  22. private let playVoiceView = LNVoicePlayView()
  23. private(set) var hasChanged = false
  24. weak var delegate: LNVoiceEditViewDelegate?
  25. var voiceInfo: (String?, Int) {
  26. (playVoiceView.curUrl, playVoiceView.duration)
  27. }
  28. override init(frame: CGRect) {
  29. super.init(frame: frame)
  30. setupViews()
  31. LNEventDeliver.addObserver(self)
  32. }
  33. func update(_ url: String, duration: Int) {
  34. playVoiceView.update(url, duration: duration)
  35. recordView.isHidden = true
  36. displayView.isHidden = true
  37. if url.isEmpty || duration == 0 {
  38. recordView.isHidden = false
  39. } else {
  40. displayView.isHidden = false
  41. }
  42. }
  43. override func didMoveToWindow() {
  44. super.didMoveToWindow()
  45. if window == nil, recordTaskId != nil {
  46. LNVoiceRecorder.shared.stopRecord()
  47. }
  48. }
  49. required init?(coder: NSCoder) {
  50. fatalError("init(coder:) has not been implemented")
  51. }
  52. }
  53. extension LNVoiceEditView {
  54. private func handleRecordResult(url: URL?, duration: Double) {
  55. guard let url else {
  56. return
  57. }
  58. if let minDuration, duration < minDuration {
  59. showToast(.init(key: "B00009", minDuration))
  60. return
  61. }
  62. playVoiceView.update(url.path, duration: duration.toDuration)
  63. recordView.isHidden = true
  64. resetRecord()
  65. displayView.isHidden = false
  66. hasChanged = true
  67. delegate?.onVoiceChanged()
  68. }
  69. }
  70. extension LNVoiceEditView: LNVoiceRecorderNotify {
  71. func onRecordTaskDurationChanged(taskId: String, duration: Double, volumeRatio: Double) {
  72. guard recordTaskId == taskId else { return }
  73. recordDurationLabel.text = duration.timeCountDisplay
  74. }
  75. func onRecordTaskRecording(taskId: String) {
  76. guard recordTaskId == taskId else { return }
  77. recordText.text = .init(key: "B00008")
  78. recordButton.setImage(.icVoiceEditStop, for: .normal)
  79. }
  80. func onRecordTaskStop(taskId: String) {
  81. guard recordTaskId == taskId else { return }
  82. resetRecord()
  83. }
  84. func onRecordTaskReachMaxDuration(taskId: String, fileUrl: URL?, duration: Double) {
  85. guard recordTaskId == taskId else { return }
  86. handleRecordResult(url: fileUrl, duration: duration)
  87. }
  88. }
  89. extension LNVoiceEditView {
  90. private func resetRecord() {
  91. recordDurationLabel.text = "00:00"
  92. recordButton.setImage(.icVoiceEditStart, for: .normal)
  93. recordText.text = .init(key: "B00007")
  94. recordTaskId = nil
  95. }
  96. private func setupViews() {
  97. layer.cornerRadius = 12
  98. backgroundColor = .fill_1
  99. snp.makeConstraints { make in
  100. make.height.equalTo(158).priority(.low)
  101. }
  102. let recordView = buildRecordView()
  103. addSubview(recordView)
  104. recordView.snp.makeConstraints { make in
  105. make.center.equalToSuperview()
  106. }
  107. let playView = buildDisplayView()
  108. addSubview(playView)
  109. playView.snp.makeConstraints { make in
  110. make.center.equalToSuperview()
  111. }
  112. }
  113. private func buildRecordView() -> UIView {
  114. recordDurationLabel.font = .body_m
  115. recordDurationLabel.textColor = .text_5
  116. recordView.addSubview(recordDurationLabel)
  117. recordDurationLabel.snp.makeConstraints { make in
  118. make.centerX.equalToSuperview()
  119. make.top.equalToSuperview()
  120. }
  121. recordView.addSubview(recordButton)
  122. recordButton.addAction(UIAction(handler: { [weak self] _ in
  123. guard let self else { return }
  124. LNVoicePlayer.shared.stop()
  125. if LNVoiceRecorder.shared.isRecording {
  126. let (url, duration) = LNVoiceRecorder.shared.stopRecord()
  127. handleRecordResult(url: url, duration: duration)
  128. } else {
  129. LNVoiceRecorder.shared.startRecord(maxDuration) { [weak self] taskId in
  130. guard let self else { return }
  131. recordTaskId = taskId
  132. }
  133. }
  134. }), for: .touchUpInside)
  135. recordButton.snp.makeConstraints { make in
  136. make.centerX.equalToSuperview()
  137. make.top.equalTo(recordDurationLabel.snp.bottom).offset(12)
  138. make.leading.greaterThanOrEqualToSuperview()
  139. }
  140. recordText.font = .body_m
  141. recordText.textColor = .text_4
  142. recordView.addSubview(recordText)
  143. recordText.snp.makeConstraints { make in
  144. make.horizontalEdges.equalToSuperview()
  145. make.top.equalTo(recordButton.snp.bottom).offset(12)
  146. make.bottom.equalToSuperview()
  147. }
  148. resetRecord()
  149. return recordView
  150. }
  151. private func buildDisplayView() -> UIView {
  152. displayView.isHidden = true
  153. let playView = UIView()
  154. displayView.addSubview(playView)
  155. playView.snp.makeConstraints { make in
  156. make.verticalEdges.equalToSuperview()
  157. make.leading.equalToSuperview()
  158. }
  159. playVoiceView.waveView.itemWidth = 3
  160. playVoiceView.waveView.build()
  161. playVoiceView.waveView.snp.updateConstraints { make in
  162. make.width.equalTo(30)
  163. make.height.equalTo(18)
  164. }
  165. playVoiceView.layer.cornerRadius = 19
  166. playVoiceView.clipsToBounds = true
  167. playView.addSubview(playVoiceView)
  168. playVoiceView.snp.makeConstraints { make in
  169. make.horizontalEdges.equalToSuperview()
  170. make.top.equalToSuperview()
  171. make.width.equalTo(206)
  172. make.height.equalTo(38)
  173. }
  174. let playLabel = UILabel()
  175. playLabel.text = .init(key: "B00011")
  176. playLabel.font = .body_s
  177. playLabel.textColor = .text_4
  178. playLabel.textAlignment = .center
  179. playView.addSubview(playLabel)
  180. playLabel.snp.makeConstraints { make in
  181. make.horizontalEdges.equalToSuperview()
  182. make.bottom.equalToSuperview()
  183. make.top.equalTo(playVoiceView.snp.bottom).offset(11)
  184. }
  185. let redoView = UIView()
  186. displayView.addSubview(redoView)
  187. redoView.snp.makeConstraints { make in
  188. make.verticalEdges.equalToSuperview()
  189. make.trailing.equalToSuperview()
  190. make.leading.equalTo(playView.snp.trailing).offset(13)
  191. make.width.equalTo(60)
  192. }
  193. let config = UIImage.SymbolConfiguration(pointSize: 18, weight: .semibold)
  194. let redoButton = UIButton()
  195. redoButton.backgroundColor = .fill_4
  196. redoButton.layer.cornerRadius = 17
  197. redoButton.setImage(.init(systemName: "xmark", withConfiguration: config)?.withRenderingMode(.alwaysTemplate), for: .normal)
  198. redoButton.tintColor = .fill
  199. redoButton.addAction(UIAction(handler: { [weak self] _ in
  200. guard let self else { return }
  201. LNVoicePlayer.shared.stop()
  202. playVoiceView.update(nil, duration: 0)
  203. recordView.isHidden = false
  204. displayView.isHidden = true
  205. delegate?.onVoiceChanged()
  206. }), for: .touchUpInside)
  207. redoView.addSubview(redoButton)
  208. redoButton.snp.makeConstraints { make in
  209. make.centerX.equalToSuperview()
  210. make.top.equalToSuperview()
  211. make.width.height.equalTo(34)
  212. }
  213. let redoLabel = UILabel()
  214. redoLabel.text = .init(key: "B00010")
  215. redoLabel.font = .body_s
  216. redoLabel.textColor = .text_4
  217. redoLabel.textAlignment = .center
  218. redoView.addSubview(redoLabel)
  219. redoLabel.snp.makeConstraints { make in
  220. make.horizontalEdges.equalToSuperview()
  221. make.bottom.equalToSuperview()
  222. }
  223. return displayView
  224. }
  225. }