Visual_Novel_iOS/crush/Crush/Src/Components/UI/Sheet/ChatModePickSheet.swift

402 lines
14 KiB
Swift
Raw Normal View History

2025-10-09 10:29:35 +00:00
//
// ChatModePickSheet.swift
// Crush
//
// Created by Leon on 2025/8/17.
//
import UIKit
///
typealias ChatModelSelectionCallback = (AIChatModel) -> Void
///
class ChatModePickSheet: EGPopBaseView {
var closeButton: EPIconTertiaryButton!
var titleLabel: UILabel!
//
var scrollView: UIScrollView!
//
var modelCardViews: [ChatModelCardView] = []
var stayTunedLabel: CLLabel!
//
private var chatModels: [AIChatModel] = []
private var currentSelectedModelCode: String?
var cardHeight = 200.0//168.0
//
var selectionCallback: ChatModelSelectionCallback?
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
init(currentSelectedModelCode: String? = nil) {
self.currentSelectedModelCode = currentSelectedModelCode
super.init(direction: .bottom)
contentView.backgroundColor = .c.csbn
//
contentLength = 200 + UIWindow.safeAreaBottom
setupViews()
loadChatModels()
}
private func setupViews() {
closeButton = {
let v = EPIconTertiaryButton(radius: .round, iconSize: .small, iconCode: .delete)
v.addTarget(self, action: #selector(bgButtonPressed), for: .touchUpInside)
contentView.addSubview(v)
v.snp.makeConstraints { make in
make.top.equalToSuperview().offset(20)
make.size.equalTo(v.bgImageSize())
make.trailing.equalToSuperview().offset(-16)
}
return v
}()
titleLabel = {
let v = UILabel()
v.font = .t.ttm
v.textColor = .text
v.textAlignment = .center
contentView.addSubview(v)
v.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(24)
make.trailing.equalToSuperview().offset(-24)
make.top.equalToSuperview().offset(32)
}
return v
}()
scrollView = {
let v = UIScrollView()
v.showsVerticalScrollIndicator = false
v.showsHorizontalScrollIndicator = false
v.delegate = self
contentView.addSubview(v)
v.snp.makeConstraints { make in
make.leading.equalToSuperview()
make.trailing.equalToSuperview()
make.top.equalTo(titleLabel.snp.bottom).offset(24)
make.height.equalTo(cardHeight) //
}
return v
}()
stayTunedLabel = {
let v = CLLabel()
v.font = .t.tbs
v.textColor = .c.ctsn
contentView.addSubview(v)
v.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(24)
make.top.equalTo(scrollView.snp.bottom).offset(16)
}
return v
}()
titleLabel.text = "Dialog Model"
stayTunedLabel.text = "Stay tuned for more models"
}
private func loadChatModels() {
guard let models = AppDictManager.shared.chatModels, models.count > 0 else {
//
AppDictManager.shared.loadChatModelDict { [weak self] success in
if success {
self?.loadChatModels()
}
}
return
}
chatModels = models
setupModelCards()
}
private func setupModelCards() {
//
modelCardViews.forEach { $0.removeFromSuperview() }
modelCardViews.removeAll()
//
for (index, model) in chatModels.enumerated() {
let cardView = ChatModelCardView()
cardView.configure(with: model, isSelected: model.code == currentSelectedModelCode)
cardView.tag = index
cardView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(modelCardTapped(_:))))
scrollView.addSubview(cardView)
modelCardViews.append(cardView)
cardView.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview()
make.width.equalTo(scrollView.snp.width)
make.height.equalTo(cardHeight) //
if index == 0 {
make.top.equalToSuperview()
} else {
make.top.equalTo(modelCardViews[index - 1].snp.bottom).offset(12)
}
if index == chatModels.count - 1 {
make.bottom.equalToSuperview()
}
}
}
// contentLength
updateContentLength()
//
// DispatchQueue.main.async { [weak self] in
// self?.scrollToSelectedModel()
// }
}
private func updateContentLength() {
// + +
let baseHeight: CGFloat = 32 + 24 + 16 + 20 + UIWindow.safeAreaBottom// titleLabel top + spacing + stayTunedLabel + bottom margin
// 200 + 12
let cardHeight: CGFloat = cardHeight
let cardSpacing: CGFloat = 12
let totalCardHeight = CGFloat(chatModels.count) * cardHeight + CGFloat(max(0, chatModels.count - 1)) * cardSpacing
// 2
let maxVisibleHeight = min(totalCardHeight, cardHeight * 2 + cardSpacing)
// scrollView
scrollView.snp.updateConstraints { make in
make.height.equalTo(maxVisibleHeight)
}
// contentLength
contentLength = baseHeight + maxVisibleHeight + UIWindow.safeAreaBottom
//
layoutIfNeeded()
}
private func scrollToSelectedModel() {
guard let selectedCode = currentSelectedModelCode,
let selectedIndex = chatModels.firstIndex(where: { $0.code == selectedCode }),
selectedIndex < chatModels.count else { return }
// scrollViewframe
guard scrollView.frame.height > 0 else {
// frame
DispatchQueue.main.async { [weak self] in
self?.scrollToSelectedModel()
}
return
}
let offsetY = CGFloat(selectedIndex) * (cardHeight + 12) // +
scrollView.setContentOffset(CGPoint(x: 0, y: offsetY), animated: false)
}
@objc private func modelCardTapped(_ gesture: UITapGestureRecognizer) {
guard let cardView = gesture.view as? ChatModelCardView,
let index = modelCardViews.firstIndex(of: cardView),
index < chatModels.count else { return }
let selectedModel = chatModels[index]
//
modelCardViews.forEach { $0.setSelected(false) }
cardView.setSelected(true)
//
selectionCallback?(selectedModel)
//
dismiss()
}
}
// MARK: - UIScrollViewDelegate
extension ChatModePickSheet: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
//
}
}
// MARK: - ChatModelCardView
class ChatModelCardView: UIView {
private var block: UIView!
private var modelName: UILabel!
private var modelDesc: UILabel!
private var selectMark: UIImageView!
private var innerBlock: UIView!
private var queryButton: EPIconTertiaryButton!
private var priceStackView: UIStackView!
private var model: AIChatModel?
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
block = {
let v = UIView()
v.backgroundColor = .c.csen
v.cornerRadius = 16
addSubview(v)
v.snp.makeConstraints { make in
// make.edges.equalToSuperview()
make.leading.equalToSuperview().offset(24)
make.trailing.equalToSuperview().offset(-24)
make.top.bottom.equalToSuperview()
}
return v
}()
selectMark = {
let v = UIImageView()
v.image = UIImage(named: "checkmark_tick")
v.isHidden = true
block.addSubview(v)
v.snp.makeConstraints { make in
make.size.equalTo(CGSize(width: 20, height: 20))
make.top.equalToSuperview().offset(18)
make.trailing.equalToSuperview().offset(-16)
}
return v
}()
modelName = {
let v = CLLabel()
v.font = .t.tts
v.setContentCompressionResistancePriority(UILayoutPriority(749), for: .horizontal)
block.addSubview(v)
v.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(16)
make.top.equalToSuperview().offset(16)
make.trailing.lessThanOrEqualTo(selectMark.snp.leading).offset(-8)
}
return v
}()
queryButton = {
let v = EPIconTertiaryButton(radius: .round, iconSize: .xs10, iconCode: .question)
v.addTarget(self, action: #selector(tapQueryButton), for: .touchUpInside)
block.addSubview(v)
v.snp.makeConstraints { make in
make.leading.equalTo(modelName.snp.trailing).offset(4)
make.centerY.equalTo(modelName)
make.trailing.lessThanOrEqualToSuperview().offset(-16)
}
return v
}()
modelDesc = {
let v = CLLabel()
v.font = .t.tbs
v.textColor = .c.ctsn
block.addSubview(v)
v.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(16)
make.trailing.equalToSuperview().offset(-16)
make.top.equalTo(modelName.snp.bottom).offset(4)
}
return v
}()
innerBlock = {
let v = UIView()
v.backgroundColor = .c.csdn
v.cornerRadius = 12
block.addSubview(v)
v.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(16)
make.trailing.equalToSuperview().offset(-16)
//make.top.equalTo(modelDesc.snp.bottom).offset(12)
make.top.equalToSuperview().offset(76)
make.height.equalTo(108) // 76
//make.bottom.equalToSuperview().offset(-16)
}
return v
}()
priceStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 12
v.alignment = .leading
innerBlock.addSubview(v)
v.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(12)
make.trailing.equalToSuperview().offset(-12)
make.top.equalToSuperview().offset(12)
make.bottom.equalToSuperview().offset(-12)
}
return v
}()
}
func configure(with model: AIChatModel, isSelected: Bool = false) {
self.model = model
modelName.text = model.name ?? ""
modelDesc.text = model.description ?? ""
//
priceStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
//
if let price = model.textPrice {
let coin = Coin(cents: price)
let priceLabel = CLIconLabel()
priceLabel.iconImageView.image = UIImage(named: "icon_16_diamond")
priceLabel.contentLabel.text = "\(coin.thousandthsFormatted)/Text Message"
priceStackView.addArrangedSubview(priceLabel)
}
if let price = model.voicePrice {
let coin = Coin(cents: price)
let priceLabel = CLIconLabel()
priceLabel.iconImageView.image = UIImage(named: "icon_16_diamond")
priceLabel.contentLabel.text = "\(coin.thousandthsFormatted)/Send or play voice"
priceStackView.addArrangedSubview(priceLabel)
}
if let price = model.voiceChatPrice {
let coin = Coin(cents: price)
let priceLabel = CLIconLabel()
priceLabel.iconImageView.image = UIImage(named: "icon_16_diamond")
priceLabel.contentLabel.text = "\(coin.thousandthsFormatted)/ 1 min Voice call"
priceStackView.addArrangedSubview(priceLabel)
}
setSelected(isSelected)
}
func setSelected(_ selected: Bool) {
selectMark.isHidden = !selected
// block.layer.borderWidth = selected ? 2 : 0
// block.layer.borderColor = selected ? UIColor.c.cpn.cgColor : UIColor.clear.cgColor
}
@objc func tapQueryButton(){
let content = "*文本消息价格是指与角色进行文本消息对话的价格,含发送文本,发送图片,发送礼物;按条计算\n\n*发送语音消息价格是指与角色发送语音或者播放角色的语音的价格;按次计算\n\n*语音通话消息价格是指与角色进行语音电话对话的价格;按分钟计算"
let alert = Alert(title: "Tips", text: content)
let action1 = AlertAction(title: "Got it", actionStyle: .confirm) {
}
alert.addAction(action1)
alert.show()
}
}