LNSkillFieldVoiceEditView.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. //
  2. // LNSkillFieldVoiceEditView.swift
  3. // Gami
  4. //
  5. // Created by OneeChan on 2026/1/21.
  6. //
  7. import Foundation
  8. import UIKit
  9. import SnapKit
  10. class LNSkillFieldVoiceEditView: LNSkillFieldBaseEditView {
  11. private var minDuration: Double?
  12. private var maxDuration: Double?
  13. private let recordView = UIView()
  14. private let recordButton = UIButton()
  15. private let recordDurationLabel = UILabel()
  16. private let recordText = UILabel()
  17. private var recordTaskId: String?
  18. private let displayView = UIView()
  19. private let playIcon = UIImageView()
  20. private let voiceWaveView = LNVoiceWaveView()
  21. private let playDurationLabel = UILabel()
  22. override init(frame: CGRect) {
  23. super.init(frame: frame)
  24. setupViews()
  25. LNEventDeliver.addObserver(self)
  26. }
  27. override func update(_ field: LNSkillEditField) {
  28. super.update(field)
  29. if let limit = field.validate.size {
  30. minDuration = Double(limit.min)
  31. maxDuration = Double(limit.max)
  32. }
  33. if let url = field.value as? String, !url.isEmpty {
  34. recordView.isHidden = true
  35. displayView.isHidden = false
  36. if field.duration == 0 {
  37. LNVoiceResourceManager.shared.getRemoteAudioDuration(urlStr: url) { [weak self] duration, _ in
  38. guard let self else { return }
  39. guard let duration else { return }
  40. guard field.value as? String == url else { return }
  41. field.duration = duration.toDuration
  42. playDurationLabel.text = "\(field.duration)“"
  43. }
  44. } else {
  45. playDurationLabel.text = "\(field.duration)“"
  46. }
  47. }
  48. }
  49. required init(coder: NSCoder) {
  50. fatalError("init(coder:) has not been implemented")
  51. }
  52. }
  53. extension LNSkillFieldVoiceEditView {
  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. field?.value = url.path
  63. field?.duration = duration.toDuration
  64. playDurationLabel.text = duration.durationDisplay
  65. recordView.isHidden = true
  66. resetRecord()
  67. displayView.isHidden = false
  68. needReview = true
  69. delegate?.onSkillFieldBaseEditViewInputChanged(view: self)
  70. }
  71. }
  72. extension LNSkillFieldVoiceEditView: LNVoicePlayerNotify {
  73. func onAudioUpdateDuration(path: String, cur: TimeInterval, total: TimeInterval) {
  74. guard field?.value as? String == path else { return }
  75. playDurationLabel.text = (total - cur).durationDisplay
  76. }
  77. func onAudioStopPlay(path: String) {
  78. guard field?.value as? String == path else { return }
  79. guard let curDuration = field?.duration else { return }
  80. playDurationLabel.text = "\(curDuration)“"
  81. playIcon.image = .icVoicePlay
  82. voiceWaveView.stopAnimate()
  83. }
  84. func onAudioStartPlay(path: String) {
  85. guard field?.value as? String == path else { return }
  86. playIcon.image = .icVoicePause
  87. voiceWaveView.startAnimate()
  88. }
  89. }
  90. extension LNSkillFieldVoiceEditView: LNVoiceRecorderNotify {
  91. func onRecordTaskDurationChanged(taskId: String, duration: Double, volumeRatio: Double) {
  92. guard recordTaskId == taskId else { return }
  93. recordDurationLabel.text = duration.timeCountDisplay
  94. }
  95. func onRecordTaskRecording(taskId: String) {
  96. guard recordTaskId == taskId else { return }
  97. recordText.text = .init(key: "B00008")
  98. recordButton.setImage(.icVoiceEditStop, for: .normal)
  99. }
  100. func onRecordTaskStop(taskId: String) {
  101. guard recordTaskId == taskId else { return }
  102. resetRecord()
  103. }
  104. func onRecordTaskReachMaxDuration(taskId: String, fileUrl: URL?, duration: Double) {
  105. guard recordTaskId == taskId else { return }
  106. handleRecordResult(url: fileUrl, duration: duration)
  107. }
  108. }
  109. extension LNSkillFieldVoiceEditView {
  110. private func resetRecord() {
  111. recordDurationLabel.text = "00:00"
  112. recordButton.setImage(.icVoiceEditStart, for: .normal)
  113. recordText.text = .init(key: "B00007")
  114. }
  115. private func setupViews() {
  116. container.layer.cornerRadius = 12
  117. container.backgroundColor = .fill_1
  118. container.snp.makeConstraints { make in
  119. make.height.equalTo(158)
  120. }
  121. let recordView = buildRecordView()
  122. container.addSubview(recordView)
  123. recordView.snp.makeConstraints { make in
  124. make.center.equalToSuperview()
  125. }
  126. let playView = buildDisplayView()
  127. container.addSubview(playView)
  128. playView.snp.makeConstraints { make in
  129. make.center.equalToSuperview()
  130. }
  131. }
  132. private func buildRecordView() -> UIView {
  133. recordDurationLabel.font = .body_m
  134. recordDurationLabel.textColor = .text_5
  135. recordView.addSubview(recordDurationLabel)
  136. recordDurationLabel.snp.makeConstraints { make in
  137. make.centerX.equalToSuperview()
  138. make.top.equalToSuperview()
  139. }
  140. recordView.addSubview(recordButton)
  141. recordButton.addAction(UIAction(handler: { [weak self] _ in
  142. guard let self else { return }
  143. LNVoicePlayer.shared.stop()
  144. if LNVoiceRecorder.shared.isRecording {
  145. let (url, duration) = LNVoiceRecorder.shared.stopRecord()
  146. handleRecordResult(url: url, duration: duration)
  147. } else {
  148. LNVoiceRecorder.shared.startRecord(maxDuration) { [weak self] taskId in
  149. guard let self else { return }
  150. recordTaskId = taskId
  151. }
  152. }
  153. }), for: .touchUpInside)
  154. recordButton.snp.makeConstraints { make in
  155. make.centerX.equalToSuperview()
  156. make.top.equalTo(recordDurationLabel.snp.bottom).offset(12)
  157. make.leading.greaterThanOrEqualToSuperview()
  158. }
  159. recordText.font = .body_m
  160. recordText.textColor = .text_4
  161. recordView.addSubview(recordText)
  162. recordText.snp.makeConstraints { make in
  163. make.horizontalEdges.equalToSuperview()
  164. make.top.equalTo(recordButton.snp.bottom).offset(12)
  165. make.bottom.equalToSuperview()
  166. }
  167. resetRecord()
  168. return recordView
  169. }
  170. private func buildDisplayView() -> UIView {
  171. displayView.isHidden = true
  172. let playView = UIView()
  173. displayView.addSubview(playView)
  174. playView.snp.makeConstraints { make in
  175. make.verticalEdges.equalToSuperview()
  176. make.leading.equalToSuperview()
  177. }
  178. let button = UIButton()
  179. button.setBackgroundImage(.primary_7, for: .normal)
  180. button.layer.cornerRadius = 19
  181. button.clipsToBounds = true
  182. playView.addSubview(button)
  183. button.snp.makeConstraints { make in
  184. make.horizontalEdges.equalToSuperview()
  185. make.top.equalToSuperview()
  186. make.width.equalTo(206)
  187. make.height.equalTo(38)
  188. }
  189. playIcon.image = .icVoicePlay
  190. button.addSubview(playIcon)
  191. playIcon.snp.makeConstraints { make in
  192. make.centerY.equalToSuperview()
  193. make.leading.equalToSuperview().offset(3)
  194. make.width.height.equalTo(32)
  195. }
  196. voiceWaveView.isUserInteractionEnabled = false
  197. voiceWaveView.itemWidth = 3
  198. voiceWaveView.build()
  199. button.addSubview(voiceWaveView)
  200. voiceWaveView.snp.makeConstraints { make in
  201. make.centerY.equalToSuperview()
  202. make.leading.equalTo(playIcon.snp.trailing).offset(7)
  203. make.width.equalTo(30)
  204. make.height.equalTo(18)
  205. }
  206. playDurationLabel.font = .heading_h4
  207. playDurationLabel.textColor = .text_1
  208. button.addSubview(playDurationLabel)
  209. playDurationLabel.snp.makeConstraints { make in
  210. make.centerY.equalToSuperview()
  211. make.trailing.equalToSuperview().offset(-8)
  212. }
  213. button.addAction(UIAction(handler: { [weak self] _ in
  214. guard let self else { return }
  215. if LNVoicePlayer.shared.isPlaying {
  216. LNVoicePlayer.shared.stop()
  217. } else if let path = field?.value as? String {
  218. LNVoicePlayer.shared.play(path: path)
  219. }
  220. }), for: .touchUpInside)
  221. let playLabel = UILabel()
  222. playLabel.text = .init(key: "B00011")
  223. playLabel.font = .body_s
  224. playLabel.textColor = .text_4
  225. playLabel.textAlignment = .center
  226. playView.addSubview(playLabel)
  227. playLabel.snp.makeConstraints { make in
  228. make.horizontalEdges.equalToSuperview()
  229. make.bottom.equalToSuperview()
  230. make.top.equalTo(button.snp.bottom).offset(11)
  231. }
  232. let redoView = UIView()
  233. displayView.addSubview(redoView)
  234. redoView.snp.makeConstraints { make in
  235. make.verticalEdges.equalToSuperview()
  236. make.trailing.equalToSuperview()
  237. make.leading.equalTo(playView.snp.trailing).offset(13)
  238. make.width.equalTo(60)
  239. }
  240. let config = UIImage.SymbolConfiguration(pointSize: 18, weight: .semibold)
  241. let redoButton = UIButton()
  242. redoButton.backgroundColor = .fill_4
  243. redoButton.layer.cornerRadius = 17
  244. redoButton.setImage(.init(systemName: "xmark", withConfiguration: config)?.withRenderingMode(.alwaysTemplate), for: .normal)
  245. redoButton.tintColor = .fill
  246. redoButton.addAction(UIAction(handler: { [weak self] _ in
  247. guard let self else { return }
  248. LNVoicePlayer.shared.stop()
  249. field?.value = nil
  250. field?.duration = 0
  251. recordView.isHidden = false
  252. displayView.isHidden = true
  253. }), for: .touchUpInside)
  254. redoView.addSubview(redoButton)
  255. redoButton.snp.makeConstraints { make in
  256. make.centerX.equalToSuperview()
  257. make.top.equalToSuperview()
  258. make.width.height.equalTo(34)
  259. }
  260. let redoLabel = UILabel()
  261. redoLabel.text = .init(key: "B00010")
  262. redoLabel.font = .body_s
  263. redoLabel.textColor = .text_4
  264. redoLabel.textAlignment = .center
  265. redoView.addSubview(redoLabel)
  266. redoLabel.snp.makeConstraints { make in
  267. make.horizontalEdges.equalToSuperview()
  268. make.bottom.equalToSuperview()
  269. }
  270. return displayView
  271. }
  272. }