LNEditVoicePanel.swift 19 KB

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