chat setting mode

This commit is contained in:
mh 2025-10-29 18:09:34 +08:00
parent 17f5eba096
commit ef162eea2c
4 changed files with 476 additions and 21 deletions

View File

@ -6,9 +6,12 @@
//
import UIKit
import SnapKit
class ChatSettingBaseCell: UITableViewCell {
var containerHeightConstraint: Constraint?
lazy var containerView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hex: "#F6F6F6")
@ -33,7 +36,11 @@ class ChatSettingBaseCell: UITableViewCell {
containerView.snp.makeConstraints { make in
make.left.right.equalToSuperview().inset(20)
make.top.bottom.equalToSuperview().inset(2.5)
make.height.equalTo(45.0)
containerHeightConstraint = make.height.equalTo(45.0).constraint
}
}
func updateContainerHeight(_ height: CGFloat) {
containerHeightConstraint?.update(offset: height)
}
}

View File

@ -6,6 +6,7 @@
//
import UIKit
import SnapKit
// icon title
struct ImageRow: RowModel {
@ -14,8 +15,20 @@ struct ImageRow: RowModel {
let showAvatar: Bool
let showArrow: Bool
let showSwitch: Bool
let subItems: [ImageRow]? //
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) {
self.icon = icon
self.title = title
self.showAvatar = showAvatar
self.showArrow = showArrow
self.showSwitch = showSwitch
self.subItems = subItems
}
}
class ChatSwipeCell: ChatSettingBaseCell, CellConfigurable {
@ -61,13 +74,28 @@ class ChatSwipeCell: ChatSettingBaseCell, CellConfigurable {
return stackView
}()
private var currentRow: ImageRow?
func configure(with row: RowModel) {
guard let row = row as? ImageRow else { return }
currentRow = row
iconImgView.image = UIImage(named: row.icon)
titleLab.text = row.title
avatarView.isHidden = !row.showAvatar
arrowImgView.isHidden = !row.showArrow
switchControl.isHidden = !row.showSwitch
//
updateArrowState(isExpanded: row.isExpanded)
// container50
updateContainerHeight(50)
}
private func updateArrowState(isExpanded: Bool) {
UIView.animate(withDuration: 0.3) { [weak self] in
self?.arrowImgView.transform = isExpanded ? CGAffineTransform(rotationAngle: .pi / 2.0) : .identity
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {

View File

@ -0,0 +1,140 @@
////
//// SubModeCell.swift
//// Visual_Novel_iOS
////
//// Created by mh on 2025/10/29.
////
//
//import UIKit
//
//// SubItemsRow - cell
//struct SubItemsRow: RowModel {
// let subItems: [ImageRow]
// var cellReuseID: String { "SubItemsContainerCell" }
// func cellHeight(tableWidth: CGFloat) -> CGFloat {
// let maxDisplayCount = 4
// let displayCount = min(subItems.count, maxDisplayCount)
// return CGFloat(displayCount) * 40 // 40
// }
//}
//
//// SubItemsContainerCell - cell
//class SubModeCell: UITableViewCell, CellConfigurable {
//
// private var subItems: [ImageRow] = []
// private var tableViewHeightConstraint: Constraint?
//
// 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(SubItemCell.self, forCellReuseIdentifier: "SubItemCell")
// tableView.estimatedRowHeight = 40
// tableView.rowHeight = 40
// tableView.contentInset = .zero
// tableView.scrollIndicatorInsets = .zero
// tableView.bounces = false
// tableView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] //
// tableView.layer.cornerRadius = 15
// 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(tableView)
//
// tableView.snp.makeConstraints { make in
// make.left.right.equalToSuperview().inset(20)
// make.top.bottom.equalToSuperview().inset(2.5)
// tableViewHeightConstraint = make.height.equalTo(0).constraint
// }
// }
//
// func configure(with row: RowModel) {
// guard let subItemsRow = row as? SubItemsRow else { return }
// subItems = subItemsRow.subItems
//
// tableView.reloadData()
// updateTableViewHeight()
// }
//
// private func updateTableViewHeight() {
// let maxDisplayCount = 4
// let displayCount = min(subItems.count, maxDisplayCount)
// let height = CGFloat(displayCount) * 40
//
// tableViewHeightConstraint?.update(offset: height)
// layoutIfNeeded()
// }
//}
//
//extension SubItemsContainerCell: UITableViewDelegate, UITableViewDataSource {
// func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// return subItems.count
// }
//
// func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// let cell = tableView.dequeueReusableCell(withIdentifier: "SubItemCell", for: indexPath) as! SubItemCell
// let subItem = subItems[indexPath.row]
// cell.configure(with: subItem)
// return cell
// }
//}
//
//// SubItemCell - cell
//class SubItemCell: UITableViewCell {
// private let titleLabel = UILabel()
// private let iconImageView = UIImageView()
//
// 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(iconImageView)
// contentView.addSubview(titleLabel)
//
// iconImageView.snp.makeConstraints { make in
// make.left.equalToSuperview().offset(40)
// make.centerY.equalToSuperview()
// make.width.height.equalTo(18)
// }
//
// titleLabel.snp.makeConstraints { make in
// make.left.equalTo(iconImageView.snp.right).offset(8)
// make.centerY.equalToSuperview()
// make.right.equalToSuperview().inset(15)
// }
//
// titleLabel.font = UIFont.systemFont(ofSize: 13)
// titleLabel.textColor = UIColor(hex: "#999999")
// }
//
// func configure(with item: ImageRow) {
// iconImageView.image = UIImage(named: item.icon)
// titleLabel.text = item.title
// }
//}

View File

@ -6,14 +6,37 @@
//
import UIKit
import SnapKit
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 rows: [[RowModel]] = [
[ImageRow(icon: "role_exchange_mode", title: "XL-0826-32K", showAvatar: false, showArrow: true, showSwitch: false), ImageRow(icon: "role_text_mode", title: "Short Text Mode", showAvatar: false, showArrow: true, showSwitch: false)],
var rows: [[RowModel]] = []
// section -> rowIndex -> isExpanded
private var expandedStates: [Int: [Int: Bool]] = [:]
override init(frame: CGRect) {
super.init(frame: frame)
// rows10
let modelRow = ImageRow(icon: "role_exchange_mode", title: "XL-0826-32K", showAvatar: false, showArrow: true, showSwitch: false, subItems: [
ImageRow(icon: "role_exchange_mode", title: "Sub Item 1", showAvatar: false, showArrow: false, showSwitch: false),
ImageRow(icon: "role_exchange_mode", title: "Sub Item 2", showAvatar: false, showArrow: false, showSwitch: false),
ImageRow(icon: "role_exchange_mode", title: "Sub Item 3", showAvatar: false, showArrow: false, showSwitch: false),
ImageRow(icon: "role_exchange_mode", title: "Sub Item 4", showAvatar: false, showArrow: false, showSwitch: false),
ImageRow(icon: "role_exchange_mode", title: "Sub Item 5", showAvatar: false, showArrow: false, showSwitch: false),
// ImageRow(icon: "role_exchange_mode", title: "Sub Item 6", showAvatar: false, showArrow: false, showSwitch: false),
// ImageRow(icon: "role_exchange_mode", title: "Sub Item 7", showAvatar: false, showArrow: false, showSwitch: false),
// ImageRow(icon: "role_exchange_mode", title: "Sub Item 8", showAvatar: false, showArrow: false, showSwitch: false),
// ImageRow(icon: "role_exchange_mode", title: "Sub Item 9", showAvatar: false, showArrow: false, showSwitch: false),
// ImageRow(icon: "role_exchange_mode", title: "Sub Item 10", showAvatar: false, showArrow: false, showSwitch: false)
])
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)],
[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), ImageRow(icon: "role_chat_buttle", title: "Chat buttle", showAvatar: false, showArrow: true, showSwitch: false)],
@ -21,6 +44,12 @@ class ChatSettingSwipeView: CLContainer {
[HistoryRow(time: "", icon: "", title: "", itemCount: 30)]
]
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
lazy var titleLab: UILabel = {
let lab = UILabel()
@ -62,21 +91,11 @@ class ChatSettingSwipeView: CLContainer {
tableView.register(ChatFontCell.self, forCellReuseIdentifier: "ChatFontCell")
tableView.register(ChatBackgroundCell.self, forCellReuseIdentifier: "ChatBackgroundCell")
tableView.register(ChatHistoryCell.self, forCellReuseIdentifier: "ChatHistoryCell")
tableView.register(SubItemsContainerCell.self, forCellReuseIdentifier: "SubItemsContainerCell")
tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
return tableView
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupViews() {
navigationView = NavigationView()
@ -151,6 +170,48 @@ extension ChatSettingSwipeView: UITableViewDelegate, UITableViewDataSource {
// contentInset
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let imageRow = rows[indexPath.section][indexPath.row] as? ImageRow,
imageRow.showArrow,
let subItems = imageRow.subItems,
!subItems.isEmpty else {
return
}
//
let isExpanded = expandedStates[indexPath.section]?[indexPath.row] ?? false
expandedStates[indexPath.section, default: [:]][indexPath.row] = !isExpanded
// SubItemsContainerCell
if !isExpanded {
// SubItemsContainerCell
let subItemsRow = SubItemsRow(subItems: subItems)
rows[indexPath.section].insert(subItemsRow, at: indexPath.row + 1)
let insertIndexPath = IndexPath(row: indexPath.row + 1, section: indexPath.section)
tableView.beginUpdates()
tableView.insertRows(at: [insertIndexPath], with: .fade)
tableView.endUpdates()
//
tableView.reloadRows(at: [indexPath], with: .none)
} else {
// SubItemsContainerCell
if indexPath.row + 1 < rows[indexPath.section].count,
rows[indexPath.section][indexPath.row + 1] is SubItemsRow {
rows[indexPath.section].remove(at: indexPath.row + 1)
let deleteIndexPath = IndexPath(row: indexPath.row + 1, section: indexPath.section)
tableView.beginUpdates()
tableView.deleteRows(at: [deleteIndexPath], with: .fade)
tableView.endUpdates()
//
tableView.reloadRows(at: [indexPath], with: .none)
}
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return rows.count
}
@ -161,9 +222,26 @@ extension ChatSettingSwipeView: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let row = rows[indexPath.section][indexPath.row]
// SubItemsRow
if let subItemsRow = row as? SubItemsRow {
let cell = tableView.dequeueReusableCell(withIdentifier: "SubItemsContainerCell", for: indexPath) as! SubItemsContainerCell
cell.selectionStyle = .none
cell.configure(with: subItemsRow)
return cell
}
let cell = tableView.dequeueReusableCell(withIdentifier: row.cellReuseID, for: indexPath)
cell.selectionStyle = .none
// ImageRow
if var imageRow = row as? ImageRow {
imageRow.isExpanded = expandedStates[indexPath.section]?[indexPath.row] ?? false
(cell as? CellConfigurable)?.configure(with: imageRow)
} else {
(cell as? CellConfigurable)?.configure(with: row)
}
return cell
}
@ -194,3 +272,205 @@ extension ChatSettingSwipeView: UITableViewDelegate, UITableViewDataSource {
return CGFLOAT_MIN
}
}
// SubItemsRow - cell
struct SubItemsRow: RowModel {
let subItems: [ImageRow]
var cellReuseID: String { "SubItemsContainerCell" }
func cellHeight(tableWidth: CGFloat) -> CGFloat {
let maxDisplayCount = 4
let displayCount = min(subItems.count, maxDisplayCount)
return CGFloat(displayCount) * 58 // 40
}
}
// SubItemsContainerCell - cell
class SubItemsContainerCell: UITableViewCell, CellConfigurable {
private var subItems: [ImageRow] = []
private var tableViewHeightConstraint: Constraint?
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(SubItemCell.self, forCellReuseIdentifier: "SubItemCell")
tableView.estimatedRowHeight = 58
tableView.rowHeight = 58
tableView.contentInset = .zero
tableView.scrollIndicatorInsets = .zero
tableView.showsVerticalScrollIndicator = false
tableView.bounces = false
tableView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] //
tableView.layer.cornerRadius = 15
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(tableView)
tableView.snp.makeConstraints { make in
make.left.right.equalToSuperview().inset(20)
make.top.bottom.equalToSuperview().inset(2.5)
tableViewHeightConstraint = make.height.equalTo(0).constraint
}
}
func configure(with row: RowModel) {
guard let subItemsRow = row as? SubItemsRow else { return }
subItems = subItemsRow.subItems
tableView.reloadData()
updateTableViewHeight()
}
private func updateTableViewHeight() {
let maxDisplayCount = 4
let displayCount = min(subItems.count, maxDisplayCount)
let height = CGFloat(displayCount) * 58
tableViewHeightConstraint?.update(offset: height)
layoutIfNeeded()
}
}
extension SubItemsContainerCell: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return subItems.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SubItemCell", for: indexPath) as! SubItemCell
let subItem = subItems[indexPath.row]
cell.configure(with: subItem)
cell.lineView.isHidden = subItems.count - 1 == indexPath.row
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
}
}
// SubItemCell - cell
class SubItemCell: UITableViewCell {
private lazy var titleLabel: UILabel = {
let lab = UILabel()
lab.text = "Max-0618"
lab.textColor = UIColor(hex: "#333333")
lab.font = UIFont.systemFont(ofSize: 13)
return lab
}()
private lazy var iconImageView: UIImageView = {
let view = UIImageView()
view.cornerRadius = 12.5
view.backgroundColor = .darkText
return view
}()
lazy var subTitleLab: UILabel = {
let lab = UILabel()
lab.text = "Previous-generation large model"
lab.textColor = UIColor(hex: "#9494C3")
lab.font = UIFont.systemFont(ofSize: 10)
return lab
}()
lazy var tokenLab: UILabel = {
let lab = UILabel()
lab.textColor = UIColor(hex: "#0066FF")
lab.text = "0.3 points / 1K tokens (Recommended)"
lab.font = UIFont.systemFont(ofSize: 11)
return lab
}()
lazy var statusImgView: UIImageView = {
let imgView = UIImageView(image: UIImage(named: ""))
imgView.backgroundColor = .blue
return imgView
}()
lazy var lineView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hex: "#ECECF9")
return view
}()
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(iconImageView)
contentView.addSubview(titleLabel)
contentView.addSubview(subTitleLab)
contentView.addSubview(tokenLab)
contentView.addSubview(statusImgView)
contentView.addSubview(lineView)
iconImageView.snp.makeConstraints { make in
make.left.top.equalToSuperview().offset(10)
make.width.height.equalTo(25)
}
titleLabel.snp.makeConstraints { make in
make.left.equalTo(iconImageView.snp.right).offset(8)
make.top.equalTo(iconImageView.snp.top).offset(-2)
make.right.equalTo(statusImgView.snp.left).inset(-10)
}
subTitleLab.snp.makeConstraints { make in
make.left.equalTo(titleLabel.snp.left)
make.top.equalTo(titleLabel.snp.bottom)
make.right.equalTo(titleLabel.snp.right)
}
lineView.snp.makeConstraints { make in
make.left.right.equalToSuperview().inset(10)
make.bottom.equalToSuperview()
make.height.equalTo(1)
}
tokenLab.snp.makeConstraints { make in
make.left.equalTo(titleLabel.snp.left)
make.right.equalTo(titleLabel.snp.right)
make.bottom.equalToSuperview().inset(5)
}
statusImgView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalToSuperview().inset(10)
make.width.height.equalTo(13)
}
}
func configure(with item: ImageRow) {
iconImageView.image = UIImage(named: item.icon)
titleLabel.text = item.title
}
}