| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438 |
- //
- // LNSkillDetailViewController.swift
- // Lanu
- //
- // Created by OneeChan on 2025/11/28.
- //
- import Foundation
- import UIKit
- import SnapKit
- import Combine
- import AutoCodable
- extension UIView {
- func pushToSkillDetail(_ skillId: String) {
- let vc = LNSkillDetailViewController(skillId: skillId)
- navigationController?.pushViewController(vc, animated: true)
- }
- }
- class LNSkillDetailViewController: LNViewController {
- private let skillId: String
-
- private let avatar = UIImageView()
- private let titleLabel = UILabel()
- private let followButton = UIButton()
- private let profileButton = UIButton()
- private let moreButton = UIButton()
-
- private let cover = UIImageView()
- private let userInfoView = LNSkillUserInfoView()
- private let gameNameLabel = UILabel()
- private let descLabel = UILabel()
- private let tagView = LNSkillTagView()
- private let photosView = LNSkillPhotosView()
- private let commentsView = LNSkillCommentsView()
- private let bottomMenu = LNSkillBottomMenuView()
-
- private var detail: LNGameMateSkillDetailVO?
- private var stayTimer: String?
-
- init(skillId: String) {
- self.skillId = skillId
-
- super.init(nibName: nil, bundle: nil)
- }
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- setupViews()
-
- loadSkillInfo()
-
- LNEventDeliver.addObserver(self)
- }
-
- override func viewDidAppear(_ animated: Bool) {
- super.viewDidAppear(animated)
-
- if detail?.userNo.isMyUid == true {
- loadSkillInfo()
- } else {
- triggerAutoReplay()
- }
- }
-
- override func viewWillDisappear(_ animated: Bool) {
- super.viewWillDisappear(animated)
-
- LNDelayTask.cancel(key: stayTimer)
- }
-
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
- }
- private extension LNSkillDetailViewController {
- func triggerAutoReplay() {
- guard let uid = detail?.userNo else { return }
- if !uid.isMyUid {
- stayTimer = LNDelayTask.perform(delay: 3) { [weak self] in
- guard let self else { return }
- LNGameMateManager.shared.triggerAutoReplayIfNeed(uid: uid)
- }
- }
- }
-
- func loadSkillInfo() {
- LNGameMateManager.shared.getSkillDetail(skillId: skillId) { [weak self] info in
- guard let self else { return }
- guard let info else { return }
- if detail == nil {
- LNStatisticManager.shared.reportVisitor(uid: info.userNo) { _ in }
- LNStatisticManager.shared.reportViewPlaymate(uid: info.userNo)
- }
- self.detail = info
-
- cover.sd_setImage(with: URL(string: info.cover.isEmpty ? info.avatar : info.cover))
- userInfoView.update(info)
- gameNameLabel.text = info.categoryName
-
- descLabel.isHidden = info.summary.isEmpty
- descLabel.text = info.summary
-
- tagView.update(info.labels)
- photosView.update(info)
-
- commentsView.update(info)
-
- bottomMenu.update(info)
-
- avatar.showAvatar(info.avatar)
- titleLabel.text = info.nickname
- followButton.isHidden = info.follow || info.userNo.isMyUid
- moreButton.isHidden = info.userNo.isMyUid
-
- triggerAutoReplay()
- }
- }
- }
- extension LNSkillDetailViewController: LNRelationManagerNotify {
- func onUserRelationChanged(uid: String, relation: LNUserRelationShip) {
- guard uid == detail?.userNo else { return }
-
- followButton.isHidden = relation.contains(.followed)
- }
- }
- extension LNSkillDetailViewController: UIScrollViewDelegate {
- func scrollViewDidScroll(_ scrollView: UIScrollView) {
- let offset = cover.bounds.height - 35 - fakeNaviBgView.bounds.height
- let min = offset - UIView.statusBarHeight
- let progress: Double = if scrollView.contentOffset.y < min {
- 0.0
- } else {
- (scrollView.contentOffset.y - min) / UIView.statusBarHeight
- }
-
- updateProgress(progress)
- }
- }
- extension LNSkillDetailViewController {
- private func showMoreMenu() {
- let panel = LNBottomSheetMenu()
- panel.update([
- .init(key: "A00043"),
- detail?.userNo.isInMyBlackList != true ? .init(key: "A00044") : .init(key: "A00045")
- ]) { [weak self] index, _ in
- guard let self else { return }
- guard let detail else { return }
- if index == 0 {
- view.pushToReport(uid: detail.userNo)
- } else if index == 1 {
- if detail.userNo.isInMyBlackList {
- LNRelationManager.shared.blackListUser(uid: detail.userNo, black: false, handler: nil)
- } else {
- LNCommonAlertView.showBlackAlert(uid: detail.userNo)
- }
- }
- }
- panel.popup()
- }
-
- private func updateProgress(_ progress: Double) {
- navigationBarColor = .white.withAlphaComponent(progress)
-
- avatar.alpha = progress
- titleLabel.alpha = progress
-
- let tintColor = UIColor.white.interpolateHSB(to: .text_4, progress: progress)
- backButton.tintColor = tintColor
- followButton.tintColor = tintColor
- profileButton.tintColor = tintColor
- moreButton.tintColor = tintColor
-
- let background = UIColor.black.withAlphaComponent(0.4 * (1 - progress))
- followButton.backgroundColor = background
- profileButton.backgroundColor = background
- moreButton.backgroundColor = background
- }
-
- private func setupViews() {
- setupNavBar()
-
- let stackView = UIStackView()
- stackView.axis = .vertical
- view.addSubview(stackView)
- stackView.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.top.equalTo(fakeNaviBgView)
- make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom)
- }
-
- let scrollView = UIScrollView()
- scrollView.delegate = self
- scrollView.clipsToBounds = false
- scrollView.backgroundColor = .fill
- scrollView.contentInsetAdjustmentBehavior = .never
- scrollView.showsVerticalScrollIndicator = false
- scrollView.showsHorizontalScrollIndicator = false
- scrollView.contentInset = .init(top: 0, left: 0, bottom: -view.commonBottomInset + 47, right: 0)
- stackView.addArrangedSubview(scrollView)
-
- let fakeView = UIView()
- scrollView.addSubview(fakeView)
- fakeView.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.width.equalToSuperview()
- make.top.equalToSuperview()
- make.height.equalTo(0)
- }
-
- let cover = buildCover()
- scrollView.addSubview(cover)
- cover.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.top.equalToSuperview()
- }
-
- let gameView = buildGameView()
- scrollView.addSubview(gameView)
- gameView.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.top.equalTo(cover.snp.bottom).offset(-35)
- make.bottom.equalToSuperview()
- }
-
- let userInfoView = buildUserInfoView()
- scrollView.addSubview(userInfoView)
- userInfoView.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.bottom.equalTo(gameView.snp.top).offset(-10)
- }
-
- let menu = buildBottomMenu()
- view.addSubview(menu)
- menu.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.bottom.equalToSuperview()
- }
- }
-
- private func setupNavBar() {
- let menu = buildButtonMenu()
- setRightButton(menu)
-
- let container = UIView()
- avatar.alpha = 0.0
- avatar.layer.cornerRadius = 16
- avatar.clipsToBounds = true
- container.addSubview(avatar)
- avatar.snp.makeConstraints { make in
- make.leading.equalToSuperview()
- make.centerY.equalToSuperview()
- make.width.height.equalTo(32)
- }
-
- titleLabel.alpha = 0.0
- titleLabel.font = .heading_h3
- titleLabel.textColor = .text_5
- titleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
- titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
- container.addSubview(titleLabel)
- titleLabel.snp.makeConstraints { make in
- make.leading.equalTo(avatar.snp.trailing).offset(12)
- make.centerY.equalToSuperview()
- make.trailing.lessThanOrEqualToSuperview().offset(-37)
- }
-
- setTitleView(container)
- container.snp.makeConstraints { make in
- make.width.equalTo(view.bounds.width).priority(.medium)
- make.height.equalTo(44)
- }
-
- updateProgress(0.0)
- }
-
- private func buildButtonMenu() -> UIView {
- let stackView = UIStackView()
- stackView.axis = .horizontal
- stackView.spacing = 14
-
- followButton.layer.cornerRadius = 16
- followButton.isHidden = true
- followButton.setImage(.icSkillFollow.withRenderingMode(.alwaysTemplate), for: .normal)
- followButton.snp.makeConstraints { make in
- make.width.height.equalTo(32)
- }
- followButton.addAction(UIAction(handler: { [weak self] _ in
- guard let self else { return }
- guard let detail else { return }
- LNRelationManager.shared.operateFollow(uid: detail.userNo, follow: true, handler: nil)
- }), for: .touchUpInside)
- stackView.addArrangedSubview(followButton)
-
- profileButton.layer.cornerRadius = 16
- profileButton.setImage(.icSkillToProfile.withRenderingMode(.alwaysTemplate), for: .normal)
- profileButton.snp.makeConstraints { make in
- make.width.height.equalTo(32)
- }
- profileButton.addAction(UIAction(handler: { [weak self] _ in
- guard let self else { return }
- guard let detail else { return }
- view.pushToProfile(uid: detail.userNo)
- }), for: .touchUpInside)
- stackView.addArrangedSubview(profileButton)
-
- moreButton.layer.cornerRadius = 16
- moreButton.setImage(.icMore.withRenderingMode(.alwaysTemplate), for: .normal)
- moreButton.snp.makeConstraints { make in
- make.width.height.equalTo(32)
- }
- moreButton.addAction(UIAction(handler: { [weak self] _ in
- guard let self else { return }
- showMoreMenu()
- }), for: .touchUpInside)
- stackView.addArrangedSubview(moreButton)
-
- return stackView
- }
-
- private func buildCover() -> UIView {
- let coverGradient = CAGradientLayer()
- coverGradient.colors = [UIColor.black.withAlphaComponent(0).cgColor, UIColor.black.cgColor]
- coverGradient.locations = [0, 1]
- coverGradient.startPoint = .init(x: 0, y: 0)
- coverGradient.endPoint = .init(x: 0, y: 1)
- cover.layer.addSublayer(coverGradient)
- cover.publisher(for: \.bounds).removeDuplicates().sink { [weak coverGradient] newValue in
- guard let coverGradient else { return }
- coverGradient.frame = .init(
- x: 0,
- y: newValue.height * 0.5,
- width: newValue.width,
- height: newValue.height * 0.5
- )
- }.store(in: &cancellables)
-
- cover.contentMode = .scaleAspectFill
- cover.clipsToBounds = true
- cover.onTap { [weak self] in
- guard let self else { return }
- guard let cover = detail?.cover.isEmpty != false ? detail?.avatar : detail?.cover else { return }
- view.presentImagePreview([cover], 0)
- }
- cover.snp.makeConstraints { make in
- make.height.equalTo(cover.snp.width).multipliedBy(363.0/375.0)
- }
-
- return cover
- }
-
- private func buildUserInfoView() -> UIView {
-
- return userInfoView
- }
-
- private func buildGameView() -> UIView {
- let container = UIView()
- container.backgroundColor = .fill
- container.layer.cornerRadius = 20
- container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
-
- gameNameLabel.font = .heading_h3
- gameNameLabel.textColor = .text_5
- container.addSubview(gameNameLabel)
- gameNameLabel.snp.makeConstraints { make in
- make.leading.equalToSuperview().offset(16)
- make.top.equalToSuperview().offset(16)
- }
-
- let line = UIView()
- line.backgroundColor = .fill_2
- container.addSubview(line)
- line.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview().inset(16)
- make.top.equalTo(gameNameLabel.snp.bottom).offset(10)
- make.height.equalTo(1)
- }
-
- let stackView = UIStackView()
- stackView.axis = .vertical
- stackView.spacing = 12
- container.addSubview(stackView)
- stackView.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview().inset(16)
- make.top.equalTo(line.snp.bottom).offset(10)
- make.bottom.equalToSuperview()
- }
-
- descLabel.font = .body_m
- descLabel.textColor = .text_4
- descLabel.numberOfLines = 0
- descLabel.isHidden = true
- stackView.addArrangedSubview(descLabel)
-
- tagView.isHidden = true
- stackView.addArrangedSubview(tagView)
-
- photosView.isHidden = true
- stackView.addArrangedSubview(photosView)
-
- commentsView.isHidden = true
- stackView.addArrangedSubview(commentsView)
-
- return container
- }
-
- private func buildBottomMenu() -> UIView {
-
- return bottomMenu
- }
- }
- #if DEBUG
- import SwiftUI
- struct LNSkillDetailViewControllerPreview: UIViewControllerRepresentable {
- func makeUIViewController(context: Context) -> some UIViewController {
- LNNavigationController(rootViewController: LNSkillDetailViewController(skillId: ""))
- }
-
- func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
- }
- #Preview(body: {
- LNSkillDetailViewControllerPreview()
- })
- #endif
|