chat setting voice actor
This commit is contained in:
parent
1cedc98838
commit
3ca9d1a516
|
|
@ -18,12 +18,13 @@ struct ImageRow: RowModel {
|
||||||
let subItems: [ImageRow]? // 子项列表
|
let subItems: [ImageRow]? // 子项列表
|
||||||
let buttleItems: [ChatButtleRow]?
|
let buttleItems: [ChatButtleRow]?
|
||||||
let chatModeItems: [ChatModeItem]?
|
let chatModeItems: [ChatModeItem]?
|
||||||
|
let voiceActorItems: [VoiceActorItem]?
|
||||||
var isExpanded: Bool = false // 展开状态
|
var isExpanded: Bool = false // 展开状态
|
||||||
|
|
||||||
var cellReuseID: String { "ChatSwipeCell" }
|
var cellReuseID: String { "ChatSwipeCell" }
|
||||||
func cellHeight(tableWidth: CGFloat) -> CGFloat { 50 }
|
func cellHeight(tableWidth: CGFloat) -> CGFloat { 50 }
|
||||||
|
|
||||||
init(icon: String, title: String, showAvatar: Bool = false, showArrow: Bool = false, showSwitch: Bool = false, subItems: [ImageRow]? = nil, buttleItems: [ChatButtleRow]? = nil, chatModeItems: [ChatModeItem]? = nil) {
|
init(icon: String, title: String, showAvatar: Bool = false, showArrow: Bool = false, showSwitch: Bool = false, subItems: [ImageRow]? = nil, buttleItems: [ChatButtleRow]? = nil, chatModeItems: [ChatModeItem]? = nil, voiceActorItems: [VoiceActorItem]? = nil) {
|
||||||
self.icon = icon
|
self.icon = icon
|
||||||
self.title = title
|
self.title = title
|
||||||
self.showAvatar = showAvatar
|
self.showAvatar = showAvatar
|
||||||
|
|
@ -32,6 +33,7 @@ struct ImageRow: RowModel {
|
||||||
self.subItems = subItems
|
self.subItems = subItems
|
||||||
self.buttleItems = buttleItems
|
self.buttleItems = buttleItems
|
||||||
self.chatModeItems = chatModeItems
|
self.chatModeItems = chatModeItems
|
||||||
|
self.voiceActorItems = voiceActorItems
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,10 +52,12 @@ class ChatSwipeCell: ChatSettingBaseCell, CellConfigurable {
|
||||||
return lab
|
return lab
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var avatarView : UIImageView = {
|
var avatarView : UIImageView = {
|
||||||
let imgView = UIImageView()
|
let imgView = UIImageView()
|
||||||
imgView.cornerRadius = 10.5
|
imgView.cornerRadius = 10.5
|
||||||
imgView.backgroundColor = .blue
|
imgView.backgroundColor = .blue
|
||||||
|
imgView.contentMode = .scaleAspectFill
|
||||||
|
imgView.clipsToBounds = true
|
||||||
return imgView
|
return imgView
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -89,6 +93,12 @@ class ChatSwipeCell: ChatSettingBaseCell, CellConfigurable {
|
||||||
arrowImgView.isHidden = !row.showArrow
|
arrowImgView.isHidden = !row.showArrow
|
||||||
switchControl.isHidden = !row.showSwitch
|
switchControl.isHidden = !row.showSwitch
|
||||||
|
|
||||||
|
// 如果有 voice actor items,设置默认选中的头像
|
||||||
|
if row.showAvatar, let voiceActorItems = row.voiceActorItems,
|
||||||
|
let selectedItem = voiceActorItems.first(where: { $0.isSelected }) {
|
||||||
|
avatarView.image = UIImage(named: selectedItem.avatarImage)
|
||||||
|
}
|
||||||
|
|
||||||
// 更新箭头状态
|
// 更新箭头状态
|
||||||
updateArrowState(isExpanded: row.isExpanded)
|
updateArrowState(isExpanded: row.isExpanded)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,502 @@
|
||||||
|
//
|
||||||
|
// VoiceActorContainerCell.swift
|
||||||
|
// Visual_Novel_iOS
|
||||||
|
//
|
||||||
|
// Created by mh on 2025/01/27.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SnapKit
|
||||||
|
|
||||||
|
// MARK: - 数据模型
|
||||||
|
struct VoiceActorItem {
|
||||||
|
let id: String
|
||||||
|
let name: String
|
||||||
|
let description: String
|
||||||
|
let avatarImage: String
|
||||||
|
let gender: String // "male", "female", "all"
|
||||||
|
var isSelected: Bool = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - RowModel
|
||||||
|
struct VoiceActorRow: RowModel {
|
||||||
|
let items: [VoiceActorItem]
|
||||||
|
var cellReuseID: String { "VoiceActorContainerCell" }
|
||||||
|
|
||||||
|
func cellHeight(tableWidth: CGFloat) -> CGFloat {
|
||||||
|
let filterHeight: CGFloat = 50 // 筛选器高度
|
||||||
|
let maxRows: CGFloat = 5.5 // 最多显示5.5行
|
||||||
|
let rowHeight: CGFloat = 80 // 每行高度
|
||||||
|
let maxListHeight = maxRows * rowHeight
|
||||||
|
return filterHeight + maxListHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - VoiceActorContainerCell
|
||||||
|
class VoiceActorContainerCell: UITableViewCell, CellConfigurable {
|
||||||
|
|
||||||
|
private var allItems: [VoiceActorItem] = []
|
||||||
|
private var filteredItems: [VoiceActorItem] = []
|
||||||
|
private var selectedItemId: String?
|
||||||
|
private var tableViewHeightConstraint: Constraint?
|
||||||
|
var selectedItemChanged: ((VoiceActorItem) -> Void)? // 选中项改变回调
|
||||||
|
|
||||||
|
// MARK: - 筛选器视图
|
||||||
|
private lazy var filterView: VoiceActorFilterView = {
|
||||||
|
let view = VoiceActorFilterView()
|
||||||
|
view.filterChanged = { [weak self] filterType in
|
||||||
|
self?.filterItems(by: filterType)
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var tableView: UITableView = {
|
||||||
|
let tableView = UITableView(frame: .zero, style: .plain)
|
||||||
|
tableView.separatorStyle = .none
|
||||||
|
tableView.delegate = self
|
||||||
|
tableView.dataSource = self
|
||||||
|
tableView.backgroundColor = UIColor(hex: "#F5F5FF")
|
||||||
|
tableView.showsVerticalScrollIndicator = true
|
||||||
|
tableView.isScrollEnabled = true
|
||||||
|
tableView.register(VoiceActorItemCell.self, forCellReuseIdentifier: "VoiceActorItemCell")
|
||||||
|
tableView.estimatedRowHeight = 80
|
||||||
|
tableView.rowHeight = 80
|
||||||
|
tableView.contentInset = .zero
|
||||||
|
tableView.scrollIndicatorInsets = .zero
|
||||||
|
tableView.showsVerticalScrollIndicator = false
|
||||||
|
tableView.bounces = false
|
||||||
|
tableView.layer.cornerRadius = 15
|
||||||
|
tableView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||||
|
return tableView
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
setupViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupViews() {
|
||||||
|
backgroundColor = .clear
|
||||||
|
selectionStyle = .none
|
||||||
|
|
||||||
|
contentView.addSubview(filterView)
|
||||||
|
contentView.addSubview(tableView)
|
||||||
|
|
||||||
|
filterView.snp.makeConstraints { make in
|
||||||
|
make.left.right.equalToSuperview().inset(20)
|
||||||
|
make.top.equalToSuperview().offset(2.5)
|
||||||
|
make.height.equalTo(50)
|
||||||
|
}
|
||||||
|
|
||||||
|
tableView.snp.makeConstraints { make in
|
||||||
|
make.left.right.equalToSuperview().inset(20)
|
||||||
|
make.top.equalTo(filterView.snp.bottom)
|
||||||
|
make.bottom.equalToSuperview().inset(2.5)
|
||||||
|
tableViewHeightConstraint = make.height.equalTo(0).constraint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func configure(with row: RowModel) {
|
||||||
|
guard let voiceActorRow = row as? VoiceActorRow else { return }
|
||||||
|
allItems = voiceActorRow.items
|
||||||
|
|
||||||
|
// 恢复选中状态
|
||||||
|
if let selected = allItems.first(where: { $0.isSelected }) {
|
||||||
|
selectedItemId = selected.id
|
||||||
|
}
|
||||||
|
|
||||||
|
filterItems(by: .all)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func filterItems(by filterType: VoiceActorFilterView.FilterType) {
|
||||||
|
var items: [VoiceActorItem]
|
||||||
|
|
||||||
|
switch filterType {
|
||||||
|
case .all:
|
||||||
|
items = allItems
|
||||||
|
case .male:
|
||||||
|
items = allItems.filter { $0.gender == "male" || $0.gender == "all" }
|
||||||
|
case .female:
|
||||||
|
items = allItems.filter { $0.gender == "female" || $0.gender == "all" }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复选中状态
|
||||||
|
filteredItems = items.map { item in
|
||||||
|
var mutableItem = item
|
||||||
|
mutableItem.isSelected = (item.id == selectedItemId)
|
||||||
|
return mutableItem
|
||||||
|
}
|
||||||
|
|
||||||
|
tableView.reloadData()
|
||||||
|
updateTableViewHeight()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateTableViewHeight() {
|
||||||
|
let rowHeight: CGFloat = 80
|
||||||
|
let maxRows: CGFloat = 5.5
|
||||||
|
let displayCount = min(CGFloat(filteredItems.count), maxRows)
|
||||||
|
let height = displayCount * rowHeight
|
||||||
|
|
||||||
|
tableViewHeightConstraint?.update(offset: height)
|
||||||
|
layoutIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UITableViewDataSource & UITableViewDelegate
|
||||||
|
extension VoiceActorContainerCell: UITableViewDataSource, UITableViewDelegate {
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
|
return filteredItems.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: "VoiceActorItemCell", for: indexPath) as! VoiceActorItemCell
|
||||||
|
let item = filteredItems[indexPath.row]
|
||||||
|
|
||||||
|
// 交替背景色(浅紫色和浅蓝色)
|
||||||
|
let bgColor = indexPath.row % 2 == 0 ? UIColor(hex: "#F5F5FF") : UIColor(hex: "#E8F0FF")
|
||||||
|
cell.configure(with: item, backgroundColor: bgColor)
|
||||||
|
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
|
|
||||||
|
let item = filteredItems[indexPath.row]
|
||||||
|
selectedItemId = item.id
|
||||||
|
|
||||||
|
// 更新所有数据的选中状态
|
||||||
|
allItems = allItems.map { var i = $0; i.isSelected = (i.id == item.id); return i }
|
||||||
|
filteredItems = filteredItems.map { var i = $0; i.isSelected = (i.id == item.id); return i }
|
||||||
|
|
||||||
|
tableView.reloadData()
|
||||||
|
|
||||||
|
// 通知外部选中项改变
|
||||||
|
selectedItemChanged?(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, didHighlightRowAt indexPath: IndexPath) {
|
||||||
|
if let cell = tableView.cellForRow(at: indexPath) as? VoiceActorItemCell {
|
||||||
|
cell.isHovered = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, didUnhighlightRowAt indexPath: IndexPath) {
|
||||||
|
if let cell = tableView.cellForRow(at: indexPath) as? VoiceActorItemCell {
|
||||||
|
cell.isHovered = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - VoiceActorFilterView
|
||||||
|
class VoiceActorFilterView: UIView {
|
||||||
|
enum FilterType: String {
|
||||||
|
case all = "All"
|
||||||
|
case male = "Male"
|
||||||
|
case female = "Female"
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedFilter: FilterType = .all {
|
||||||
|
didSet {
|
||||||
|
updateButtonStates()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var filterChanged: ((FilterType) -> Void)?
|
||||||
|
|
||||||
|
lazy var filterStackView: UIStackView = {
|
||||||
|
let stackView = UIStackView(arrangedSubviews: [allButton, maleButton, femaleButton])
|
||||||
|
stackView.spacing = 20
|
||||||
|
stackView.alignment = .center
|
||||||
|
// 让 stackView 按内容自适应宽度,不强行拉伸
|
||||||
|
stackView.distribution = .fill
|
||||||
|
// 提高水平方向的 Hugging/Compression,保证“贴内容”
|
||||||
|
stackView.setContentHuggingPriority(.required, for: .horizontal)
|
||||||
|
stackView.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||||
|
return stackView
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var allButton: UIView = {
|
||||||
|
return createFilterButton(title: "All", tag: 0)
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var maleButton: UIView = {
|
||||||
|
return createFilterButton(title: "Male", tag: 1)
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var femaleButton: UIView = {
|
||||||
|
return createFilterButton(title: "Female", tag: 2)
|
||||||
|
}()
|
||||||
|
|
||||||
|
private var filledCircles: [UIView: UIView] = [:]
|
||||||
|
private var circleBorders: [UIView: UIView] = [:]
|
||||||
|
private var titleLabels: [UIView: UILabel] = [:]
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.cornerRadius = 15.0
|
||||||
|
setupViews()
|
||||||
|
updateButtonStates()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupViews() {
|
||||||
|
backgroundColor = UIColor(hex: "#1A1A2E")
|
||||||
|
addSubview(filterStackView)
|
||||||
|
|
||||||
|
filterStackView.snp.makeConstraints { make in
|
||||||
|
make.left.equalToSuperview().inset(12)
|
||||||
|
make.centerY.equalToSuperview()
|
||||||
|
// 根据内容自适应,最长不超过右侧 12 间距
|
||||||
|
make.right.lessThanOrEqualToSuperview().inset(12)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createFilterButton(title: String, tag: Int) -> UIView {
|
||||||
|
// 使用自定义容器视图替代 UIButton,以便精确控制布局
|
||||||
|
let containerView = UIView()
|
||||||
|
containerView.tag = tag
|
||||||
|
|
||||||
|
// 创建单选按钮图标(圆圈)
|
||||||
|
let circleView = UIView()
|
||||||
|
circleView.backgroundColor = .clear
|
||||||
|
circleView.layer.borderWidth = 2
|
||||||
|
// 默认 60% 白色描边
|
||||||
|
circleView.layer.borderColor = UIColor(white: 1, alpha: 0.6).cgColor
|
||||||
|
circleView.layer.cornerRadius = 5
|
||||||
|
containerView.addSubview(circleView)
|
||||||
|
|
||||||
|
let filledCircle = UIView()
|
||||||
|
filledCircle.backgroundColor = .white
|
||||||
|
filledCircle.layer.cornerRadius = 5
|
||||||
|
filledCircle.isHidden = true
|
||||||
|
containerView.addSubview(filledCircle)
|
||||||
|
|
||||||
|
// 创建文字标签
|
||||||
|
let titleLabel = UILabel()
|
||||||
|
titleLabel.text = title
|
||||||
|
titleLabel.textColor = UIColor(white: 1, alpha: 0.6) // 默认未选中状态
|
||||||
|
titleLabel.font = UIFont.systemFont(ofSize: 14)
|
||||||
|
titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||||
|
titleLabel.setContentHuggingPriority(.required, for: .horizontal)
|
||||||
|
containerView.addSubview(titleLabel)
|
||||||
|
|
||||||
|
// 布局约束:圆圈 (10x10) -> 间距 (5pt) -> 文字标签
|
||||||
|
circleView.snp.makeConstraints { make in
|
||||||
|
make.left.equalToSuperview()
|
||||||
|
make.centerY.equalToSuperview()
|
||||||
|
make.width.height.equalTo(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
filledCircle.snp.makeConstraints { make in
|
||||||
|
make.center.equalTo(circleView)
|
||||||
|
make.width.height.equalTo(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
titleLabel.snp.makeConstraints { make in
|
||||||
|
make.left.equalTo(circleView.snp.right).offset(5) // 5pt 间距
|
||||||
|
make.right.equalToSuperview()
|
||||||
|
make.centerY.equalToSuperview()
|
||||||
|
make.top.bottom.greaterThanOrEqualToSuperview() // 确保容器高度足够
|
||||||
|
}
|
||||||
|
|
||||||
|
// 容器视图的约束:根据内容自适应
|
||||||
|
containerView.setContentHuggingPriority(.required, for: .horizontal)
|
||||||
|
containerView.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||||
|
|
||||||
|
// 添加点击手势
|
||||||
|
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(filterButtonTapped(_:)))
|
||||||
|
containerView.addGestureRecognizer(tapGesture)
|
||||||
|
containerView.isUserInteractionEnabled = true
|
||||||
|
|
||||||
|
// 保存引用以便更新状态
|
||||||
|
filledCircles[containerView] = filledCircle
|
||||||
|
circleBorders[containerView] = circleView
|
||||||
|
titleLabels[containerView] = titleLabel
|
||||||
|
|
||||||
|
return containerView
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func filterButtonTapped(_ sender: UITapGestureRecognizer) {
|
||||||
|
guard let containerView = sender.view else { return }
|
||||||
|
let filters: [FilterType] = [.all, .male, .female]
|
||||||
|
selectedFilter = filters[containerView.tag]
|
||||||
|
filterChanged?(selectedFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateButtonStates() {
|
||||||
|
let buttons = [allButton, maleButton, femaleButton]
|
||||||
|
let filters: [FilterType] = [.all, .male, .female]
|
||||||
|
|
||||||
|
for (index, button) in buttons.enumerated() {
|
||||||
|
let isSelected = filters[index] == selectedFilter
|
||||||
|
// 选中:文字纯白、显示实心圆
|
||||||
|
// 未选中:文字 60% 白、空心圆(60% 白描边)
|
||||||
|
titleLabels[button]?.textColor = UIColor(white: 1, alpha: isSelected ? 1.0 : 0.6)
|
||||||
|
filledCircles[button]?.isHidden = !isSelected
|
||||||
|
circleBorders[button]?.layer.borderColor = UIColor(white: 1, alpha: isSelected ? 1.0 : 0.6).cgColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - VoiceActorItemCell
|
||||||
|
class VoiceActorItemCell: UITableViewCell {
|
||||||
|
|
||||||
|
private lazy var containerView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.layer.cornerRadius = 12
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var avatarImageView: UIImageView = {
|
||||||
|
let view = UIImageView()
|
||||||
|
view.cornerRadius = 27.5
|
||||||
|
view.backgroundColor = .darkText
|
||||||
|
view.contentMode = .scaleAspectFill
|
||||||
|
view.clipsToBounds = true
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var playButton: UIButton = {
|
||||||
|
let btn = UIButton(type: .custom)
|
||||||
|
if let playImage = UIImage(named: "voice_play") {
|
||||||
|
btn.setImage(playImage, for: .normal)
|
||||||
|
} else {
|
||||||
|
let config = UIImage.SymbolConfiguration(pointSize: 10, weight: .bold)
|
||||||
|
btn.setImage(UIImage(systemName: "play.fill", withConfiguration: config), for: .normal)
|
||||||
|
btn.tintColor = .white
|
||||||
|
}
|
||||||
|
btn.backgroundColor = UIColor(hex: "#0066FF")
|
||||||
|
btn.layer.cornerRadius = 12
|
||||||
|
btn.imageEdgeInsets = UIEdgeInsets(top: 6, left: 8, bottom: 6, right: 8)
|
||||||
|
btn.imageView?.contentMode = .scaleAspectFit
|
||||||
|
return btn
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var titleLabel: UILabel = {
|
||||||
|
let lab = UILabel()
|
||||||
|
lab.textColor = UIColor(hex: "#333333")
|
||||||
|
lab.font = UIFont.systemFont(ofSize: 13, weight: .medium)
|
||||||
|
return lab
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var subTitleLab: UILabel = {
|
||||||
|
let lab = UILabel()
|
||||||
|
lab.textColor = UIColor(hex: "#9494C3")
|
||||||
|
lab.font = UIFont.italicSystemFont(ofSize: 11)
|
||||||
|
return lab
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var selectionIndicator: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.layer.cornerRadius = 10
|
||||||
|
view.backgroundColor = UIColor.black
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private var hoverBorderView: UIView?
|
||||||
|
|
||||||
|
var item: VoiceActorItem?
|
||||||
|
var isHovered: Bool = false {
|
||||||
|
didSet {
|
||||||
|
updateHoverState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
setupViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupViews() {
|
||||||
|
backgroundColor = .clear
|
||||||
|
selectionStyle = .none
|
||||||
|
|
||||||
|
contentView.addSubview(containerView)
|
||||||
|
containerView.addSubview(avatarImageView)
|
||||||
|
containerView.addSubview(playButton)
|
||||||
|
containerView.addSubview(titleLabel)
|
||||||
|
containerView.addSubview(subTitleLab)
|
||||||
|
containerView.addSubview(selectionIndicator)
|
||||||
|
|
||||||
|
containerView.snp.makeConstraints { make in
|
||||||
|
make.left.right.equalToSuperview()
|
||||||
|
make.top.bottom.equalToSuperview().inset(2.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
avatarImageView.snp.makeConstraints { make in
|
||||||
|
make.left.top.equalToSuperview().offset(10)
|
||||||
|
make.width.height.equalTo(55)
|
||||||
|
make.bottom.equalToSuperview().offset(-10)
|
||||||
|
}
|
||||||
|
|
||||||
|
playButton.snp.makeConstraints { make in
|
||||||
|
make.bottom.right.equalTo(avatarImageView).offset(2)
|
||||||
|
make.width.height.equalTo(24)
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionIndicator.snp.makeConstraints { make in
|
||||||
|
make.centerY.equalToSuperview()
|
||||||
|
make.right.equalToSuperview().inset(10)
|
||||||
|
make.width.height.equalTo(20)
|
||||||
|
}
|
||||||
|
|
||||||
|
titleLabel.snp.makeConstraints { make in
|
||||||
|
make.left.equalTo(avatarImageView.snp.right).offset(10)
|
||||||
|
make.right.equalTo(selectionIndicator.snp.left).offset(-10)
|
||||||
|
make.centerY.equalToSuperview().offset(-8)
|
||||||
|
}
|
||||||
|
|
||||||
|
subTitleLab.snp.makeConstraints { make in
|
||||||
|
make.left.right.equalTo(titleLabel)
|
||||||
|
make.top.equalTo(titleLabel.snp.bottom).offset(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建hover边框视图
|
||||||
|
hoverBorderView = UIView()
|
||||||
|
hoverBorderView?.backgroundColor = .clear
|
||||||
|
hoverBorderView?.layer.borderWidth = 2
|
||||||
|
hoverBorderView?.layer.borderColor = UIColor(hex: "#FF69B4").cgColor
|
||||||
|
hoverBorderView?.layer.cornerRadius = 14
|
||||||
|
hoverBorderView?.isHidden = true
|
||||||
|
if let hoverView = hoverBorderView {
|
||||||
|
containerView.addSubview(hoverView)
|
||||||
|
hoverView.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func configure(with item: VoiceActorItem, backgroundColor: UIColor? = nil) {
|
||||||
|
self.item = item
|
||||||
|
avatarImageView.image = UIImage(named: item.avatarImage)
|
||||||
|
titleLabel.text = item.name
|
||||||
|
subTitleLab.text = item.description
|
||||||
|
|
||||||
|
// 设置背景色(交替)
|
||||||
|
containerView.backgroundColor = backgroundColor ?? UIColor(hex: "#F5F5FF")
|
||||||
|
|
||||||
|
updateSelectionState(isSelected: item.isSelected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateSelectionState(isSelected: Bool) {
|
||||||
|
selectionIndicator.backgroundColor = isSelected ? UIColor(hex: "#00CC88") : UIColor.black
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateHoverState() {
|
||||||
|
hoverBorderView?.isHidden = !isHovered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,7 +12,7 @@ class ChatSettingSwipeView: CLContainer {
|
||||||
|
|
||||||
var closeAction: (()->Void)?
|
var closeAction: (()->Void)?
|
||||||
|
|
||||||
var sectionTitle: [String] = ["Switch Model", "Sound", "Maximum number of response tokens Maximum number of response tokens", "Appearance", "Background", "Historical Archives"]
|
var sectionTitle: [String] = ["Switch Model", "Sound", "Maximum number of response tokens", "Appearance", "Background", "Historical Archives"]
|
||||||
var rows: [[RowModel]] = []
|
var rows: [[RowModel]] = []
|
||||||
|
|
||||||
// 展开状态管理:section -> rowIndex -> isExpanded
|
// 展开状态管理:section -> rowIndex -> isExpanded
|
||||||
|
|
@ -55,7 +55,7 @@ class ChatSettingSwipeView: CLContainer {
|
||||||
|
|
||||||
rows = [
|
rows = [
|
||||||
[modelRow, ImageRow(icon: "role_text_mode", title: "Short Text Mode", showAvatar: false, showArrow: false, showSwitch: true)],
|
[modelRow, ImageRow(icon: "role_text_mode", title: "Short Text Mode", showAvatar: false, showArrow: false, showSwitch: true)],
|
||||||
[ImageRow(icon: "role_voice", title: "Voice actor", showAvatar: true, showArrow: true, showSwitch: false), ImageRow(icon: "role_talk", title: "Play dialogue only", showAvatar: false, showArrow: false, showSwitch: true)],
|
[ImageRow(icon: "role_voice", title: "Voice actor", showAvatar: true, showArrow: true, showSwitch: false, voiceActorItems: createVoiceActorItems()), ImageRow(icon: "role_talk", title: "Play dialogue only", showAvatar: false, showArrow: false, showSwitch: true)],
|
||||||
[TokenRow(count: "2500")],
|
[TokenRow(count: "2500")],
|
||||||
[FontRow(count: "20", icon: "role_font", title: "Font size"), ImageRow(icon: "role_chat_mode", title: "Chat Mode", showAvatar: false, showArrow: true, showSwitch: false, chatModeItems: createChatModeItems()), buttleRow],
|
[FontRow(count: "20", icon: "role_font", title: "Font size"), ImageRow(icon: "role_chat_mode", title: "Chat Mode", showAvatar: false, showArrow: true, showSwitch: false, chatModeItems: createChatModeItems()), buttleRow],
|
||||||
[BackgroundRow(count: 50)],
|
[BackgroundRow(count: 50)],
|
||||||
|
|
@ -112,6 +112,7 @@ class ChatSettingSwipeView: CLContainer {
|
||||||
tableView.register(SubItemsContainerCell.self, forCellReuseIdentifier: "SubItemsContainerCell")
|
tableView.register(SubItemsContainerCell.self, forCellReuseIdentifier: "SubItemsContainerCell")
|
||||||
tableView.register(ChatButtleCollectionCell.self, forCellReuseIdentifier: "ChatButtleCollectionCell")
|
tableView.register(ChatButtleCollectionCell.self, forCellReuseIdentifier: "ChatButtleCollectionCell")
|
||||||
tableView.register(ChatModeContainerCell.self, forCellReuseIdentifier: "ChatModeContainerCell")
|
tableView.register(ChatModeContainerCell.self, forCellReuseIdentifier: "ChatModeContainerCell")
|
||||||
|
tableView.register(VoiceActorContainerCell.self, forCellReuseIdentifier: "VoiceActorContainerCell")
|
||||||
tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
||||||
return tableView
|
return tableView
|
||||||
}()
|
}()
|
||||||
|
|
@ -196,6 +197,37 @@ extension ChatSettingSwipeView: UITableViewDelegate, UITableViewDataSource {
|
||||||
|
|
||||||
tableView.deselectRow(at: indexPath, animated: true)
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
|
|
||||||
|
// 处理 Voice actor 展开
|
||||||
|
if imageRow.title == "Voice actor", let voiceActorItems = imageRow.voiceActorItems, !voiceActorItems.isEmpty {
|
||||||
|
let isExpanded = expandedStates[indexPath.section]?[indexPath.row] ?? false
|
||||||
|
expandedStates[indexPath.section, default: [:]][indexPath.row] = !isExpanded
|
||||||
|
|
||||||
|
if !isExpanded {
|
||||||
|
// 展开:插入 VoiceActorRow
|
||||||
|
let actorRow = VoiceActorRow(items: voiceActorItems)
|
||||||
|
rows[indexPath.section].insert(actorRow, at: indexPath.row + 1)
|
||||||
|
let insertIndexPath = IndexPath(row: indexPath.row + 1, section: indexPath.section)
|
||||||
|
|
||||||
|
tableView.performBatchUpdates({
|
||||||
|
tableView.insertRows(at: [insertIndexPath], with: .fade)
|
||||||
|
tableView.reloadRows(at: [indexPath], with: .none)
|
||||||
|
}, completion: nil)
|
||||||
|
} else {
|
||||||
|
// 折叠
|
||||||
|
if indexPath.row + 1 < rows[indexPath.section].count,
|
||||||
|
rows[indexPath.section][indexPath.row + 1] is VoiceActorRow {
|
||||||
|
rows[indexPath.section].remove(at: indexPath.row + 1)
|
||||||
|
let deleteIndexPath = IndexPath(row: indexPath.row + 1, section: indexPath.section)
|
||||||
|
|
||||||
|
tableView.performBatchUpdates({
|
||||||
|
tableView.deleteRows(at: [deleteIndexPath], with: .fade)
|
||||||
|
tableView.reloadRows(at: [indexPath], with: .none)
|
||||||
|
}, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 处理 Chat Mode 展开
|
// 处理 Chat Mode 展开
|
||||||
if imageRow.title == "Chat Mode", let chatModeItems = imageRow.chatModeItems, !chatModeItems.isEmpty {
|
if imageRow.title == "Chat Mode", let chatModeItems = imageRow.chatModeItems, !chatModeItems.isEmpty {
|
||||||
let isExpanded = expandedStates[indexPath.section]?[indexPath.row] ?? false
|
let isExpanded = expandedStates[indexPath.section]?[indexPath.row] ?? false
|
||||||
|
|
@ -310,6 +342,20 @@ extension ChatSettingSwipeView: UITableViewDelegate, UITableViewDataSource {
|
||||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
let row = rows[indexPath.section][indexPath.row]
|
let row = rows[indexPath.section][indexPath.row]
|
||||||
|
|
||||||
|
// 处理VoiceActorRow
|
||||||
|
if let actorRow = row as? VoiceActorRow {
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: "VoiceActorContainerCell", for: indexPath) as! VoiceActorContainerCell
|
||||||
|
cell.selectionStyle = .none
|
||||||
|
|
||||||
|
// 设置选中回调,更新父cell头像
|
||||||
|
cell.selectedItemChanged = { [weak self] item in
|
||||||
|
self?.updateVoiceActorAvatar(item, at: indexPath.section, rowIndex: indexPath.row - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
cell.configure(with: actorRow)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
// 处理ChatModeRow
|
// 处理ChatModeRow
|
||||||
if let modeRow = row as? ChatModeRow {
|
if let modeRow = row as? ChatModeRow {
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: "ChatModeContainerCell", for: indexPath) as! ChatModeContainerCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: "ChatModeContainerCell", for: indexPath) as! ChatModeContainerCell
|
||||||
|
|
@ -382,4 +428,52 @@ extension ChatSettingSwipeView: UITableViewDelegate, UITableViewDataSource {
|
||||||
ChatModeItem(id: "2", title: "Immersive Mode", subtitle: "Previous-generation large model", isVip: true, isSelected: false)
|
ChatModeItem(id: "2", title: "Immersive Mode", subtitle: "Previous-generation large model", isVip: true, isSelected: false)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - 创建VoiceActor示例数据
|
||||||
|
private func createVoiceActorItems() -> [VoiceActorItem] {
|
||||||
|
return [
|
||||||
|
VoiceActorItem(id: "1", name: "Name 1", description: "Anime-Style Girl", avatarImage: "voice_actor_1", gender: "female", isSelected: true),
|
||||||
|
VoiceActorItem(id: "2", name: "Name 2", description: "Anime-Style Girl", avatarImage: "voice_actor_1", gender: "female", isSelected: false),
|
||||||
|
VoiceActorItem(id: "3", name: "Name 3", description: "Anime-Style Boy", avatarImage: "voice_actor_2", gender: "male", isSelected: false),
|
||||||
|
VoiceActorItem(id: "4", name: "Name 4", description: "Anime-Style Girl", avatarImage: "voice_actor_1", gender: "female", isSelected: false),
|
||||||
|
VoiceActorItem(id: "5", name: "Name 5", description: "Anime-Style Girl", avatarImage: "voice_actor_1", gender: "female", isSelected: false),
|
||||||
|
VoiceActorItem(id: "6", name: "Name 6", description: "Anime-Style Boy", avatarImage: "voice_actor_2", gender: "male", isSelected: false),
|
||||||
|
VoiceActorItem(id: "7", name: "Name 7", description: "Anime-Style Girl", avatarImage: "voice_actor_1", gender: "female", isSelected: false),
|
||||||
|
VoiceActorItem(id: "8", name: "Name 8", description: "Anime-Style Boy", avatarImage: "voice_actor_2", gender: "male", isSelected: false),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 更新Voice Actor头像
|
||||||
|
private func updateVoiceActorAvatar(_ item: VoiceActorItem, at section: Int, rowIndex: Int) {
|
||||||
|
guard section >= 0 && section < rows.count,
|
||||||
|
rowIndex >= 0 && rowIndex < rows[section].count else { return }
|
||||||
|
|
||||||
|
// 更新数据源中的选中状态
|
||||||
|
if var imageRow = rows[section][rowIndex] as? ImageRow,
|
||||||
|
var voiceActorItems = imageRow.voiceActorItems {
|
||||||
|
// 更新所有items的选中状态
|
||||||
|
voiceActorItems = voiceActorItems.map { var i = $0; i.isSelected = (i.id == item.id); return i }
|
||||||
|
|
||||||
|
// 由于 ImageRow 是 struct,需要重新创建
|
||||||
|
let updatedRow = ImageRow(
|
||||||
|
icon: imageRow.icon,
|
||||||
|
title: imageRow.title,
|
||||||
|
showAvatar: imageRow.showAvatar,
|
||||||
|
showArrow: imageRow.showArrow,
|
||||||
|
showSwitch: imageRow.showSwitch,
|
||||||
|
subItems: imageRow.subItems,
|
||||||
|
buttleItems: imageRow.buttleItems,
|
||||||
|
chatModeItems: imageRow.chatModeItems,
|
||||||
|
voiceActorItems: voiceActorItems
|
||||||
|
)
|
||||||
|
rows[section][rowIndex] = updatedRow
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新父cell以更新头像
|
||||||
|
let parentIndexPath = IndexPath(row: rowIndex, section: section)
|
||||||
|
if let cell = tableView.cellForRow(at: parentIndexPath) as? ChatSwipeCell {
|
||||||
|
// 更新avatarView的图像
|
||||||
|
cell.avatarView.image = UIImage(named: item.avatarImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue