// // RoleVoiceSetController.swift // Crush // // Created by Leon on 2025/7/20. // import JXPagingView import JXSegmentedView import UIKit import Combine class RoleVoiceSetController: CLBaseViewController { weak var viewModel: RoleCreateViewModel! /// Public 外部传入, 上次选中的声音 var selectVoiceBefore: VoiceModel? /// Public 外部传入, 首次进入,需要根据之前的性别选择,默认选中。 var sexSelect: Sex? private let segmentedViewHeight = 40 private lazy var segmentedView = JXSegmentedView(frame: CGRect(x: 0, y: 0, width: UIScreen.width, height: CGFloat(segmentedViewHeight))) private lazy var pagingView = JXPagingListRefreshView(delegate: self) private var headerView: RoleVoiceSetHeaderView! private var headerViewHeight: CGFloat = 288 private var controllers = [JXPagingViewListViewDelegate]() private let dataSource = JXSegmentedTitleDataSource() private var maleVoiceListVc: RoleVoiceListController! private var femaleVoiceListVc: RoleVoiceListController! var selectVoiceAction: ((_ voice: VoiceModel?) -> Void)? // Data @Published private var selectVoice:VoiceModel? private var cancellables = Set() // Flag var tabSelectIndexLast: Int = 0 override func viewDidLoad() { super.viewDidLoad() setupViews() setupDatas() setupEvents() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) navigationController?.interactivePopGestureRecognizer?.isEnabled = false } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) SpeechManager.shared.stopPlayCurrent() navigationController?.interactivePopGestureRecognizer?.isEnabled = true } private func setupViews() { navigationView.alpha0Title = "Voice" navigationView.styleMainButton.text = "Confirm" navigationView.styleMainButton.addTarget(self, action: #selector(tapNaviComfirm), for: .touchUpInside) headerView = { let v = RoleVoiceSetHeaderView() v.heightChangeBlock = { [weak self] height in self?.headerViewHeight = height self?.pagingView.resizeTableHeaderViewHeight() } return v }() pagingView.mainTableView.backgroundColor = .clear view.addSubview(pagingView) pagingView.snp.makeConstraints { make in make.leading.trailing.equalToSuperview() make.bottom.equalToSuperview() make.top.equalTo(navigationView.snp.bottom) } dataSource.clNormalStyle() dataSource.titles = ["Male", "Female"] segmentedView.listContainer = pagingView.listContainerView segmentedView.dataSource = dataSource segmentedView.delegate = self segmentedView.clNormalStyle() maleVoiceListVc = { let vc = RoleVoiceListController() controllers.append(vc) return vc }() femaleVoiceListVc = { let vc = RoleVoiceListController() controllers.append(vc) return vc }() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) disabledFullScreenPan() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) enabledFullScreenPan() } private func setupDatas() { guard let timbreDictArray = AppDictManager.shared.aiDict?.timbreDictList else{ Hud.showIndicator() AppDictManager.shared.loadAIDict {[weak self] ok in Hud.hideIndicator() if let array = AppDictManager.shared.aiDict?.timbreDictList, array.count > 0{ self?.setupDatas() } } return } // 分男女 var maleVoices = [VoiceModel]() var femaleVoices = [VoiceModel]() // 上次选中的 var selectVoiceLastTimeId = 0 if let lastTime = selectVoiceBefore{ selectVoiceLastTimeId = lastTime.voiceDict.id! } var defaultTabIndex = 0 let sex = sexSelect ?? .noncomfirming for (_, per) in timbreDictArray.enumerated(){ // 男 if let type = per.type, type == 1{ let model = VoiceModel(voiceDict: per) if maleVoices.count <= 0 && sex == .male{ defaultTabIndex = 0 } if per.id! == selectVoiceLastTimeId{ selectVoice = model selectVoice?.syncConfigFrom(other: selectVoiceBefore) defaultTabIndex = 0 } maleVoices.append(model) }else{ // 女 let model = VoiceModel(voiceDict: per) if femaleVoices.count <= 0 && sex == .female{ defaultTabIndex = 1 } if per.id! == selectVoiceLastTimeId{ selectVoice = model selectVoice?.syncConfigFrom(other: selectVoiceBefore) defaultTabIndex = 1 } femaleVoices.append(model) } } tabSelectIndexLast = defaultTabIndex segmentedView.defaultSelectedIndex = defaultTabIndex maleVoiceListVc.voices = maleVoices femaleVoiceListVc.voices = femaleVoices } private func setupEvents() { headerView.roleVoiceAdjustView.tapCloseButtonBlock = {[weak self] in self?.selectVoice = nil } maleVoiceListVc.selectVoiceAction = {[weak self] result in dlog("🎤当前选中的音频:\(String(describing: result))") self?.selectVoice = result } femaleVoiceListVc.selectVoiceAction = {[weak self] result in dlog("🎤当前选中的音频2:\(String(describing: result))") // self?.headerView.bind(voice: result) // self?.refreshViewStates() self?.selectVoice = result } $selectVoice.sink {[weak self] result in guard let `self` = self else { return } SpeechManager.shared.stopPlayCurrent() self.headerView.bind(voice: result) self.refreshViewStates() assert(self.femaleVoiceListVc != nil, "femaleVoiceListVc must be initialized") if let voice = result{ self.femaleVoiceListVc.select(voiceId: voice.voiceDict.id!) self.maleVoiceListVc.select(voiceId: voice.voiceDict.id!) }else{ self.femaleVoiceListVc.select(voiceId: 0) self.maleVoiceListVc.select(voiceId: 0) } }.store(in: &cancellables) } // MARK: - Helper private func refreshViewStates(){ // dlog("两个voice:\(String(describing: maleVoiceListVc.selectVoice)) and \(String(describing: femaleVoiceListVc.selectVoice))") // navigationView.styleMainButton.isEnabled = (maleVoiceListVc.selectVoice != nil || femaleVoiceListVc.selectVoice != nil) dlog("headView's voice \(String(describing: headerView.roleVoiceAdjustView.voice))") navigationView.styleMainButton.isEnabled = headerView.roleVoiceAdjustView.voice != nil } // MARK: - Action @objc private func tapNaviComfirm(){ guard selectVoice != nil else {return} let pitch = "\(headerView.roleVoiceAdjustView.speechLoudness)" let speed = "\(headerView.roleVoiceAdjustView.speechRate)" selectVoice?.dialoguePitch = pitch selectVoice?.dialogueSpeechRate = speed selectVoiceAction?(selectVoice) navigationController?.popViewController(animated: true) } } extension RoleVoiceSetController: JXPagingViewDelegate, JXSegmentedViewDelegate { func tableHeaderViewHeight(in _: JXPagingView) -> Int { return Int(headerViewHeight) } func tableHeaderView(in _: JXPagingView) -> UIView { return headerView } func heightForPinSectionHeader(in _: JXPagingView) -> Int { return segmentedViewHeight } func viewForPinSectionHeader(in _: JXPagingView) -> UIView { return segmentedView } func numberOfLists(in _: JXPagingView) -> Int { return controllers.count } func pagingView(_: JXPagingView, initListAtIndex index: Int) -> JXPagingViewListViewDelegate { let vc = controllers[index] return vc } func segmentedView(_ segmentedView: JXSegmentedView, didSelectedItemAt index: Int) { if tabSelectIndexLast != index{ tabSelectIndexLast = index; SpeechManager.shared.stopPlayCurrent() } } func mainTableViewDidScroll(_ scrollView: UIScrollView) { NaviAlphaHandle.changeNaviTitleAlpha(scrollView: scrollView, titleLabel: navigationView.titleLabel) } } class RoleVoiceSetHeaderView: UIView { var voiceRecommentTitle: UILabel! var voiceSelectView: VoiceSelectView! var roleVoiceAdjustView: RoleVoiceAdjustView! var heightChangeBlock: ((CGFloat) -> Void)? override init(frame: CGRect) { super.init(frame: frame) setupViews() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupViews() { let titleView = { let v = TitleView() v.optionInnerBottomPadding = 16 v.optionInnerTopPadding = 16 addSubview(v) v.snp.makeConstraints { make in make.leading.equalToSuperview() make.trailing.equalToSuperview() make.top.equalToSuperview() } return v }() titleView.titleLabel.text = "Voice" let subTitleView = { let v = UILabel() v.font = .t.tlm v.textColor = .c.ctsn addSubview(v) v.snp.makeConstraints { make in make.leading.equalToSuperview().offset(CGFloat.lrs) make.trailing.equalToSuperview().offset(-CGFloat.lrs) make.top.equalTo(titleView.snp.bottom).offset(12) } v.text = "Select your voice" return v }() // StackViewV has voiceSelectView, RoleVoiceAdjustView let stackView = { let v = UIStackView() v.axis = .vertical v.spacing = 12 addSubview(v) v.snp.makeConstraints { make in make.leading.equalToSuperview().offset(CGFloat.lrs) make.trailing.equalToSuperview().offset(-CGFloat.lrs) make.top.equalTo(subTitleView.snp.bottom).offset(12) } return v }() voiceSelectView = { let v = VoiceSelectView() stackView.addArrangedSubview(v) v.snp.makeConstraints { _ in // make.height.equalTo(120) } return v }() roleVoiceAdjustView = { let v = RoleVoiceAdjustView() stackView.addArrangedSubview(v) v.snp.makeConstraints { _ in // make.height.equalTo(120) } return v }() voiceSelectView.isHidden = false roleVoiceAdjustView.isHidden = true voiceRecommentTitle = { let v = UILabel() v.font = .t.ttm v.textColor = .c.ctpn addSubview(v) v.snp.makeConstraints { make in make.leading.equalToSuperview().offset(CGFloat.lrs) make.top.equalTo(stackView.snp.bottom).offset(12) } v.text = "Voice Recommendation" return v }() } override func layoutSubviews() { super.layoutSubviews() let maxY = voiceRecommentTitle.frame.maxY + 12 heightChangeBlock?(maxY) } public func bind(voice: VoiceModel?){ if voice != nil{ voiceSelectView.isHidden = true roleVoiceAdjustView.isHidden = false let name = voice?.voiceDict.name ?? "-" roleVoiceAdjustView.titleLabel.text = name }else{ voiceSelectView.isHidden = false roleVoiceAdjustView.isHidden = true } roleVoiceAdjustView.bind(voice: voice) } }