LNEditVoicePanel.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. //
  2. // LNEditVoicePanel.swift
  3. // Gami
  4. //
  5. // Created by OneeChan on 2026/1/14.
  6. //
  7. import Foundation
  8. import UIKit
  9. import SnapKit
  10. enum LNVoiceEditState {
  11. case record
  12. case edit
  13. case review
  14. case play
  15. }
  16. class LNEditVoicePanel: LNPopupView {
  17. private let minDuration: Double = 3
  18. private let maxDuration: Double = 60
  19. private let recordView = UIView()
  20. private let recordDurationLabel = UILabel()
  21. private let recordButton = UIButton()
  22. private let recordText = UILabel()
  23. private var recordTaskId: String?
  24. private let editView = UIView()
  25. private let editPlayButton = UIButton()
  26. private let editDurationLabel = UILabel()
  27. private var curUrl: URL?
  28. private var curDuration: Double?
  29. private let reviewView = UIView()
  30. private let displayView = UIView()
  31. private let playIcon = UIImageView()
  32. private let voiceWaveView = LNVoiceWaveView()
  33. private let playDurationLabel = UILabel()
  34. private var curState: LNVoiceEditState = .record {
  35. didSet {
  36. recordView.isHidden = curState != .record
  37. editView.isHidden = curState != .edit
  38. reviewView.isHidden = curState != .review
  39. displayView.isHidden = curState != .play
  40. }
  41. }
  42. override init(frame: CGRect) {
  43. super.init(frame: frame)
  44. setupViews()
  45. adjustByUserInfo()
  46. onTouchOutside = { [weak self] in
  47. guard let self else { return }
  48. LNVoicePlayer.shared.stop()
  49. if LNVoiceRecorder.shared.isRecording {
  50. let (url, duration) = LNVoiceRecorder.shared.stopRecord()
  51. handleRecordResult(url: url, duration: duration)
  52. } else if curState == .edit {
  53. let alert = LNCommonAlertView()
  54. alert.titleLabel.text = .init(key: "B00018")
  55. alert.setConfirm { [weak self] in
  56. guard let self else { return }
  57. dismiss()
  58. }
  59. alert.setCancel()
  60. alert.popup()
  61. } else {
  62. dismiss()
  63. }
  64. }
  65. LNEventDeliver.addObserver(self)
  66. LNVoiceRecorder.shared.prepare()
  67. }
  68. required init?(coder: NSCoder) {
  69. fatalError("init(coder:) has not been implemented")
  70. }
  71. }
  72. extension LNEditVoicePanel {
  73. private func adjustByUserInfo() {
  74. if myVoiceBarInfo.status == .review {
  75. curState = .review
  76. } else if myVoiceBarInfo.status == .done,
  77. !myUserInfo.voiceBar.isEmpty {
  78. curState = .play
  79. playDurationLabel.text = Double(myVoiceBarInfo.voiceBarDuration).durationDisplay
  80. } else {
  81. curState = .record
  82. }
  83. }
  84. private func handleRecordResult(url: URL?, duration: Double) {
  85. guard let url else {
  86. return
  87. }
  88. if duration < minDuration {
  89. showToast(.init(key: "B00009", minDuration))
  90. return
  91. }
  92. curUrl = url
  93. curDuration = duration
  94. editDurationLabel.text = duration.timeCountDisplay
  95. curState = .edit
  96. }
  97. }
  98. extension LNEditVoicePanel: LNVoicePlayerNotify {
  99. func onAudioUpdateDuration(path: String, cur: TimeInterval, total: TimeInterval) {
  100. if !editView.isHidden {
  101. guard curUrl?.path == path else { return }
  102. let remain = Int(total - cur)
  103. editDurationLabel.text = remain.timeCountDisplay
  104. } else if !displayView.isHidden {
  105. guard path == myUserInfo.voiceBar else { return }
  106. playDurationLabel.text = (total - cur).durationDisplay
  107. }
  108. }
  109. func onAudioStopPlay(path: String) {
  110. if !editView.isHidden {
  111. guard curUrl?.path == path else { return }
  112. guard let curDuration else { return }
  113. editDurationLabel.text = curDuration.timeCountDisplay
  114. editPlayButton.setImage(.icVoiceEditPlay, for: .normal)
  115. } else if !displayView.isHidden {
  116. guard path == myUserInfo.voiceBar else { return }
  117. playDurationLabel.text = "\(myVoiceBarInfo.voiceBarDuration)“"
  118. playIcon.image = .icVoicePlay
  119. voiceWaveView.stopAnimate()
  120. }
  121. }
  122. func onAudioStartPlay(path: String) {
  123. if !editView.isHidden {
  124. guard curUrl?.path == path else { return }
  125. editPlayButton.setImage(.icVoiceEditPause, for: .normal)
  126. } else if !displayView.isHidden {
  127. guard path == myUserInfo.voiceBar else { return }
  128. playIcon.image = .icVoicePause
  129. voiceWaveView.startAnimate()
  130. }
  131. }
  132. }
  133. extension LNEditVoicePanel: LNVoiceRecorderNotify {
  134. func onRecordTaskDurationChanged(taskId: String, duration: Double, volumeRatio: Double) {
  135. guard recordTaskId == taskId else { return }
  136. recordDurationLabel.text = duration.timeCountDisplay
  137. }
  138. func onRecordTaskRecording(taskId: String) {
  139. guard recordTaskId == taskId else { return }
  140. recordText.text = .init(key: "B00008")
  141. recordButton.setImage(.icVoiceEditStop, for: .normal)
  142. }
  143. func onRecordTaskStop(taskId: String) {
  144. guard recordTaskId == taskId else { return }
  145. resetRecord()
  146. }
  147. func onRecordTaskReachMaxDuration(taskId: String, fileUrl: URL?, duration: Double) {
  148. guard recordTaskId == taskId else { return }
  149. handleRecordResult(url: fileUrl, duration: duration)
  150. }
  151. }
  152. extension LNEditVoicePanel {
  153. private func resetRecord() {
  154. recordDurationLabel.text = "00:00"
  155. recordButton.setImage(.icVoiceEditStart, for: .normal)
  156. recordText.text = .init(key: "B00007")
  157. }
  158. private func setupViews() {
  159. let fakeView = UIView()
  160. fakeView.isUserInteractionEnabled = false
  161. container.addSubview(fakeView)
  162. fakeView.snp.makeConstraints { make in
  163. make.edges.equalToSuperview()
  164. make.height.equalTo(326)
  165. }
  166. let titleView = buildTitle()
  167. container.addSubview(titleView)
  168. titleView.snp.makeConstraints { make in
  169. make.horizontalEdges.equalToSuperview()
  170. make.top.equalToSuperview()
  171. }
  172. let recordView = buildRecordView()
  173. container.addSubview(recordView)
  174. recordView.snp.makeConstraints { make in
  175. make.horizontalEdges.equalToSuperview()
  176. make.top.equalTo(titleView.snp.bottom)
  177. make.bottom.equalToSuperview()
  178. }
  179. let editView = buildEditView()
  180. container.addSubview(editView)
  181. editView.snp.makeConstraints { make in
  182. make.horizontalEdges.equalToSuperview()
  183. make.top.equalTo(titleView.snp.bottom)
  184. make.bottom.equalToSuperview()
  185. }
  186. let reviewView = buildReviewView()
  187. container.addSubview(reviewView)
  188. reviewView.snp.makeConstraints { make in
  189. make.horizontalEdges.equalToSuperview()
  190. make.top.equalTo(titleView.snp.bottom)
  191. make.bottom.equalToSuperview()
  192. }
  193. let displayView = buildDisplayView()
  194. container.addSubview(displayView)
  195. displayView.snp.makeConstraints { make in
  196. make.horizontalEdges.equalToSuperview()
  197. make.top.equalTo(titleView.snp.bottom)
  198. make.bottom.equalToSuperview()
  199. }
  200. }
  201. private func buildTitle() -> UIView {
  202. let container = UIView()
  203. container.snp.makeConstraints { make in
  204. make.height.equalTo(50)
  205. }
  206. let titleLabel = UILabel()
  207. titleLabel.text = .init(key: "B00006")
  208. container.addSubview(titleLabel)
  209. titleLabel.snp.makeConstraints { make in
  210. make.center.equalToSuperview()
  211. }
  212. return container
  213. }
  214. private func buildRecordView() -> UIView {
  215. recordDurationLabel.font = .body_m
  216. recordDurationLabel.textColor = .text_5
  217. recordView.addSubview(recordDurationLabel)
  218. recordDurationLabel.snp.makeConstraints { make in
  219. make.centerX.equalToSuperview()
  220. make.top.equalToSuperview().offset(21)
  221. }
  222. recordView.addSubview(recordButton)
  223. recordButton.addAction(UIAction(handler: { [weak self] _ in
  224. guard let self else { return }
  225. if LNVoiceRecorder.shared.isRecording {
  226. let (url, duration) = LNVoiceRecorder.shared.stopRecord()
  227. handleRecordResult(url: url, duration: duration)
  228. } else {
  229. recordTaskId = LNVoiceRecorder.shared.startRecord(maxDuration)
  230. }
  231. }), for: .touchUpInside)
  232. recordButton.snp.makeConstraints { make in
  233. make.centerX.equalToSuperview()
  234. make.top.equalTo(recordDurationLabel.snp.bottom).offset(12)
  235. }
  236. recordText.font = .body_m
  237. recordText.textColor = .text_4
  238. recordView.addSubview(recordText)
  239. recordText.snp.makeConstraints { make in
  240. make.centerX.equalToSuperview()
  241. make.top.equalTo(recordButton.snp.bottom).offset(12)
  242. }
  243. let confirmButton = UIButton()
  244. confirmButton.setTitle(.init(key: "A00240"), for: .normal)
  245. confirmButton.setTitleColor(.text_1, for: .normal)
  246. confirmButton.titleLabel?.font = .heading_h3
  247. confirmButton.layer.cornerRadius = 23.5
  248. confirmButton.backgroundColor = .fill_4
  249. confirmButton.isEnabled = false
  250. recordView.addSubview(confirmButton)
  251. confirmButton.snp.makeConstraints { make in
  252. make.horizontalEdges.equalToSuperview().inset(12)
  253. make.bottom.equalToSuperview().offset(commonBottomInset)
  254. make.height.equalTo(47)
  255. }
  256. resetRecord()
  257. return recordView
  258. }
  259. private func buildEditView() -> UIView {
  260. editView.isHidden = true
  261. editDurationLabel.font = .body_m
  262. editDurationLabel.textColor = .text_5
  263. editView.addSubview(editDurationLabel)
  264. editDurationLabel.snp.makeConstraints { make in
  265. make.centerX.equalToSuperview()
  266. make.top.equalToSuperview()
  267. }
  268. let stackView = UIStackView()
  269. stackView.axis = .horizontal
  270. stackView.spacing = 24
  271. editView.addSubview(stackView)
  272. stackView.snp.makeConstraints { make in
  273. make.centerX.equalToSuperview()
  274. make.top.equalTo(editDurationLabel.snp.bottom).offset(34)
  275. }
  276. let remakeView = UIView()
  277. stackView.addArrangedSubview(remakeView)
  278. let remakeButton = UIButton()
  279. remakeButton.setImage(.icVoiceEditRemake, for: .normal)
  280. remakeButton.addAction(UIAction(handler: { [weak self] _ in
  281. guard let self else { return }
  282. LNVoicePlayer.shared.stop()
  283. curUrl = nil
  284. curDuration = nil
  285. curState = .record
  286. }), for: .touchUpInside)
  287. remakeView.addSubview(remakeButton)
  288. remakeButton.snp.makeConstraints { make in
  289. make.centerX.equalToSuperview()
  290. make.leading.greaterThanOrEqualToSuperview()
  291. make.top.equalToSuperview()
  292. }
  293. let remakeLabel = UILabel()
  294. remakeLabel.text = .init(key: "B00010")
  295. remakeLabel.font = .body_m
  296. remakeLabel.textColor = .text_4
  297. remakeLabel.textAlignment = .center
  298. remakeView.addSubview(remakeLabel)
  299. remakeLabel.snp.makeConstraints { make in
  300. make.horizontalEdges.equalToSuperview()
  301. make.bottom.equalToSuperview()
  302. make.top.equalTo(remakeButton.snp.bottom).offset(12)
  303. }
  304. let playView = UIView()
  305. stackView.addArrangedSubview(playView)
  306. editPlayButton.setImage(.icVoiceEditPlay, for: .normal)
  307. editPlayButton.addAction(UIAction(handler: { [weak self] _ in
  308. guard let self else { return }
  309. guard let curUrl else { return }
  310. if LNVoicePlayer.shared.isPlaying {
  311. LNVoicePlayer.shared.stop()
  312. } else {
  313. LNVoicePlayer.shared.play(path: curUrl.path)
  314. }
  315. }), for: .touchUpInside)
  316. playView.addSubview(editPlayButton)
  317. editPlayButton.snp.makeConstraints { make in
  318. make.centerX.equalToSuperview()
  319. make.leading.greaterThanOrEqualToSuperview()
  320. make.top.equalToSuperview()
  321. }
  322. let playLabel = UILabel()
  323. playLabel.text = .init(key: "B00011")
  324. playLabel.font = .body_m
  325. playLabel.textColor = .text_4
  326. playLabel.textAlignment = .center
  327. playView.addSubview(playLabel)
  328. playLabel.snp.makeConstraints { make in
  329. make.horizontalEdges.equalToSuperview()
  330. make.bottom.equalToSuperview()
  331. make.top.equalTo(editPlayButton.snp.bottom).offset(12)
  332. }
  333. let confirmButton = UIButton()
  334. confirmButton.setTitle(.init(key: "A00240"), for: .normal)
  335. confirmButton.setTitleColor(.text_1, for: .normal)
  336. confirmButton.titleLabel?.font = .heading_h3
  337. confirmButton.layer.cornerRadius = 23.5
  338. confirmButton.setBackgroundImage(.primary_8, for: .normal)
  339. confirmButton.clipsToBounds = true
  340. confirmButton.addAction(UIAction(handler: { [weak self] _ in
  341. guard let self else { return }
  342. guard let curUrl, let curDuration else { return }
  343. LNVoicePlayer.shared.stop()
  344. showLoading()
  345. LNFileUploader.shared.startUpload(type: .voice, fileURL: curUrl,
  346. progressHandler: nil) { [weak self] url, err in
  347. dismissLoading()
  348. guard let self else { return }
  349. guard let url, err == nil else {
  350. showToast(err)
  351. return
  352. }
  353. LNProfileManager.shared.setMyVoiceBar(url: url, duration: curDuration.toDuration)
  354. { [weak self] success in
  355. guard let self else { return }
  356. guard success else { return }
  357. dismiss()
  358. }
  359. }
  360. }), for: .touchUpInside)
  361. editView.addSubview(confirmButton)
  362. confirmButton.snp.makeConstraints { make in
  363. make.horizontalEdges.equalToSuperview().inset(12)
  364. make.bottom.equalToSuperview().offset(commonBottomInset)
  365. make.height.equalTo(47)
  366. }
  367. return editView
  368. }
  369. private func buildReviewView() -> UIView {
  370. let icon = UIImageView()
  371. icon.image = .icVoiceEditReview
  372. reviewView.addSubview(icon)
  373. icon.snp.makeConstraints { make in
  374. make.centerX.equalToSuperview()
  375. make.top.equalToSuperview().offset(47)
  376. }
  377. let titleLabel = UILabel()
  378. titleLabel.text = .init(key: "B00012")
  379. titleLabel.textColor = .text_5
  380. titleLabel.font = .heading_h4
  381. reviewView.addSubview(titleLabel)
  382. titleLabel.snp.makeConstraints { make in
  383. make.centerX.equalToSuperview()
  384. make.top.equalTo(icon.snp.bottom).offset(12)
  385. }
  386. let descLabel = UILabel()
  387. descLabel.text = .init(key: "B00013")
  388. descLabel.font = .body_m
  389. descLabel.textColor = .text_5
  390. reviewView.addSubview(descLabel)
  391. descLabel.snp.makeConstraints { make in
  392. make.centerX.equalToSuperview()
  393. make.top.equalTo(titleLabel.snp.bottom).offset(3)
  394. }
  395. return reviewView
  396. }
  397. private func buildDisplayView() -> UIView {
  398. let button = UIButton()
  399. button.setBackgroundImage(.primary_7, for: .normal)
  400. button.layer.cornerRadius = 16
  401. button.clipsToBounds = true
  402. displayView.addSubview(button)
  403. button.snp.makeConstraints { make in
  404. make.centerX.equalToSuperview()
  405. make.top.equalToSuperview().offset(59)
  406. make.width.equalTo(163)
  407. make.height.equalTo(32)
  408. }
  409. playIcon.image = .icVoicePlay
  410. button.addSubview(playIcon)
  411. playIcon.snp.makeConstraints { make in
  412. make.centerY.equalToSuperview()
  413. make.leading.equalToSuperview().offset(3)
  414. make.width.height.equalTo(22)
  415. }
  416. voiceWaveView.isUserInteractionEnabled = false
  417. voiceWaveView.build()
  418. button.addSubview(voiceWaveView)
  419. voiceWaveView.snp.makeConstraints { make in
  420. make.centerY.equalToSuperview()
  421. make.leading.equalTo(playIcon.snp.trailing).offset(7)
  422. make.width.equalTo(19)
  423. make.height.equalTo(11)
  424. }
  425. playDurationLabel.font = .heading_h5
  426. playDurationLabel.textColor = .text_1
  427. button.addSubview(playDurationLabel)
  428. playDurationLabel.snp.makeConstraints { make in
  429. make.centerY.equalToSuperview()
  430. make.trailing.equalToSuperview().offset(-11)
  431. }
  432. button.addAction(UIAction(handler: { [weak self] _ in
  433. guard self != nil else { return }
  434. if LNVoicePlayer.shared.isPlaying {
  435. LNVoicePlayer.shared.stop()
  436. } else if !myUserInfo.voiceBar.isEmpty {
  437. LNVoicePlayer.shared.play(myUserInfo.voiceBar)
  438. }
  439. }), for: .touchUpInside)
  440. let confirmButton = UIButton()
  441. confirmButton.setTitle(.init(key: "B00107"), for: .normal)
  442. confirmButton.setTitleColor(.text_6, for: .normal)
  443. confirmButton.titleLabel?.font = .heading_h3
  444. confirmButton.layer.cornerRadius = 23.5
  445. confirmButton.setBackgroundImage(.primary_7, for: .normal)
  446. confirmButton.clipsToBounds = true
  447. confirmButton.addAction(UIAction(handler: { [weak self] _ in
  448. guard let self else { return }
  449. LNVoicePlayer.shared.stop()
  450. LNProfileManager.shared.cleanVoiceBar { [weak self] success in
  451. guard let self else { return }
  452. guard success else { return }
  453. resetRecord()
  454. curState = .record
  455. }
  456. }), for: .touchUpInside)
  457. displayView.addSubview(confirmButton)
  458. confirmButton.snp.makeConstraints { make in
  459. make.horizontalEdges.equalToSuperview().inset(12)
  460. make.bottom.equalToSuperview().offset(commonBottomInset)
  461. make.height.equalTo(47)
  462. }
  463. let cover = UIView()
  464. cover.layer.cornerRadius = 22
  465. cover.backgroundColor = .fill
  466. cover.isUserInteractionEnabled = false
  467. confirmButton.insertSubview(cover, at: 0)
  468. cover.snp.makeConstraints { make in
  469. make.edges.equalToSuperview().inset(1)
  470. }
  471. return displayView
  472. }
  473. }