| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581 |
- //
- // LNGameMateListCell.swift
- // Lanu
- //
- // Created by OneeChan on 2025/11/16.
- //
- import Foundation
- import UIKit
- import SnapKit
- import Combine
- class LNGameMateListCell: UITableViewCell {
- private let avatar = UIImageView()
- private let onlineView = LNOnlineView()
-
- private let playButton = UIButton()
- private let voiceLabel = UILabel()
- private let voiceWaveView = LNVoiceWaveView()
-
- private let priceLabel = UILabel()
- private let unitLabel = UILabel()
-
- private let saleView = UIView()
- private let discountView = LNNewbieDiscountView()
-
- private let nameLabel = UILabel()
- private let genderView = LNGenderView()
-
- private let newbieView = UIView()
-
- private let scoreView = UIView()
- private let scoreLabel = UILabel()
- private let orderCountLabel = UILabel()
-
- private let gameIc = UIImageView()
- private let gameNameLanel = UILabel()
-
- private let bioLabel = UILabel()
-
- private let maxShow = 3
- private let photoView = UIStackView()
- private var photos: [UIImageView] = []
-
- private var curItem: LNGameMateListItemVO?
-
- override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
- super.init(style: style, reuseIdentifier: reuseIdentifier)
-
- setupViews()
-
- LNEventDeliver.addObserver(self)
- }
-
- func update(_ item: LNGameMateListItemVO) {
- avatar.showAvatar(item.avatar)
- onlineView.isHidden = !item.online
- priceLabel.text = item.price.toDisplay
- unitLabel.text = "/\(item.unit)"
-
- nameLabel.text = item.nickname
- genderView.update(item.gender, item.age)
-
- scoreLabel.text = "\(item.star)"
- orderCountLabel.text = .init(key: "A00037", item.orderCount)
-
- gameIc.sd_setImage(with: URL(string: item.categoryIcon))
- gameNameLanel.text = item.categoryName
-
- bioLabel.text = item.summary
- bioLabel.superview?.isHidden = item.summary.isEmpty
-
- newbieView.isHidden = !item.newcomer
-
- if item.images.isEmpty {
- photoView.isHidden = true
- bioLabel.numberOfLines = 4
- } else {
- for index in 0..<maxShow {
- if index < item.images.count {
- photos[index].sd_setImage(with: URL(string: item.images[index]))
- } else {
- photos[index].sd_setImage(with: nil)
- }
- }
- photoView.isHidden = false
- bioLabel.numberOfLines = 1
- }
-
- if let curItem {
- LNVoiceResourceManager.shared.cancelLoadingAsset(urlStr: curItem.voiceBar)
- }
-
- curItem = item
-
- if LNVoicePlayer.shared.playingUrl == curItem?.voiceBar {
- voiceWaveView.startAnimate()
- } else {
- voiceWaveView.stopAnimate()
- }
-
- playButton.isHidden = true
- if !item.voiceBar.isEmpty {
- LNVoiceResourceManager.shared.getRemoteAudioDuration(urlStr: item.voiceBar)
- { [weak self] duration, error in
- guard let self else { return }
- guard let duration, error == nil else { return }
- guard curItem?.id == item.id else { return }
- voiceLabel.text = duration.durationDisplay
- playButton.isHidden = false
- }
- }
-
- updateDiscount()
- }
-
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
- }
- extension LNGameMateListCell: LNVoicePlayerNotify {
- func onAudioStartPlay(path: String) {
- guard path == curItem?.voiceBar else { return }
- voiceWaveView.startAnimate()
- }
-
- func onAudioStopPlay(path: String) {
- guard path == curItem?.voiceBar else { return }
- voiceWaveView.stopAnimate()
- let url = curItem?.voiceBar
- LNVoiceResourceManager.shared.getRemoteAudioDuration(urlStr: curItem?.voiceBar)
- { [weak self] duration, err in
- guard let self else { return }
- guard let duration, err == nil else { return }
- guard curItem?.voiceBar == url else { return }
- voiceLabel.text = duration.durationDisplay
- }
- }
-
- func onAudioUpdateDuration(path: String, cur: TimeInterval, total: TimeInterval) {
- guard path == curItem?.voiceBar else { return }
- voiceLabel.text = (total - cur).durationDisplay
- }
- }
- extension LNGameMateListCell: LNOrderManagerNotify {
- func onMyDiscountInfoChanged(info: LNOrderDiscountVO?) {
- updateDiscount()
- }
-
- private func updateDiscount() {
- guard let curItem else { return }
- if curItem.userNo.isMyUid == false,
- let discount = LNOrderManager.shared.discountFor(curItem.price) {
- saleView.isHidden = false
- discountView.update(1 - discount)
- priceLabel.textColor = .init(hex: "#FF6F32")
- priceLabel.text = (curItem.price * discount).toDisplay
- } else {
- saleView.isHidden = true
- priceLabel.textColor = .text_5
- priceLabel.text = curItem.price.toDisplay
- }
- }
- }
- extension LNGameMateListCell {
- private func setupViews() {
- backgroundColor = .clear
-
- let container = UIView()
- container.onTap { [weak self] in
- guard let self else { return }
- guard let curItem else { return }
- pushToSkillDetail(curItem.id)
- }
- contentView.addSubview(container)
- container.snp.makeConstraints { make in
- make.leading.equalToSuperview().offset(16)
- make.trailing.equalToSuperview().offset(-16)
- make.top.equalToSuperview()
- make.bottom.equalToSuperview().offset(-10).priority(.medium)
- }
-
- let bg = UIImageView()
- bg.image = .icGameMateListCellBg.resizableImage(
- withCapInsets: .init(top: 48, left: 53, bottom: 12, right: 17), resizingMode: .stretch
- )
- container.addSubview(bg)
- bg.snp.makeConstraints { make in
- make.edges.equalToSuperview()
- }
-
- let avatar = buildAvatar()
- container.addSubview(avatar)
- avatar.snp.makeConstraints { make in
- make.leading.equalToSuperview().offset(13)
- make.top.equalToSuperview().offset(12)
- make.bottom.lessThanOrEqualToSuperview().offset(-12)
- }
-
- let stackView = UIStackView()
- stackView.axis = .vertical
- stackView.spacing = 6
- container.addSubview(stackView)
- stackView.snp.makeConstraints { make in
- make.leading.equalTo(avatar.snp.trailing).offset(14)
- make.trailing.equalToSuperview().offset(-12)
- make.top.equalToSuperview().offset(10)
- make.bottom.lessThanOrEqualToSuperview().offset(-10)
- }
-
- stackView.addArrangedSubview(buildTextViews())
- stackView.addArrangedSubview(buildPhotos())
- }
-
- private func buildAvatar() -> UIView {
- let container = UIView()
-
- avatar.layer.cornerRadius = 38
- avatar.clipsToBounds = true
- avatar.contentMode = .scaleAspectFill
- container.addSubview(avatar)
- avatar.snp.makeConstraints { make in
- make.width.height.equalTo(76)
- make.top.equalToSuperview()
- make.horizontalEdges.equalToSuperview()
- }
-
- onlineView.offset = 5
- container.addSubview(onlineView)
- onlineView.snp.makeConstraints { make in
- make.edges.equalTo(avatar).inset(-2)
- }
-
- playButton.setBackgroundImage(.primary_7, for: .normal)
- playButton.layer.cornerRadius = 11
- playButton.clipsToBounds = true
- playButton.addAction(UIAction(handler: { [weak self] _ in
- guard let self else { return }
- guard let curItem, !curItem.voiceBar.isEmpty else { return }
- if LNVoicePlayer.shared.playingUrl == curItem.voiceBar {
- LNVoicePlayer.shared.stop()
- } else {
- LNVoicePlayer.shared.play(curItem.voiceBar)
- }
- }), for: .touchUpInside)
- container.addSubview(playButton)
- playButton.snp.makeConstraints { make in
- make.centerX.equalToSuperview()
- make.bottom.equalTo(avatar).offset(10)
- make.width.equalTo(64)
- make.height.equalTo(22)
- }
-
- let voice = UIView()
- voice.isUserInteractionEnabled = false
- playButton.addSubview(voice)
- voice.snp.makeConstraints { make in
- make.center.equalToSuperview()
- }
-
- voiceWaveView.build()
- voice.addSubview(voiceWaveView)
- voiceWaveView.snp.makeConstraints { make in
- make.leading.centerY.equalToSuperview()
- make.width.equalTo(19)
- make.height.equalTo(11)
- }
-
- voiceLabel.font = .heading_h5
- voiceLabel.textColor = .text_1
- voice.addSubview(voiceLabel)
- voiceLabel.snp.makeConstraints { make in
- make.verticalEdges.equalToSuperview()
- make.trailing.equalToSuperview()
- make.leading.equalTo(voiceWaveView.snp.trailing).offset(4)
- }
-
- let price = UIView()
- container.addSubview(price)
- price.snp.makeConstraints { make in
- make.centerX.equalToSuperview()
- make.top.equalTo(playButton.snp.bottom).offset(2)
- }
-
- let coin = UIImageView.coinImageView()
- price.addSubview(coin)
- coin.snp.makeConstraints { make in
- make.leading.centerY.equalToSuperview()
- make.width.height.equalTo(18)
- }
-
- priceLabel.font = .heading_h4
- priceLabel.textColor = .text_5
- priceLabel.setContentHuggingPriority(.required, for: .vertical)
- priceLabel.setContentCompressionResistancePriority(.required, for: .vertical)
- price.addSubview(priceLabel)
- priceLabel.snp.makeConstraints { make in
- make.verticalEdges.equalToSuperview()
- make.leading.equalTo(coin.snp.trailing)
- }
-
- unitLabel.font = .body_s
- unitLabel.textColor = .text_3
- price.addSubview(unitLabel)
- unitLabel.snp.makeConstraints { make in
- make.centerY.trailing.equalToSuperview()
- make.leading.equalTo(priceLabel.snp.trailing).offset(1)
- }
-
- let saleView = buildSaleView()
- container.addSubview(saleView)
- saleView.snp.makeConstraints { make in
- make.centerX.equalToSuperview()
- make.top.equalTo(price.snp.bottom)
- make.bottom.equalToSuperview()
- }
-
- return container
- }
-
- private func buildBaseInfo() -> UIView {
- let container = UIView()
- container.isUserInteractionEnabled = false
-
- nameLabel.font = .heading_h4
- nameLabel.textColor = .text_5
- nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
- container.addSubview(nameLabel)
- nameLabel.snp.makeConstraints { make in
- make.leading.equalToSuperview()
- make.top.equalToSuperview()
- }
-
- container.addSubview(genderView)
- genderView.snp.makeConstraints { make in
- make.leading.equalTo(nameLabel.snp.trailing).offset(4)
- make.centerY.equalTo(nameLabel)
- make.trailing.lessThanOrEqualToSuperview()
- }
-
- container.addSubview(scoreView)
- scoreView.snp.makeConstraints { make in
- make.leading.bottom.equalToSuperview()
- make.top.equalTo(nameLabel.snp.bottom).offset(3)
- make.height.equalTo(15)
- }
-
- let starIc = UIImageView()
- starIc.image = .icStarFill
- scoreView.addSubview(starIc)
- starIc.snp.makeConstraints { make in
- make.leading.equalToSuperview()
- make.width.height.equalTo(14)
- make.centerY.equalToSuperview()
- }
-
- scoreLabel.font = .heading_h5
- scoreLabel.textColor = .text_5
- scoreView.addSubview(scoreLabel)
- scoreLabel.snp.makeConstraints { make in
- make.leading.equalTo(starIc.snp.trailing).offset(3)
- make.centerY.equalToSuperview()
- }
-
- orderCountLabel.font = .body_xs
- orderCountLabel.textColor = .text_3
- scoreView.addSubview(orderCountLabel)
- orderCountLabel.snp.makeConstraints { make in
- make.leading.equalTo(scoreLabel.snp.trailing).offset(3)
- make.centerY.equalToSuperview()
- make.trailing.equalToSuperview()
- }
-
- return container
- }
-
- private func buildNewbieView() -> UIView {
- newbieView.layer.cornerRadius = 7.5
- newbieView.clipsToBounds = true
- newbieView.snp.makeConstraints { make in
- make.height.equalTo(15)
- }
-
- let holder = UIView()
- newbieView.addSubview(holder)
- holder.snp.makeConstraints { make in
- make.leading.equalToSuperview()
- make.verticalEdges.equalToSuperview()
- }
-
- let gradientLayer = CAGradientLayer()
- gradientLayer.colors = [
- UIColor(hex: "#FFD35B").cgColor,
- UIColor(hex: "#FFFFD35B").cgColor
- ]
- gradientLayer.startPoint = .init(x: 0, y: 0)
- gradientLayer.endPoint = .init(x: 1, y: 0)
- holder.layer.addSublayer(gradientLayer)
- holder.publisher(for: \.bounds).removeDuplicates().sink
- { [weak gradientLayer] newValue in
- guard let gradientLayer else { return }
- gradientLayer.frame = newValue
- }.store(in: &cancellables)
-
- let label = UILabel()
- label.font = .body_xs
- label.textColor = .init(hex: "#DE9D2E")
- label.text = .init(key: "A00310")
- holder.addSubview(label)
- label.snp.makeConstraints { make in
- make.leading.equalToSuperview().offset(6)
- make.trailing.equalToSuperview().offset(-14)
- make.centerY.equalToSuperview()
- }
-
- return newbieView
- }
-
- private func buildGameView() -> UIView {
- let container = UIView()
- container.isUserInteractionEnabled = false
-
- let background = UIView()
- background.backgroundColor = .fill_1
- background.layer.cornerRadius = 10
- container.addSubview(background)
- background.snp.makeConstraints { make in
- make.verticalEdges.equalToSuperview()
- make.leading.equalToSuperview()
- }
-
- let border = UIImageView()
- border.image = .primary_7
- border.layer.cornerRadius = 8
- border.clipsToBounds = true
- background.addSubview(border)
- border.snp.makeConstraints { make in
- make.verticalEdges.equalToSuperview().inset(2)
- make.leading.equalToSuperview().offset(2)
- make.width.height.equalTo(16)
- }
-
- gameIc.backgroundColor = .white
- gameIc.layer.cornerRadius = 7
- background.addSubview(gameIc)
- gameIc.snp.makeConstraints { make in
- make.center.equalTo(border)
- make.width.height.equalTo(14)
- }
-
- gameNameLanel.font = .body_xs
- gameNameLanel.textColor = .text_4
- gameNameLanel.setContentHuggingPriority(.required, for: .horizontal)
- gameNameLanel.setContentCompressionResistancePriority(.required, for: .horizontal)
- background.addSubview(gameNameLanel)
- gameNameLanel.snp.makeConstraints { make in
- make.centerY.equalToSuperview()
- make.trailing.equalToSuperview().offset(-6)
- make.leading.equalTo(border.snp.trailing).offset(2)
- }
-
- return container
- }
-
- private func buildBio() -> UIView {
- let container = UIView()
- container.isUserInteractionEnabled = false
-
- let bg = UIImageView()
- bg.image = .icGameMateListCellBubble.resizableImage(
- withCapInsets: .init(top: 16, left: 20, bottom: 12, right: 15), resizingMode: .stretch
- )
- container.addSubview(bg)
- bg.snp.makeConstraints { make in
- make.edges.equalToSuperview()
- make.height.greaterThanOrEqualTo(29)
- }
-
- bioLabel.font = .body_xs
- bioLabel.textColor = .text_6
- container.addSubview(bioLabel)
- bioLabel.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview().inset(8)
- make.top.equalToSuperview().offset(9)
- make.bottom.equalToSuperview().offset(-6)
- }
-
- return container
- }
-
- private func buildTextViews() -> UIView {
- let stackView = UIStackView()
- stackView.axis = .vertical
- stackView.spacing = 2
-
- stackView.addArrangedSubview(buildBaseInfo())
- stackView.addArrangedSubview(buildNewbieView())
- stackView.addArrangedSubview(buildGameView())
- stackView.addArrangedSubview(buildBio())
-
- return stackView
- }
-
- private func buildPhotos() -> UIView {
- photoView.axis = .horizontal
- photoView.spacing = 6
- photoView.distribution = .fillEqually
- photoView.alignment = .leading
-
- for _ in 0..<maxShow {
- let icon = UIImageView()
- icon.layer.cornerRadius = 12
- icon.clipsToBounds = true
- icon.contentMode = .scaleAspectFill
- photoView.addArrangedSubview(icon)
- icon.snp.makeConstraints { make in
- make.height.equalTo(icon.snp.width)
- }
- photos.append(icon)
- }
-
- return photoView
- }
-
- private func buildSaleView() -> UIView {
- let triangle = UIImageView()
- triangle.image = .icTriangleDown.withRenderingMode(.alwaysTemplate)
- triangle.tintColor = .init(hex: "#FF6F32")
- triangle.transform = .init(scaleX: 1, y: -1)
- saleView.addSubview(triangle)
- triangle.snp.makeConstraints { make in
- make.centerX.equalToSuperview()
- make.top.equalToSuperview()
- make.width.equalTo(10)
- make.height.equalTo(4)
- }
-
- discountView.discountOnly = true
- saleView.addSubview(discountView)
- discountView.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.bottom.equalToSuperview()
- make.top.equalTo(triangle.snp.bottom)
- }
-
- return saleView
- }
- }
- #if DEBUG
- import SwiftUI
- struct LNGameMateListCellPreview: UIViewRepresentable {
- func makeUIView(context: Context) -> some UIView {
- let container = UIView()
- container.backgroundColor = .lightGray
-
- let view = LNGameMateListCell()
- container.addSubview(view)
- view.snp.makeConstraints { make in
- make.leading.trailing.centerY.equalToSuperview()
- make.height.equalTo(187)
- }
-
- return container
- }
-
- func updateUIView(_ uiView: UIViewType, context: Context) { }
- }
- #Preview(body: {
- LNGameMateListCellPreview()
- })
- #endif
|