| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542 |
- //
- // LNEditVoicePanel.swift
- // Gami
- //
- // Created by OneeChan on 2026/1/14.
- //
- import Foundation
- import UIKit
- import SnapKit
- enum LNVoiceEditState {
- case record
- case edit
- case review
- case play
- }
- class LNEditVoicePanel: LNPopupView {
- private let minDuration: Double = 3
- private let maxDuration: Double = 60
- private let recordView = UIView()
- private let recordDurationLabel = UILabel()
- private let recordButton = UIButton()
- private let recordText = UILabel()
- private var recordTaskId: String?
-
- private let editView = UIView()
- private let editPlayButton = UIButton()
- private let editDurationLabel = UILabel()
- private var curUrl: URL?
- private var curDuration: Double?
-
- private let reviewView = UIView()
-
- private let displayView = UIView()
- private let playIcon = UIImageView()
- private let voiceWaveView = LNVoiceWaveView()
- private let playDurationLabel = UILabel()
-
- private var curState: LNVoiceEditState = .record {
- didSet {
- recordView.isHidden = curState != .record
- editView.isHidden = curState != .edit
- reviewView.isHidden = curState != .review
- displayView.isHidden = curState != .play
- }
- }
-
- override init(frame: CGRect) {
- super.init(frame: frame)
-
- setupViews()
- adjustByUserInfo()
-
- onTouchOutside = { [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 if curState == .edit {
- let alert = LNCommonAlertView()
- alert.titleLabel.text = .init(key: "B00018")
- alert.showConfirm { [weak self] in
- guard let self else { return }
- dismiss()
- }
- alert.showCancel()
- alert.popup()
- } else {
- dismiss()
- }
- }
-
- LNEventDeliver.addObserver(self)
- }
-
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
- }
- extension LNEditVoicePanel {
- private func adjustByUserInfo() {
- if myVoiceBarInfo.status == .review {
- curState = .review
- } else if myVoiceBarInfo.status == .done,
- !myUserInfo.voiceBar.isEmpty {
- curState = .play
- playDurationLabel.text = Double(myVoiceBarInfo.voiceBarDuration).durationDisplay
- } else {
- curState = .record
- }
- }
-
- private func handleRecordResult(url: URL?, duration: Double) {
- guard let url else {
- return
- }
- if duration < minDuration {
- showToast(.init(key: "B00009", minDuration))
- return
- }
- curUrl = url
- curDuration = duration
- editDurationLabel.text = duration.timeCountDisplay
- curState = .edit
- }
- }
- extension LNEditVoicePanel: LNVoicePlayerNotify {
- func onAudioUpdateDuration(path: String, cur: TimeInterval, total: TimeInterval) {
- if !editView.isHidden {
- guard curUrl?.path == path else { return }
- let remain = Int(total - cur)
- editDurationLabel.text = remain.timeCountDisplay
- } else if !displayView.isHidden {
- guard path == myUserInfo.voiceBar else { return }
- playDurationLabel.text = (total - cur).durationDisplay
- }
- }
-
- func onAudioStopPlay(path: String) {
- if !editView.isHidden {
- guard curUrl?.path == path else { return }
- guard let curDuration else { return }
- editDurationLabel.text = curDuration.timeCountDisplay
- editPlayButton.setImage(.icVoiceEditPlay, for: .normal)
- } else if !displayView.isHidden {
- guard path == myUserInfo.voiceBar else { return }
- playDurationLabel.text = "\(myVoiceBarInfo.voiceBarDuration)“"
- playIcon.image = .icVoicePlay
- voiceWaveView.stopAnimate()
- }
- }
-
- func onAudioStartPlay(path: String) {
- if !editView.isHidden {
- guard curUrl?.path == path else { return }
- editPlayButton.setImage(.icVoiceEditPause, for: .normal)
- } else if !displayView.isHidden {
- guard path == myUserInfo.voiceBar else { return }
- playIcon.image = .icVoicePause
- voiceWaveView.startAnimate()
- }
- }
- }
- extension LNEditVoicePanel: 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 LNEditVoicePanel {
- private func resetRecord() {
- recordDurationLabel.text = "00:00"
- recordButton.setImage(.icVoiceEditStart, for: .normal)
- recordText.text = .init(key: "B00007")
- }
-
- private func setupViews() {
- let fakeView = UIView()
- fakeView.isUserInteractionEnabled = false
- container.addSubview(fakeView)
- fakeView.snp.makeConstraints { make in
- make.edges.equalToSuperview()
- make.height.equalTo(326)
- }
-
- let titleView = buildTitle()
- container.addSubview(titleView)
- titleView.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.top.equalToSuperview()
- }
-
- let recordView = buildRecordView()
- container.addSubview(recordView)
- recordView.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.top.equalTo(titleView.snp.bottom)
- make.bottom.equalToSuperview()
- }
-
- let editView = buildEditView()
- container.addSubview(editView)
- editView.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.top.equalTo(titleView.snp.bottom)
- make.bottom.equalToSuperview()
- }
-
- let reviewView = buildReviewView()
- container.addSubview(reviewView)
- reviewView.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.top.equalTo(titleView.snp.bottom)
- make.bottom.equalToSuperview()
- }
-
- let displayView = buildDisplayView()
- container.addSubview(displayView)
- displayView.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.top.equalTo(titleView.snp.bottom)
- make.bottom.equalToSuperview()
- }
- }
-
- private func buildTitle() -> UIView {
- let container = UIView()
- container.snp.makeConstraints { make in
- make.height.equalTo(50)
- }
-
- let titleLabel = UILabel()
- titleLabel.text = .init(key: "B00006")
- container.addSubview(titleLabel)
- titleLabel.snp.makeConstraints { make in
- make.center.equalToSuperview()
- }
-
- return container
- }
-
- 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().offset(21)
- }
-
- recordView.addSubview(recordButton)
- recordButton.addAction(UIAction(handler: { [weak self] _ in
- guard let self else { return }
- 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)
- }
-
- recordText.font = .body_m
- recordText.textColor = .text_4
- recordView.addSubview(recordText)
- recordText.snp.makeConstraints { make in
- make.centerX.equalToSuperview()
- make.top.equalTo(recordButton.snp.bottom).offset(12)
- }
-
- let confirmButton = UIButton()
- confirmButton.setTitle(.init(key: "A00240"), for: .normal)
- confirmButton.setTitleColor(.text_1, for: .normal)
- confirmButton.titleLabel?.font = .heading_h3
- confirmButton.layer.cornerRadius = 23.5
- confirmButton.backgroundColor = .fill_4
- confirmButton.isEnabled = false
- recordView.addSubview(confirmButton)
- confirmButton.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview().inset(12)
- make.bottom.equalToSuperview().offset(commonBottomInset)
- make.height.equalTo(47)
- }
-
- resetRecord()
-
- return recordView
- }
-
- private func buildEditView() -> UIView {
- editView.isHidden = true
-
- editDurationLabel.font = .body_m
- editDurationLabel.textColor = .text_5
- editView.addSubview(editDurationLabel)
- editDurationLabel.snp.makeConstraints { make in
- make.centerX.equalToSuperview()
- make.top.equalToSuperview()
- }
-
- let stackView = UIStackView()
- stackView.axis = .horizontal
- stackView.spacing = 24
- editView.addSubview(stackView)
- stackView.snp.makeConstraints { make in
- make.centerX.equalToSuperview()
- make.top.equalTo(editDurationLabel.snp.bottom).offset(34)
- }
-
- let remakeView = UIView()
- stackView.addArrangedSubview(remakeView)
-
- let remakeButton = UIButton()
- remakeButton.setImage(.icVoiceEditRemake, for: .normal)
- remakeButton.addAction(UIAction(handler: { [weak self] _ in
- guard let self else { return }
- LNVoicePlayer.shared.stop()
- curUrl = nil
- curDuration = nil
- curState = .record
- }), for: .touchUpInside)
- remakeView.addSubview(remakeButton)
- remakeButton.snp.makeConstraints { make in
- make.centerX.equalToSuperview()
- make.leading.greaterThanOrEqualToSuperview()
- make.top.equalToSuperview()
- }
-
- let remakeLabel = UILabel()
- remakeLabel.text = .init(key: "B00010")
- remakeLabel.font = .body_m
- remakeLabel.textColor = .text_4
- remakeLabel.textAlignment = .center
- remakeView.addSubview(remakeLabel)
- remakeLabel.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.bottom.equalToSuperview()
- make.top.equalTo(remakeButton.snp.bottom).offset(12)
- }
-
- let playView = UIView()
- stackView.addArrangedSubview(playView)
-
- editPlayButton.setImage(.icVoiceEditPlay, for: .normal)
- editPlayButton.addAction(UIAction(handler: { [weak self] _ in
- guard let self else { return }
- guard let curUrl else { return }
- if LNVoicePlayer.shared.isPlaying {
- LNVoicePlayer.shared.stop()
- } else {
- LNVoicePlayer.shared.play(path: curUrl.path)
- }
- }), for: .touchUpInside)
- playView.addSubview(editPlayButton)
- editPlayButton.snp.makeConstraints { make in
- make.centerX.equalToSuperview()
- make.leading.greaterThanOrEqualToSuperview()
- make.top.equalToSuperview()
- }
-
- let playLabel = UILabel()
- playLabel.text = .init(key: "B00011")
- playLabel.font = .body_m
- playLabel.textColor = .text_4
- playLabel.textAlignment = .center
- playView.addSubview(playLabel)
- playLabel.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.bottom.equalToSuperview()
- make.top.equalTo(editPlayButton.snp.bottom).offset(12)
- }
-
- let confirmButton = UIButton()
- confirmButton.setTitle(.init(key: "A00240"), for: .normal)
- confirmButton.setTitleColor(.text_1, for: .normal)
- confirmButton.titleLabel?.font = .heading_h3
- confirmButton.layer.cornerRadius = 23.5
- confirmButton.setBackgroundImage(.primary_8, for: .normal)
- confirmButton.clipsToBounds = true
- confirmButton.addAction(UIAction(handler: { [weak self] _ in
- guard let self else { return }
- guard let curUrl, let curDuration else { return }
- LNVoicePlayer.shared.stop()
- showLoading()
- LNFileUploader.shared.startUpload(type: .voice, fileURL: curUrl,
- progressHandler: nil) { [weak self] url, err in
- dismissLoading()
- guard let self else { return }
- guard let url, err == nil else {
- showToast(err)
- return
- }
- LNProfileManager.shared.setMyVoiceBar(url: url, duration: curDuration.toDuration)
- { [weak self] success in
- guard let self else { return }
- guard success else { return }
- dismiss()
- }
- }
- }), for: .touchUpInside)
- editView.addSubview(confirmButton)
- confirmButton.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview().inset(12)
- make.bottom.equalToSuperview().offset(commonBottomInset)
- make.height.equalTo(47)
- }
-
- return editView
- }
-
- private func buildReviewView() -> UIView {
- let icon = UIImageView()
- icon.image = .icVoiceEditReview
- reviewView.addSubview(icon)
- icon.snp.makeConstraints { make in
- make.centerX.equalToSuperview()
- make.top.equalToSuperview().offset(47)
- }
-
- let titleLabel = UILabel()
- titleLabel.text = .init(key: "B00012")
- titleLabel.textColor = .text_5
- titleLabel.font = .heading_h4
- reviewView.addSubview(titleLabel)
- titleLabel.snp.makeConstraints { make in
- make.centerX.equalToSuperview()
- make.top.equalTo(icon.snp.bottom).offset(12)
- }
-
- let descLabel = UILabel()
- descLabel.text = .init(key: "B00013")
- descLabel.font = .body_m
- descLabel.textColor = .text_5
- reviewView.addSubview(descLabel)
- descLabel.snp.makeConstraints { make in
- make.centerX.equalToSuperview()
- make.top.equalTo(titleLabel.snp.bottom).offset(3)
- }
-
- return reviewView
- }
-
- private func buildDisplayView() -> UIView {
- let button = UIButton()
- button.setBackgroundImage(.primary_7, for: .normal)
- button.layer.cornerRadius = 16
- button.clipsToBounds = true
- displayView.addSubview(button)
- button.snp.makeConstraints { make in
- make.centerX.equalToSuperview()
- make.top.equalToSuperview().offset(59)
- make.width.equalTo(163)
- make.height.equalTo(32)
- }
-
- playIcon.image = .icVoicePlay
- button.addSubview(playIcon)
- playIcon.snp.makeConstraints { make in
- make.centerY.equalToSuperview()
- make.leading.equalToSuperview().offset(3)
- make.width.height.equalTo(22)
- }
-
- voiceWaveView.isUserInteractionEnabled = false
- voiceWaveView.build()
- button.addSubview(voiceWaveView)
- voiceWaveView.snp.makeConstraints { make in
- make.centerY.equalToSuperview()
- make.leading.equalTo(playIcon.snp.trailing).offset(7)
- make.width.equalTo(19)
- make.height.equalTo(11)
- }
-
- playDurationLabel.font = .heading_h5
- playDurationLabel.textColor = .text_1
- button.addSubview(playDurationLabel)
- playDurationLabel.snp.makeConstraints { make in
- make.centerY.equalToSuperview()
- make.trailing.equalToSuperview().offset(-11)
- }
-
- button.addAction(UIAction(handler: { [weak self] _ in
- guard self != nil else { return }
- if LNVoicePlayer.shared.isPlaying {
- LNVoicePlayer.shared.stop()
- } else if !myUserInfo.voiceBar.isEmpty {
- LNVoicePlayer.shared.play(myUserInfo.voiceBar)
- }
- }), for: .touchUpInside)
-
- let confirmButton = UIButton()
- confirmButton.setTitle(.init(key: "B00107"), for: .normal)
- confirmButton.setTitleColor(.text_6, for: .normal)
- confirmButton.titleLabel?.font = .heading_h3
- confirmButton.layer.cornerRadius = 23.5
- confirmButton.setBackgroundImage(.primary_7, for: .normal)
- confirmButton.clipsToBounds = true
- confirmButton.addAction(UIAction(handler: { [weak self] _ in
- guard let self else { return }
- LNVoicePlayer.shared.stop()
-
- LNProfileManager.shared.cleanVoiceBar { [weak self] success in
- guard let self else { return }
- guard success else { return }
- resetRecord()
- curState = .record
- }
- }), for: .touchUpInside)
- displayView.addSubview(confirmButton)
- confirmButton.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview().inset(12)
- make.bottom.equalToSuperview().offset(commonBottomInset)
- make.height.equalTo(47)
- }
-
- let cover = UIView()
- cover.layer.cornerRadius = 22
- cover.backgroundColor = .fill
- cover.isUserInteractionEnabled = false
- confirmButton.insertSubview(cover, at: 0)
- cover.snp.makeConstraints { make in
- make.edges.equalToSuperview().inset(1)
- }
-
- return displayView
- }
- }
|