// // LNVoiceEditView.swift // Gami // // Created by OneeChan on 2026/3/10. // import Foundation import UIKit import SnapKit protocol LNVoiceEditViewDelegate: AnyObject { func onVoiceChanged() } class LNVoiceEditView: UIView { 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 playVoiceView = LNVoicePlayView() private(set) var hasChanged = false weak var delegate: LNVoiceEditViewDelegate? var voiceInfo: (String?, Int) { (playVoiceView.curUrl, playVoiceView.duration) } override init(frame: CGRect) { super.init(frame: frame) setupViews() LNEventDeliver.addObserver(self) } func update(_ url: String, duration: Int) { playVoiceView.update(url, duration: duration) recordView.isHidden = true displayView.isHidden = true if url.isEmpty || duration == 0 { recordView.isHidden = false } else { displayView.isHidden = false } } override func didMoveToWindow() { super.didMoveToWindow() if window == nil, recordTaskId != nil { LNVoiceRecorder.shared.stopRecord() } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } extension LNVoiceEditView { private func handleRecordResult(url: URL?, duration: Double) { guard let url else { return } if let minDuration, duration < minDuration { showToast(.init(key: "B00009", minDuration)) return } playVoiceView.update(url.path, duration: duration.toDuration) recordView.isHidden = true resetRecord() displayView.isHidden = false hasChanged = true delegate?.onVoiceChanged() } } extension LNVoiceEditView: 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 LNVoiceEditView { private func resetRecord() { recordDurationLabel.text = "00:00" recordButton.setImage(.icVoiceEditStart, for: .normal) recordText.text = .init(key: "B00007") recordTaskId = nil } private func setupViews() { layer.cornerRadius = 12 backgroundColor = .fill_1 snp.makeConstraints { make in make.height.equalTo(158).priority(.low) } let recordView = buildRecordView() addSubview(recordView) recordView.snp.makeConstraints { make in make.center.equalToSuperview() } let playView = buildDisplayView() 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() } playVoiceView.waveView.itemWidth = 3 playVoiceView.waveView.build() playVoiceView.waveView.snp.updateConstraints { make in make.width.equalTo(30) make.height.equalTo(18) } playVoiceView.layer.cornerRadius = 19 playVoiceView.clipsToBounds = true playView.addSubview(playVoiceView) playVoiceView.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview() make.top.equalToSuperview() make.width.equalTo(206) make.height.equalTo(38) } 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(playVoiceView.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() playVoiceView.update(nil, duration: 0) recordView.isHidden = false displayView.isHidden = true delegate?.onVoiceChanged() }), 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 } }