226 lines
7.8 KiB
Swift
226 lines
7.8 KiB
Swift
//
|
||
// 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
|
||
}
|
||
}
|
||
|