chat setting voice actor
This commit is contained in:
parent
1cedc98838
commit
3ca9d1a516
|
|
@ -18,12 +18,13 @@ struct ImageRow: RowModel {
|
|||
let subItems: [ImageRow]? // 子项列表
|
||||
let buttleItems: [ChatButtleRow]?
|
||||
let chatModeItems: [ChatModeItem]?
|
||||
let voiceActorItems: [VoiceActorItem]?
|
||||
var isExpanded: Bool = false // 展开状态
|
||||
|
||||
var cellReuseID: String { "ChatSwipeCell" }
|
||||
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.title = title
|
||||
self.showAvatar = showAvatar
|
||||
|
|
@ -32,6 +33,7 @@ struct ImageRow: RowModel {
|
|||
self.subItems = subItems
|
||||
self.buttleItems = buttleItems
|
||||
self.chatModeItems = chatModeItems
|
||||
self.voiceActorItems = voiceActorItems
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -50,10 +52,12 @@ class ChatSwipeCell: ChatSettingBaseCell, CellConfigurable {
|
|||
return lab
|
||||
}()
|
||||
|
||||
lazy var avatarView : UIImageView = {
|
||||
var avatarView : UIImageView = {
|
||||
let imgView = UIImageView()
|
||||
imgView.cornerRadius = 10.5
|
||||
imgView.backgroundColor = .blue
|
||||
imgView.contentMode = .scaleAspectFill
|
||||
imgView.clipsToBounds = true
|
||||
return imgView
|
||||
}()
|
||||
|
||||
|
|
@ -89,6 +93,12 @@ class ChatSwipeCell: ChatSettingBaseCell, CellConfigurable {
|
|||
arrowImgView.isHidden = !row.showArrow
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 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]] = []
|
||||
|
||||
// 展开状态管理:section -> rowIndex -> isExpanded
|
||||
|
|
@ -55,7 +55,7 @@ class ChatSettingSwipeView: CLContainer {
|
|||
|
||||
rows = [
|
||||
[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")],
|
||||
[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)],
|
||||
|
|
@ -112,6 +112,7 @@ class ChatSettingSwipeView: CLContainer {
|
|||
tableView.register(SubItemsContainerCell.self, forCellReuseIdentifier: "SubItemsContainerCell")
|
||||
tableView.register(ChatButtleCollectionCell.self, forCellReuseIdentifier: "ChatButtleCollectionCell")
|
||||
tableView.register(ChatModeContainerCell.self, forCellReuseIdentifier: "ChatModeContainerCell")
|
||||
tableView.register(VoiceActorContainerCell.self, forCellReuseIdentifier: "VoiceActorContainerCell")
|
||||
tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
||||
return tableView
|
||||
}()
|
||||
|
|
@ -196,6 +197,37 @@ extension ChatSettingSwipeView: UITableViewDelegate, UITableViewDataSource {
|
|||
|
||||
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 展开
|
||||
if imageRow.title == "Chat Mode", let chatModeItems = imageRow.chatModeItems, !chatModeItems.isEmpty {
|
||||
let isExpanded = expandedStates[indexPath.section]?[indexPath.row] ?? false
|
||||
|
|
@ -310,6 +342,20 @@ extension ChatSettingSwipeView: UITableViewDelegate, UITableViewDataSource {
|
|||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
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
|
||||
if let modeRow = row as? ChatModeRow {
|
||||
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)
|
||||
]
|
||||
}
|
||||
|
||||
// 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