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
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|