| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325 |
- //
- // LNSkillFieldVoiceEditView.swift
- // Gami
- //
- // Created by OneeChan on 2026/1/21.
- //
- import Foundation
- import UIKit
- import SnapKit
- class LNSkillFieldVoiceEditView: LNSkillFieldBaseEditView {
- private var minDuration: Double?
- private var maxDuration: Double?
-
- private let recordView = UIView()
- private let recordButton = UIButton()
- private let recordDurationLabel = UILabel()
- private let recordText = UILabel()
- private var recordTaskId: String?
-
- private let displayView = UIView()
- private let playIcon = UIImageView()
- private let voiceWaveView = LNVoiceWaveView()
- private let playDurationLabel = UILabel()
-
- override init(frame: CGRect) {
- super.init(frame: frame)
-
- setupViews()
-
- LNEventDeliver.addObserver(self)
- }
-
- override func update(_ field: LNSkillEditField) {
- super.update(field)
-
- if let limit = field.validate.size {
- minDuration = Double(limit.min)
- maxDuration = Double(limit.max)
- }
-
- if let url = field.value as? String, !url.isEmpty {
- recordView.isHidden = true
- displayView.isHidden = false
- if field.duration == 0 {
- LNVoiceResourceManager.shared.getRemoteAudioDuration(urlStr: url) { [weak self] duration, _ in
- guard let self else { return }
- guard let duration else { return }
- guard field.value as? String == url else { return }
- field.duration = duration.toDuration
- playDurationLabel.text = "\(field.duration)“"
- }
- } else {
- playDurationLabel.text = "\(field.duration)“"
- }
- }
- }
-
- required init(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
- }
- extension LNSkillFieldVoiceEditView {
- private func handleRecordResult(url: URL?, duration: Double) {
- guard let url else {
- return
- }
- if let minDuration, duration < minDuration {
- showToast(.init(key: "B00009", minDuration))
- return
- }
- field?.value = url.path
- field?.duration = duration.toDuration
- playDurationLabel.text = duration.durationDisplay
-
- recordView.isHidden = true
- resetRecord()
- displayView.isHidden = false
-
- needReview = true
- delegate?.onSkillFieldBaseEditViewInputChanged(view: self)
- }
- }
- extension LNSkillFieldVoiceEditView: LNVoicePlayerNotify {
- func onAudioUpdateDuration(path: String, cur: TimeInterval, total: TimeInterval) {
- guard field?.value as? String == path else { return }
-
- playDurationLabel.text = (total - cur).durationDisplay
- }
-
- func onAudioStopPlay(path: String) {
- guard field?.value as? String == path else { return }
-
- guard let curDuration = field?.duration else { return }
-
- playDurationLabel.text = "\(curDuration)“"
- playIcon.image = .icVoicePlay
- voiceWaveView.stopAnimate()
- }
-
- func onAudioStartPlay(path: String) {
- guard field?.value as? String == path else { return }
-
- playIcon.image = .icVoicePause
- voiceWaveView.startAnimate()
- }
- }
- extension LNSkillFieldVoiceEditView: LNVoiceRecorderNotify {
- func onRecordTaskDurationChanged(taskId: String, duration: Double, volumeRatio: Double) {
- guard recordTaskId == taskId else { return }
-
- recordDurationLabel.text = duration.timeCountDisplay
- }
-
- func onRecordTaskRecording(taskId: String) {
- guard recordTaskId == taskId else { return }
-
- recordText.text = .init(key: "B00008")
- recordButton.setImage(.icVoiceEditStop, for: .normal)
- }
-
- func onRecordTaskStop(taskId: String) {
- guard recordTaskId == taskId else { return }
-
- resetRecord()
- }
-
- func onRecordTaskReachMaxDuration(taskId: String, fileUrl: URL?, duration: Double) {
- guard recordTaskId == taskId else { return }
-
- handleRecordResult(url: fileUrl, duration: duration)
- }
- }
- extension LNSkillFieldVoiceEditView {
- private func resetRecord() {
- recordDurationLabel.text = "00:00"
- recordButton.setImage(.icVoiceEditStart, for: .normal)
- recordText.text = .init(key: "B00007")
- }
-
- private func setupViews() {
- container.layer.cornerRadius = 12
- container.backgroundColor = .fill_1
- container.snp.makeConstraints { make in
- make.height.equalTo(158)
- }
-
- let recordView = buildRecordView()
- container.addSubview(recordView)
- recordView.snp.makeConstraints { make in
- make.center.equalToSuperview()
- }
-
- let playView = buildDisplayView()
- container.addSubview(playView)
- playView.snp.makeConstraints { make in
- make.center.equalToSuperview()
- }
- }
-
- private func buildRecordView() -> UIView {
- recordDurationLabel.font = .body_m
- recordDurationLabel.textColor = .text_5
- recordView.addSubview(recordDurationLabel)
- recordDurationLabel.snp.makeConstraints { make in
- make.centerX.equalToSuperview()
- make.top.equalToSuperview()
- }
-
- recordView.addSubview(recordButton)
- recordButton.addAction(UIAction(handler: { [weak self] _ in
- guard let self else { return }
- LNVoicePlayer.shared.stop()
-
- if LNVoiceRecorder.shared.isRecording {
- let (url, duration) = LNVoiceRecorder.shared.stopRecord()
- handleRecordResult(url: url, duration: duration)
- } else {
- LNVoiceRecorder.shared.startRecord(maxDuration) { [weak self] taskId in
- guard let self else { return }
- recordTaskId = taskId
- }
- }
- }), for: .touchUpInside)
- recordButton.snp.makeConstraints { make in
- make.centerX.equalToSuperview()
- make.top.equalTo(recordDurationLabel.snp.bottom).offset(12)
- make.leading.greaterThanOrEqualToSuperview()
- }
-
- recordText.font = .body_m
- recordText.textColor = .text_4
- recordView.addSubview(recordText)
- recordText.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.top.equalTo(recordButton.snp.bottom).offset(12)
- make.bottom.equalToSuperview()
- }
-
- resetRecord()
-
- return recordView
- }
-
- private func buildDisplayView() -> UIView {
- displayView.isHidden = true
-
- let playView = UIView()
- displayView.addSubview(playView)
- playView.snp.makeConstraints { make in
- make.verticalEdges.equalToSuperview()
- make.leading.equalToSuperview()
- }
-
- let button = UIButton()
- button.setBackgroundImage(.primary_7, for: .normal)
- button.layer.cornerRadius = 19
- button.clipsToBounds = true
- playView.addSubview(button)
- button.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.top.equalToSuperview()
- make.width.equalTo(206)
- make.height.equalTo(38)
- }
-
- playIcon.image = .icVoicePlay
- button.addSubview(playIcon)
- playIcon.snp.makeConstraints { make in
- make.centerY.equalToSuperview()
- make.leading.equalToSuperview().offset(3)
- make.width.height.equalTo(32)
- }
-
- voiceWaveView.isUserInteractionEnabled = false
- voiceWaveView.itemWidth = 3
- voiceWaveView.build()
- button.addSubview(voiceWaveView)
- voiceWaveView.snp.makeConstraints { make in
- make.centerY.equalToSuperview()
- make.leading.equalTo(playIcon.snp.trailing).offset(7)
- make.width.equalTo(30)
- make.height.equalTo(18)
- }
-
- playDurationLabel.font = .heading_h4
- playDurationLabel.textColor = .text_1
- button.addSubview(playDurationLabel)
- playDurationLabel.snp.makeConstraints { make in
- make.centerY.equalToSuperview()
- make.trailing.equalToSuperview().offset(-8)
- }
-
- button.addAction(UIAction(handler: { [weak self] _ in
- guard let self else { return }
- if LNVoicePlayer.shared.isPlaying {
- LNVoicePlayer.shared.stop()
- } else if let path = field?.value as? String {
- LNVoicePlayer.shared.play(path: path)
- }
- }), for: .touchUpInside)
-
- let playLabel = UILabel()
- playLabel.text = .init(key: "B00011")
- playLabel.font = .body_s
- playLabel.textColor = .text_4
- playLabel.textAlignment = .center
- playView.addSubview(playLabel)
- playLabel.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.bottom.equalToSuperview()
- make.top.equalTo(button.snp.bottom).offset(11)
- }
-
- let redoView = UIView()
- displayView.addSubview(redoView)
- redoView.snp.makeConstraints { make in
- make.verticalEdges.equalToSuperview()
- make.trailing.equalToSuperview()
- make.leading.equalTo(playView.snp.trailing).offset(13)
- make.width.equalTo(60)
- }
-
- let config = UIImage.SymbolConfiguration(pointSize: 18, weight: .semibold)
- let redoButton = UIButton()
- redoButton.backgroundColor = .fill_4
- redoButton.layer.cornerRadius = 17
- redoButton.setImage(.init(systemName: "xmark", withConfiguration: config)?.withRenderingMode(.alwaysTemplate), for: .normal)
- redoButton.tintColor = .fill
- redoButton.addAction(UIAction(handler: { [weak self] _ in
- guard let self else { return }
- LNVoicePlayer.shared.stop()
- field?.value = nil
- field?.duration = 0
-
- recordView.isHidden = false
- displayView.isHidden = true
- }), for: .touchUpInside)
- redoView.addSubview(redoButton)
- redoButton.snp.makeConstraints { make in
- make.centerX.equalToSuperview()
- make.top.equalToSuperview()
- make.width.height.equalTo(34)
- }
-
- let redoLabel = UILabel()
- redoLabel.text = .init(key: "B00010")
- redoLabel.font = .body_s
- redoLabel.textColor = .text_4
- redoLabel.textAlignment = .center
- redoView.addSubview(redoLabel)
- redoLabel.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.bottom.equalToSuperview()
- }
-
- return displayView
- }
- }
|