LNPostShareViewController.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. //
  2. // LNPostShareViewController.swift
  3. // Lanu
  4. //
  5. // Created by OneeChan on 2025/11/27.
  6. //
  7. import Foundation
  8. import UIKit
  9. import SnapKit
  10. import Combine
  11. extension UIView {
  12. func pushToPostShare() {
  13. let vc = LNPostShareViewController()
  14. navigationController?.pushViewController(vc, animated: true)
  15. }
  16. }
  17. class LNPostShareViewController: LNViewController {
  18. private let postView = UIImageView()
  19. private let skillStackView = UIStackView()
  20. private var selectedSkills: [LNGameMateSkillVO] = []
  21. private var lastDesc: String?
  22. override func viewDidLoad() {
  23. super.viewDidLoad()
  24. setupViews()
  25. prefetchSkillIcon()
  26. loadLastOrder()
  27. selectedSkills = Array(myGameMateInfo?.skills.prefix(3) ?? [])
  28. updateSkillsView()
  29. }
  30. }
  31. extension LNPostShareViewController {
  32. private func prefetchSkillIcon() {
  33. myGameMateInfo?.skills.forEach {
  34. SDWebImageManager.shared.loadImage(with: URL(string: $0.icon),
  35. progress: nil)
  36. { _, _, _, _, _, _ in }
  37. }
  38. }
  39. private func loadLastOrder() {
  40. LNOrderManager.shared.getLastOrder { [weak self] res in
  41. guard let self else { return }
  42. guard let res else { return }
  43. lastDesc = .init(key: "A00220", Double(res.finishTime).formattedDate("."), res.nickname, res.bizCateGoryName)
  44. }
  45. }
  46. }
  47. extension LNPostShareViewController {
  48. private func updateSkillsView() {
  49. skillStackView.arrangedSubviews.forEach {
  50. skillStackView.removeArrangedSubview($0)
  51. $0.removeFromSuperview()
  52. }
  53. selectedSkills.prefix(3).forEach {
  54. let itemView = LNPostShareSkillItemView()
  55. itemView.icon.sd_setImage(with: URL(string: $0.icon))
  56. itemView.nameLabel.text = $0.name
  57. skillStackView.addArrangedSubview(itemView)
  58. }
  59. }
  60. private func showSkillSelectPanel() {
  61. guard let skills = myGameMateInfo?.skills,
  62. !skills.isEmpty else { return }
  63. let panel = LNPostSkillSelectPanel()
  64. panel.setSkills(skills: skills, selecteds: selectedSkills)
  65. panel.handler = { [weak self] selections in
  66. guard let self else { return }
  67. selectedSkills = selections
  68. updateSkillsView()
  69. }
  70. panel.popup(view)
  71. }
  72. private func setupViews() {
  73. title = .init(key: "A00221")
  74. view.backgroundColor = .primary_1
  75. let topCover = UIImageView()
  76. topCover.image = .icHomeTopBg
  77. view.addSubview(topCover)
  78. topCover.snp.makeConstraints { make in
  79. make.top.leading.trailing.equalToSuperview()
  80. }
  81. let scrollView = UIScrollView()
  82. scrollView.backgroundColor = .clear
  83. scrollView.showsHorizontalScrollIndicator = false
  84. scrollView.showsVerticalScrollIndicator = false
  85. view.addSubview(scrollView)
  86. scrollView.snp.makeConstraints { make in
  87. make.edges.equalToSuperview()
  88. }
  89. let fakeView = UIView()
  90. scrollView.addSubview(fakeView)
  91. fakeView.snp.makeConstraints { make in
  92. make.horizontalEdges.equalToSuperview()
  93. make.top.equalToSuperview()
  94. make.width.equalToSuperview()
  95. make.height.equalTo(0)
  96. }
  97. let post = buildPost()
  98. scrollView.addSubview(post)
  99. post.snp.makeConstraints { make in
  100. make.horizontalEdges.equalToSuperview().inset(16)
  101. make.top.equalToSuperview().offset(32)
  102. }
  103. let skill = buildSkillView()
  104. scrollView.addSubview(skill)
  105. skill.snp.makeConstraints { make in
  106. make.top.equalTo(post.snp.bottom)
  107. make.horizontalEdges.equalToSuperview().inset(16)
  108. make.height.equalTo(100)
  109. }
  110. let album = buildAlbum()
  111. scrollView.addSubview(album)
  112. album.snp.makeConstraints { make in
  113. make.leading.equalToSuperview().offset(16)
  114. make.top.equalTo(skill.snp.bottom).offset(20)
  115. make.trailing.equalToSuperview()
  116. make.bottom.equalToSuperview().offset(-view.safeBottomInset - 45)
  117. }
  118. let share = UIButton()
  119. share.setTitle(.init(key: "A00222"), for: .normal)
  120. share.setTitleColor(.text_1, for: .normal)
  121. share.setBackgroundImage(.primary_8, for: .normal)
  122. share.layer.cornerRadius = 23.5
  123. share.clipsToBounds = true
  124. share.addAction(UIAction(handler: { [weak self] _ in
  125. guard let self else { return }
  126. guard let info = myGameMateInfo else { return }
  127. guard let album = postView.image else { return }
  128. let generator = LNPostShareImageGenerator()
  129. generator.setInfo(info: info)
  130. generator.setOrderInfo(desc: lastDesc)
  131. generator.setAlbum(image: album)
  132. generator.setSkills(skills: selectedSkills)
  133. generator.setShareQRCode(url: .profileShareUrl + "?id=\(myUid)&share=app")
  134. let shareImage = generator.generate()
  135. shareImage.saveToLibrary { [weak self] success, err in
  136. guard let self else { return }
  137. if let err {
  138. showToast(err.localizedDescription)
  139. } else {
  140. showToast(.init(key: "A00157"))
  141. navigationController?.popViewController(animated: true)
  142. }
  143. }
  144. }), for: .touchUpInside)
  145. view.addSubview(share)
  146. share.snp.makeConstraints { make in
  147. make.horizontalEdges.equalToSuperview().inset(16)
  148. make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom).offset(-4)
  149. make.height.equalTo(47)
  150. }
  151. }
  152. private func buildPost() -> UIView {
  153. let container = UIView()
  154. container.backgroundColor = .fill
  155. container.layer.cornerRadius = 20
  156. let info = buildUserInfo()
  157. container.addSubview(info)
  158. info.snp.makeConstraints { make in
  159. make.horizontalEdges.equalToSuperview().inset(16)
  160. make.top.equalToSuperview().offset(12)
  161. }
  162. postView.contentMode = .scaleAspectFill
  163. postView.layer.cornerRadius = 20
  164. postView.clipsToBounds = true
  165. container.addSubview(postView)
  166. postView.snp.makeConstraints { make in
  167. make.horizontalEdges.equalToSuperview().inset(16)
  168. make.top.equalToSuperview().offset(44)
  169. make.height.equalTo(postView.snp.width).multipliedBy(317.0/311.0)
  170. }
  171. let bio = UILabel()
  172. bio.font = .body_m
  173. bio.textColor = .text_4
  174. bio.numberOfLines = 0
  175. bio.text = myGameMateInfo?.intro ?? ""
  176. container.addSubview(bio)
  177. bio.snp.makeConstraints { make in
  178. make.horizontalEdges.equalToSuperview().inset(22)
  179. make.top.equalTo(postView.snp.bottom).offset(14)
  180. make.bottom.equalToSuperview().offset(-20)
  181. }
  182. let line = buildDashLine()
  183. container.layer.addSublayer(line)
  184. container.publisher(for: \.bounds).removeDuplicates().sink
  185. { [weak line] newValue in
  186. guard let line else { return }
  187. line.frame = .init(
  188. x: 14,
  189. y: newValue.height - 1,
  190. width: newValue.width - 28,
  191. height: 1)
  192. let path = UIBezierPath()
  193. path.move(to: CGPoint(x: 0, y: 0))
  194. path.addLine(to: CGPoint(x: newValue.width - 28, y: 0))
  195. line.path = path.cgPath
  196. }.store(in: &bag)
  197. return container
  198. }
  199. private func buildUserInfo() -> UIView {
  200. let container = UIView()
  201. container.snp.makeConstraints { make in
  202. make.height.equalTo(22)
  203. }
  204. let userNameLabel = UILabel()
  205. userNameLabel.text = myUserInfo.nickname
  206. userNameLabel.font = .heading_h2
  207. userNameLabel.textColor = .text_5
  208. container.addSubview(userNameLabel)
  209. userNameLabel.snp.makeConstraints { make in
  210. make.leading.centerY.equalToSuperview()
  211. }
  212. let gender = LNGenderView()
  213. gender.update(myUserInfo.gender, myUserInfo.age)
  214. container.addSubview(gender)
  215. gender.snp.makeConstraints { make in
  216. make.centerY.equalToSuperview()
  217. make.leading.equalTo(userNameLabel.snp.trailing).offset(4)
  218. }
  219. let starLabel = UILabel()
  220. starLabel.text = "\(myGameMateInfo?.star ?? 0)"
  221. starLabel.font = .heading_h2
  222. starLabel.textColor = .text_5
  223. container.addSubview(starLabel)
  224. starLabel.snp.makeConstraints { make in
  225. make.centerY.equalToSuperview()
  226. make.trailing.equalToSuperview()
  227. }
  228. let star = LNStarScoreView()
  229. star.score = 1.0
  230. star.icSize = 18
  231. container.addSubview(star)
  232. star.snp.makeConstraints { make in
  233. make.centerY.equalToSuperview()
  234. make.trailing.equalTo(starLabel.snp.leading).offset(-4)
  235. }
  236. return container
  237. }
  238. private func buildDashLine() -> CAShapeLayer {
  239. let lineLayer = CAShapeLayer()
  240. lineLayer.frame = .zero
  241. lineLayer.fillColor = UIColor.clear.cgColor
  242. lineLayer.strokeColor = UIColor.fill_3.cgColor
  243. lineLayer.lineWidth = 2.0
  244. lineLayer.lineDashPattern = [NSNumber(value: 3), NSNumber(value: 3)]
  245. return lineLayer
  246. }
  247. private func buildSkillView() -> UIView {
  248. let container = UIView()
  249. container.backgroundColor = .fill
  250. container.layer.cornerRadius = 20
  251. container.onTap { [weak self] in
  252. guard let self else { return }
  253. showSkillSelectPanel()
  254. }
  255. skillStackView.axis = .horizontal
  256. skillStackView.spacing = 10
  257. container.addSubview(skillStackView)
  258. skillStackView.snp.makeConstraints { make in
  259. make.leading.equalToSuperview().offset(113)
  260. make.bottom.equalToSuperview().offset(-18)
  261. }
  262. let ic = UIImageView()
  263. ic.image = .icHomeTabSelected
  264. ic.alpha = 0.5
  265. container.addSubview(ic)
  266. ic.snp.makeConstraints { make in
  267. make.leading.equalToSuperview().offset(20)
  268. make.top.equalToSuperview().offset(31)
  269. }
  270. let tipsLabel = UILabel()
  271. tipsLabel.text = .init(key: "A00219")
  272. tipsLabel.font = .heading_h4
  273. tipsLabel.textColor = .text_5
  274. tipsLabel.numberOfLines = 0
  275. container.addSubview(tipsLabel)
  276. tipsLabel.snp.makeConstraints { make in
  277. make.centerY.equalToSuperview()
  278. make.leading.equalToSuperview().offset(30)
  279. make.width.equalTo(60)
  280. }
  281. let arrow = UIImageView.arrowImageView(size: 16)
  282. container.addSubview(arrow)
  283. arrow.snp.makeConstraints { make in
  284. make.centerY.equalToSuperview()
  285. make.trailing.equalToSuperview().offset(-8)
  286. }
  287. return container
  288. }
  289. private func buildAlbum() -> UIView {
  290. let scrollView = UIScrollView()
  291. scrollView.showsVerticalScrollIndicator = false
  292. scrollView.showsHorizontalScrollIndicator = false
  293. let fakeView = UIView()
  294. scrollView.addSubview(fakeView)
  295. fakeView.snp.makeConstraints { make in
  296. make.verticalEdges.equalToSuperview()
  297. make.height.equalToSuperview()
  298. make.leading.equalToSuperview()
  299. make.width.equalTo(0)
  300. }
  301. let stackView = UIStackView()
  302. stackView.axis = .horizontal
  303. stackView.spacing = 10
  304. scrollView.addSubview(stackView)
  305. stackView.snp.makeConstraints { make in
  306. make.edges.equalToSuperview()
  307. }
  308. var albumItemViews: [LNSharePostAlbumItemView] = []
  309. myGameMateInfo?.photos.forEach { url in
  310. let album = LNSharePostAlbumItemView()
  311. album.imageView.sd_setImage(with: URL(string: url))
  312. album.onTap { [weak self, weak album] in
  313. guard let self, let album else { return }
  314. albumItemViews.forEach {
  315. $0.isSelected = $0 == album
  316. }
  317. postView.sd_setImage(with: URL(string: url))
  318. }
  319. album.isSelected = false
  320. stackView.addArrangedSubview(album)
  321. albumItemViews.append(album)
  322. }
  323. albumItemViews.first?.isSelected = true
  324. postView.sd_setImage(with: URL(string: myGameMateInfo?.photos.first ?? ""))
  325. return scrollView
  326. }
  327. }
  328. private class LNSharePostAlbumItemView: UIView {
  329. let imageView = UIImageView()
  330. var isSelected: Bool = false {
  331. didSet {
  332. if isSelected {
  333. imageView.layer.cornerRadius = 11
  334. imageView.snp.remakeConstraints { make in
  335. make.center.equalToSuperview()
  336. make.width.height.equalToSuperview().inset(2)
  337. }
  338. selectedBorder.isHidden = false
  339. alpha = 1.0
  340. } else {
  341. imageView.layer.cornerRadius = 12
  342. imageView.snp.remakeConstraints { make in
  343. make.center.equalToSuperview()
  344. make.width.height.equalToSuperview()
  345. }
  346. selectedBorder.isHidden = true
  347. alpha = 0.5
  348. }
  349. }
  350. }
  351. private let selectedBorder = UIImageView()
  352. override init(frame: CGRect) {
  353. super.init(frame: frame)
  354. snp.makeConstraints { make in
  355. make.width.height.equalTo(90)
  356. }
  357. selectedBorder.image = .primary_7
  358. selectedBorder.layer.cornerRadius = 12
  359. selectedBorder.clipsToBounds = true
  360. selectedBorder.isHidden = true
  361. addSubview(selectedBorder)
  362. selectedBorder.snp.makeConstraints { make in
  363. make.edges.equalToSuperview()
  364. }
  365. imageView.clipsToBounds = true
  366. imageView.contentMode = .scaleAspectFill
  367. addSubview(imageView)
  368. }
  369. required init?(coder: NSCoder) {
  370. fatalError("init(coder:) has not been implemented")
  371. }
  372. }
  373. #if DEBUG
  374. import SwiftUI
  375. struct LNSharePostViewControllerPreview: UIViewControllerRepresentable {
  376. func makeUIViewController(context: Context) -> some UIViewController {
  377. LNPostShareViewController()
  378. }
  379. func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
  380. }
  381. #Preview(body: {
  382. LNSharePostViewControllerPreview()
  383. })
  384. #endif