LNSkillDetailViewController.swift 15 KB

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