Visual_Novel_iOS/crush/Crush/Src/Modules/Chat/ContentView/IMAIMsgContentView.swift

226 lines
7.8 KiB
Swift
Raw Normal View History

2025-10-09 10:29:35 +00:00
//
// IMAIMsgContentView.swift
// Crush
//
// Created by Leon on 2025/8/20.
//
import UIKit
import ActiveLabel
class IMAIMsgContentConfig: IMContentBaseConfig {
override func contentSize(model: SessionBaseModel) -> CGSize {
guard model.v2msg != nil else { return .zero }
let contentView = IMAIMsgContentView.init(frame: .zero)
let content = contentView.contentWith(model: model)
// Way 1 to calculate
//contentView.contentLabel.attributedText = contentView.formatAttrubuteString(string: content)
//var size = contentView.contentLabel.sizeThatFits(CGSize(width: SessionBaseModel.maxBubbleContentWidth, height: CGFloat.greatestFiniteMagnitude))
// Way 2 to calculate
let attributedString = contentView.formatAttrubuteString(string: content)
var size = attributedString.boundingRect(
with: CGSize(width: SessionBaseModel.maxBubbleContentWidth, height: .greatestFiniteMagnitude),
options: [.usesLineFragmentOrigin, .usesFontLeading],
context: nil
).size
let calculatedHeight = ceil(size.height)
size.height = calculatedHeight
// dlog("\(SessionBaseModel.maxBubbleContentWidth) :\(size.height)")
if size.height < 20 {
size = CGSize(width: size.width, height: 20)
}
return size
}
override func cellInsets(model: SessionBaseModel) -> UIEdgeInsets {
return UIEdgeInsets(top: 0, left: 24, bottom: 8, right: 16)
}
override func contentInsets(model: SessionBaseModel) -> UIEdgeInsets {
return UIEdgeInsets(top: 36, left: 16, bottom: 16, right: 16)
}
override func contentViewClass(model: SessionBaseModel) -> IMContentBaseView.Type {
return IMAIMsgContentView.self
}
}
class IMAIMsgContentView: IMContentBaseView{
var effectView: UIVisualEffectView!
var contentLabel: LineSpaceLabel! // ActiveLabel
var audioView : IMAudioFlagView!
lazy var audioHelper = IMAudioHelper()
required override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupUI() {
effectView = {
let v = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
v.alpha = 1
v.backgroundColor = .c.csedn
v.cornerRadius = 16
insertSubview(v, at: 0)
v.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 16, left: 0, bottom: 0, right: 0))
}
return v
}()
contentLabel = {
//let v = ActiveLabel()
//v.font = CLSystemToken.font(token: .tbm)
let v = LineSpaceLabel()
let typo = CLSystemToken.typography(token: .tbm)
v.config(typo)
v.textColor = UIColor.c.ctsn
v.numberOfLines = 0
v.textColor = .white
containerView.addSubview(v)
v.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
return v
}()
audioView = {
let v = IMAudioFlagView()
v.topButton.addTarget(self, action: #selector(tapAudioButton), for: .touchUpInside)
addSubview(v)
v.snp.makeConstraints { make in
make.leading.equalToSuperview()
make.top.equalToSuperview().offset(4) // -12
}
return v
}()
contentLabel.text = ""
//contentLabel.textColor = .white
// Long press gesture
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressGesture(_:)))
addGestureRecognizer(longPressGesture)
}
func contentWith(model: SessionBaseModel) -> String {
var content: String? = nil
let message = model.v2msg
if String.realEmpty(str: content) {
content = message?.text
}
// #warning("test")
// content = "(Watching her parents toast you respectfully, I feel very uncomfortable. After all, she has been standing on the top of the magic capital since she was a child. She has never seen her parents like this, but should I say that he is really handsome?) Are you?"
// dlog("content:\(String(describing: content)), createTime:\(String(describing: message?.createTime)), modifytime:\(String(describing: message?.modifyTime))")
return content ?? ""
}
func formatAttrubuteString(string: String) -> NSMutableAttributedString{
let content = string
let basic = [NSAttributedString.Key.font: UIFont.t.tbm,
NSAttributedString.Key.foregroundColor: UIColor.white,
]
let aStr = NSMutableAttributedString(string: content, attributes: basic)
//content.withAttributes([.font(.t.tbm), .textColor(.text)])
let ranges = String.findBracketRanges(in: content)
let att = [NSAttributedString.Key.foregroundColor: UIColor.c.ctsn]
for range in ranges {
aStr.addAttributes(att, range: range)
}
return aStr
}
override func refreshModel(model: SessionBaseModel) {
super.refreshModel(model: model)
let content = contentWith(model:model)
//
var speedrate = 0
if let userSpeed = IMAIViewModel.shared.aiIMInfo?.dialogueSpeechRate, let intSpeed = Int(userSpeed){
speedrate = intSpeed
}
let duration = audioHelper.calculateAudioDuration(text: content, speechRate: speedrate)
audioView.secondsLabel.text = duration.imAIaudioDurationString
contentLabel.attributedText = formatAttrubuteString(string: content)
audioView.reloadState(with: model.speechModel)
if model.autoPlayAudioOnce && model.autoPlayAlreadyPlayed == false{
tapAudioButton()
model.autoPlayAlreadyPlayed = true
}
}
// MARK: - Action
@objc private func handleLongPressGesture(_ gesture: UILongPressGestureRecognizer) {
if gesture.state == .began {
// dlog("Long press detected")
let event = IMEventModel()
event.eventType = .aiMsgLongPress
event.cellModel = self.model
event.senderView = self
delegate?.onTapAction(event: event)
}
}
@objc private func tapAudioButton(){
// let text = contentWith(model: self.model)
// Generate voice(mp3 base64string)
dlog("tap Audio button...")
let event = IMEventModel()
event.eventType = .playAITextToAudio
event.cellModel = self.model
event.senderView = self
delegate?.onTapAction(event: event)
audioView.startLoading()
}
// MARK: - Helper
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
//
let view = super.hitTest(point, with: event)
if view != nil {
return view
}
// UIButton
for subview in subviews {
// UIButton
guard subview is UIButton else { continue }
//
let convertedPoint = subview.convert(point, from: self)
//
if subview.bounds.contains(convertedPoint) {
return subview.hitTest(convertedPoint, with: event)
}
}
return nil
}
}