LNSkillDetailViewController.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. //
  2. // LNSkillDetailViewController.swift
  3. // Lanu
  4. //
  5. // Created by OneeChan on 2025/11/28.
  6. //
  7. import Foundation
  8. import UIKit
  9. import SnapKit
  10. import Combine
  11. import AutoCodable
  12. extension UIView {
  13. func pushToSkillDetail(_ skillId: String) {
  14. let vc = LNSkillDetailViewController(skillId: skillId)
  15. navigationController?.pushViewController(vc, animated: true)
  16. }
  17. }
  18. class LNSkillDetailViewController: LNViewController {
  19. private let skillId: String
  20. private let avatar = UIImageView()
  21. private let titleLabel = UILabel()
  22. private let followButton = UIButton()
  23. private let profileButton = UIButton()
  24. private let moreButton = UIButton()
  25. private let cover = UIImageView()
  26. private let userInfoView = LNSkillUserInfoView()
  27. private let gameNameLabel = UILabel()
  28. private let descLabel = UILabel()
  29. private let tagView = LNSkillTagView()
  30. private let photosView = LNSkillPhotosView()
  31. private let commentsView = LNSkillCommentsView()
  32. private let bottomMenu = LNSkillBottomMenuView()
  33. private var detail: LNGameMateSkillDetailVO?
  34. private var stayTimer: String?
  35. init(skillId: String) {
  36. self.skillId = skillId
  37. super.init(nibName: nil, bundle: nil)
  38. }
  39. override func viewDidLoad() {
  40. super.viewDidLoad()
  41. setupViews()
  42. loadSkillInfo()
  43. LNEventDeliver.addObserver(self)
  44. }
  45. override func viewDidAppear(_ animated: Bool) {
  46. super.viewDidAppear(animated)
  47. if detail?.userNo.isMyUid == true {
  48. loadSkillInfo()
  49. } else if let uid = detail?.userNo {
  50. stayTimer = LNDelayTask.perform(delay: 3) {
  51. LNGameMateManager.shared.triggerAutoReplayIfNeed(uid: uid)
  52. }
  53. }
  54. }
  55. override func viewWillDisappear(_ animated: Bool) {
  56. super.viewWillDisappear(animated)
  57. LNDelayTask.cancel(key: stayTimer)
  58. }
  59. required init?(coder: NSCoder) {
  60. fatalError("init(coder:) has not been implemented")
  61. }
  62. }
  63. extension LNSkillDetailViewController {
  64. private func loadSkillInfo() {
  65. LNGameMateManager.shared.getSkillDetail(skillId: skillId) { [weak self] info in
  66. guard let self else { return }
  67. guard let info else { return }
  68. if detail == nil {
  69. LNStatisticManager.shared.reportVisitor(uid: info.userNo) { _ in }
  70. LNStatisticManager.shared.reportViewPlaymate(uid: info.userNo)
  71. }
  72. self.detail = info
  73. cover.sd_setImage(with: URL(string: info.cover.isEmpty ? info.avatar : info.cover))
  74. userInfoView.update(info)
  75. gameNameLabel.text = info.categoryName
  76. descLabel.isHidden = info.summary.isEmpty
  77. descLabel.text = info.summary
  78. tagView.update(info.labels)
  79. photosView.update(info)
  80. commentsView.update(info)
  81. bottomMenu.update(info)
  82. avatar.showAvatar(info.avatar)
  83. titleLabel.text = info.nickname
  84. followButton.isHidden = info.follow || info.userNo.isMyUid
  85. moreButton.isHidden = info.userNo.isMyUid
  86. }
  87. }
  88. }
  89. extension LNSkillDetailViewController: LNRelationManagerNotify {
  90. func onUserRelationChanged(uid: String, relation: LNUserRelationShip) {
  91. guard uid == detail?.userNo else { return }
  92. followButton.isHidden = relation.contains(.followed)
  93. }
  94. }
  95. extension LNSkillDetailViewController: UIScrollViewDelegate {
  96. func scrollViewDidScroll(_ scrollView: UIScrollView) {
  97. let offset = cover.bounds.height - 35 - fakeNaviBgView.bounds.height
  98. let min = offset - UIView.statusBarHeight
  99. let progress: Double = if scrollView.contentOffset.y < min {
  100. 0.0
  101. } else {
  102. (scrollView.contentOffset.y - min) / UIView.statusBarHeight
  103. }
  104. updateProgress(progress)
  105. }
  106. }
  107. extension LNSkillDetailViewController {
  108. private func showMoreMenu() {
  109. let panel = LNBottomSheetMenu()
  110. panel.update([
  111. .init(key: "A00043"),
  112. detail?.userNo.isInMyBlackList != true ? .init(key: "A00044") : .init(key: "A00045")
  113. ]) { [weak self] index, _ in
  114. guard let self else { return }
  115. guard let detail else { return }
  116. if index == 0 {
  117. view.pushToReport(uid: detail.userNo)
  118. } else if index == 1 {
  119. if detail.userNo.isInMyBlackList {
  120. LNRelationManager.shared.blackListUser(uid: detail.userNo, black: false, handler: nil)
  121. } else {
  122. LNCommonAlertView.showBlackAlert(uid: detail.userNo)
  123. }
  124. }
  125. }
  126. panel.popup()
  127. }
  128. private func updateProgress(_ progress: Double) {
  129. navigationBarColor = .white.withAlphaComponent(progress)
  130. avatar.alpha = progress
  131. titleLabel.alpha = progress
  132. let tintColor = UIColor.white.interpolateHSB(to: .text_4, progress: progress)
  133. backButton.tintColor = tintColor
  134. followButton.tintColor = tintColor
  135. profileButton.tintColor = tintColor
  136. moreButton.tintColor = tintColor
  137. let background = UIColor.black.withAlphaComponent(0.4 * (1 - progress))
  138. followButton.backgroundColor = background
  139. profileButton.backgroundColor = background
  140. moreButton.backgroundColor = background
  141. }
  142. private func setupViews() {
  143. setupNavBar()
  144. let stackView = UIStackView()
  145. stackView.axis = .vertical
  146. view.addSubview(stackView)
  147. stackView.snp.makeConstraints { make in
  148. make.horizontalEdges.equalToSuperview()
  149. make.top.equalTo(fakeNaviBgView)
  150. make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom)
  151. }
  152. let scrollView = UIScrollView()
  153. scrollView.delegate = self
  154. scrollView.clipsToBounds = false
  155. scrollView.backgroundColor = .fill
  156. scrollView.contentInsetAdjustmentBehavior = .never
  157. scrollView.showsVerticalScrollIndicator = false
  158. scrollView.showsHorizontalScrollIndicator = false
  159. scrollView.contentInset = .init(top: 0, left: 0, bottom: -view.commonBottomInset + 47, right: 0)
  160. stackView.addArrangedSubview(scrollView)
  161. let fakeView = UIView()
  162. scrollView.addSubview(fakeView)
  163. fakeView.snp.makeConstraints { make in
  164. make.horizontalEdges.equalToSuperview()
  165. make.width.equalToSuperview()
  166. make.top.equalToSuperview()
  167. make.height.equalTo(0)
  168. }
  169. let cover = buildCover()
  170. scrollView.addSubview(cover)
  171. cover.snp.makeConstraints { make in
  172. make.horizontalEdges.equalToSuperview()
  173. make.top.equalToSuperview()
  174. }
  175. let gameView = buildGameView()
  176. scrollView.addSubview(gameView)
  177. gameView.snp.makeConstraints { make in
  178. make.horizontalEdges.equalToSuperview()
  179. make.top.equalTo(cover.snp.bottom).offset(-35)
  180. make.bottom.equalToSuperview()
  181. }
  182. let userInfoView = buildUserInfoView()
  183. scrollView.addSubview(userInfoView)
  184. userInfoView.snp.makeConstraints { make in
  185. make.horizontalEdges.equalToSuperview()
  186. make.bottom.equalTo(gameView.snp.top).offset(-10)
  187. }
  188. let menu = buildBottomMenu()
  189. view.addSubview(menu)
  190. menu.snp.makeConstraints { make in
  191. make.horizontalEdges.equalToSuperview()
  192. make.bottom.equalToSuperview()
  193. }
  194. }
  195. private func setupNavBar() {
  196. let menu = buildButtonMenu()
  197. setRightButton(menu)
  198. let container = UIView()
  199. avatar.alpha = 0.0
  200. avatar.layer.cornerRadius = 16
  201. avatar.clipsToBounds = true
  202. container.addSubview(avatar)
  203. avatar.snp.makeConstraints { make in
  204. make.leading.equalToSuperview()
  205. make.centerY.equalToSuperview()
  206. make.width.height.equalTo(32)
  207. }
  208. titleLabel.alpha = 0.0
  209. titleLabel.font = .heading_h3
  210. titleLabel.textColor = .text_5
  211. titleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
  212. titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
  213. container.addSubview(titleLabel)
  214. titleLabel.snp.makeConstraints { make in
  215. make.leading.equalTo(avatar.snp.trailing).offset(12)
  216. make.centerY.equalToSuperview()
  217. make.trailing.lessThanOrEqualToSuperview().offset(-37)
  218. }
  219. setTitleView(container)
  220. container.snp.makeConstraints { make in
  221. make.width.equalTo(view.bounds.width).priority(.medium)
  222. make.height.equalTo(44)
  223. }
  224. updateProgress(0.0)
  225. }
  226. private func buildButtonMenu() -> UIView {
  227. let stackView = UIStackView()
  228. stackView.axis = .horizontal
  229. stackView.spacing = 14
  230. followButton.layer.cornerRadius = 16
  231. followButton.isHidden = true
  232. followButton.setImage(.icSkillFollow.withRenderingMode(.alwaysTemplate), for: .normal)
  233. followButton.snp.makeConstraints { make in
  234. make.width.height.equalTo(32)
  235. }
  236. followButton.addAction(UIAction(handler: { [weak self] _ in
  237. guard let self else { return }
  238. guard let detail else { return }
  239. LNRelationManager.shared.operateFollow(uid: detail.userNo, follow: true, handler: nil)
  240. }), for: .touchUpInside)
  241. stackView.addArrangedSubview(followButton)
  242. profileButton.layer.cornerRadius = 16
  243. profileButton.setImage(.icSkillToProfile.withRenderingMode(.alwaysTemplate), for: .normal)
  244. profileButton.snp.makeConstraints { make in
  245. make.width.height.equalTo(32)
  246. }
  247. profileButton.addAction(UIAction(handler: { [weak self] _ in
  248. guard let self else { return }
  249. guard let detail else { return }
  250. view.pushToProfile(uid: detail.userNo)
  251. }), for: .touchUpInside)
  252. stackView.addArrangedSubview(profileButton)
  253. moreButton.layer.cornerRadius = 16
  254. moreButton.setImage(.icMore.withRenderingMode(.alwaysTemplate), for: .normal)
  255. moreButton.snp.makeConstraints { make in
  256. make.width.height.equalTo(32)
  257. }
  258. moreButton.addAction(UIAction(handler: { [weak self] _ in
  259. guard let self else { return }
  260. showMoreMenu()
  261. }), for: .touchUpInside)
  262. stackView.addArrangedSubview(moreButton)
  263. return stackView
  264. }
  265. private func buildCover() -> UIView {
  266. let coverGradient = CAGradientLayer()
  267. coverGradient.colors = [UIColor.black.withAlphaComponent(0).cgColor, UIColor.black.cgColor]
  268. coverGradient.locations = [0, 1]
  269. coverGradient.startPoint = .init(x: 0, y: 0)
  270. coverGradient.endPoint = .init(x: 0, y: 1)
  271. cover.layer.addSublayer(coverGradient)
  272. cover.publisher(for: \.bounds).removeDuplicates().sink { [weak coverGradient] newValue in
  273. guard let coverGradient else { return }
  274. coverGradient.frame = .init(
  275. x: 0,
  276. y: newValue.height * 0.5,
  277. width: newValue.width,
  278. height: newValue.height * 0.5
  279. )
  280. }.store(in: &bag)
  281. cover.contentMode = .scaleAspectFill
  282. cover.isUserInteractionEnabled = true
  283. cover.clipsToBounds = true
  284. cover.onTap { [weak self] in
  285. guard let self else { return }
  286. guard let cover = detail?.cover.isEmpty != false ? detail?.avatar : detail?.cover else { return }
  287. view.presentImagePreview([cover], 0)
  288. }
  289. cover.snp.makeConstraints { make in
  290. make.height.equalTo(cover.snp.width).multipliedBy(363.0/375.0)
  291. }
  292. return cover
  293. }
  294. private func buildUserInfoView() -> UIView {
  295. return userInfoView
  296. }
  297. private func buildGameView() -> UIView {
  298. let container = UIView()
  299. container.backgroundColor = .fill
  300. container.layer.cornerRadius = 20
  301. container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
  302. gameNameLabel.font = .heading_h3
  303. gameNameLabel.textColor = .text_5
  304. container.addSubview(gameNameLabel)
  305. gameNameLabel.snp.makeConstraints { make in
  306. make.leading.equalToSuperview().offset(16)
  307. make.top.equalToSuperview().offset(16)
  308. }
  309. let line = UIView()
  310. line.backgroundColor = .fill_2
  311. container.addSubview(line)
  312. line.snp.makeConstraints { make in
  313. make.horizontalEdges.equalToSuperview().inset(16)
  314. make.top.equalTo(gameNameLabel.snp.bottom).offset(10)
  315. make.height.equalTo(1)
  316. }
  317. let stackView = UIStackView()
  318. stackView.axis = .vertical
  319. stackView.spacing = 12
  320. container.addSubview(stackView)
  321. stackView.snp.makeConstraints { make in
  322. make.horizontalEdges.equalToSuperview().inset(16)
  323. make.top.equalTo(line.snp.bottom).offset(10)
  324. make.bottom.equalToSuperview()
  325. }
  326. descLabel.font = .body_m
  327. descLabel.textColor = .text_4
  328. descLabel.numberOfLines = 0
  329. descLabel.isHidden = true
  330. stackView.addArrangedSubview(descLabel)
  331. tagView.isHidden = true
  332. stackView.addArrangedSubview(tagView)
  333. photosView.isHidden = true
  334. stackView.addArrangedSubview(photosView)
  335. commentsView.isHidden = true
  336. stackView.addArrangedSubview(commentsView)
  337. return container
  338. }
  339. private func buildBottomMenu() -> UIView {
  340. return bottomMenu
  341. }
  342. }
  343. #if DEBUG
  344. import SwiftUI
  345. struct LNSkillDetailViewControllerPreview: UIViewControllerRepresentable {
  346. func makeUIViewController(context: Context) -> some UIViewController {
  347. LNNavigationController(rootViewController: LNSkillDetailViewController(skillId: ""))
  348. }
  349. func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
  350. }
  351. #Preview(body: {
  352. LNSkillDetailViewControllerPreview()
  353. })
  354. #endif