LNEditProfileViewController.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. //
  2. // LNEditProfileViewController.swift
  3. // Lanu
  4. //
  5. // Created by OneeChan on 2025/11/26.
  6. //
  7. import Foundation
  8. import UIKit
  9. import SnapKit
  10. extension UIView {
  11. func pushToEditProfile() {
  12. let vc = LNEditProfileViewController()
  13. navigationController?.pushViewController(vc, animated: true)
  14. }
  15. }
  16. class LNEditProfileViewController: LNViewController {
  17. private let saveButton = UIButton()
  18. private let profilePhoto = LNImageUploadView()
  19. private var curProfilePhoto = "" {
  20. didSet {
  21. profilePhoto.loadImage(url: curProfilePhoto)
  22. }
  23. }
  24. private let photoWall = LNEditProfilePhotoWallView()
  25. private let nameLabel = UILabel()
  26. private var curName = "" {
  27. didSet {
  28. nameLabel.text = curName
  29. }
  30. }
  31. private let genderLabel = UILabel()
  32. private var curGender: LNUserGender = .unknow {
  33. didSet {
  34. genderLabel.text = curGender.desc
  35. }
  36. }
  37. private let birthdayLabel = UILabel()
  38. private var curBirthday: Double = 0 {
  39. didSet {
  40. birthdayLabel.text = curBirthday.formattedFullDate("-")
  41. }
  42. }
  43. private let bioLabel = UILabel()
  44. private var curBio = "" {
  45. didSet {
  46. if curBio.isEmpty {
  47. bioLabel.text = nil
  48. bioLabel.attributedText = .init(string: .init(key: "A00200"), attributes: [.foregroundColor: UIColor.text_6])
  49. } else {
  50. bioLabel.attributedText = nil
  51. bioLabel.text = curBio
  52. }
  53. }
  54. }
  55. private let interestLabel = UILabel()
  56. private var curInterest: [LNUserInterestVO] = [] {
  57. didSet {
  58. if curInterest.isEmpty {
  59. interestLabel.text = nil
  60. interestLabel.attributedText = .init(string: .init(key: "A00201"), attributes: [.foregroundColor: UIColor.text_6])
  61. } else {
  62. interestLabel.attributedText = nil
  63. interestLabel.text = curInterest.map({ $0.name }).joined(separator: ", ")
  64. }
  65. }
  66. }
  67. private let voiceLabel = UILabel()
  68. override func viewDidLoad() {
  69. super.viewDidLoad()
  70. setupViews()
  71. loadUserInfo()
  72. checkSaveButton()
  73. LNEventDeliver.addObserver(self)
  74. }
  75. }
  76. extension LNEditProfileViewController {
  77. private func loadUserInfo() {
  78. curBio = myUserInfo.intro
  79. curInterest = myUserInfo.interests
  80. curProfilePhoto = myUserInfo.avatar
  81. curName = myUserInfo.nickname
  82. curGender = myUserInfo.gender
  83. curBirthday = Double(myUserInfo.birthday / 1_000)
  84. updateVoiceText()
  85. }
  86. }
  87. extension LNEditProfileViewController: LNProfileManagerNotify {
  88. func onUserVoiceBarInfoChanged() {
  89. updateVoiceText()
  90. }
  91. func onUserInfoChanged(userInfo: LNUserProfileVO) {
  92. guard userInfo.userNo.isMyUid else { return }
  93. updateVoiceText()
  94. }
  95. private func updateVoiceText() {
  96. if myVoiceBarInfo.status == .done {
  97. voiceLabel.attributedText = nil
  98. voiceLabel.text = .init(key: "B00002")
  99. } else if myVoiceBarInfo.status == .review {
  100. voiceLabel.attributedText = nil
  101. voiceLabel.text = .init(key: "B00003")
  102. } else if myUserInfo.voiceBar.isEmpty {
  103. voiceLabel.text = nil
  104. voiceLabel.attributedText = .init(string: .init(key: "B00001"), attributes: [.foregroundColor: UIColor.text_6])
  105. }
  106. }
  107. }
  108. extension LNEditProfileViewController: LNImageUploadViewDelegate {
  109. func onImageUploadView(view: LNImageUploadView, didUploadImage url: String) {
  110. checkSaveButton()
  111. }
  112. func onImageUploadViewDidClickDelete(view: LNImageUploadView) {
  113. checkSaveButton()
  114. }
  115. }
  116. extension LNEditProfileViewController: LNEditProfilePhotoWallViewDelegate {
  117. func onEditProfilePhotoWallViewDidChanged(view: LNEditProfilePhotoWallView) {
  118. checkSaveButton()
  119. }
  120. }
  121. extension LNEditProfileViewController {
  122. private func checkSaveButton() {
  123. let enable = profilePhoto.imageUrl != myUserInfo.avatar
  124. || photoWall.curPhotos != myUserInfo.photos
  125. || curName != myUserInfo.nickname
  126. || curGender != myUserInfo.gender
  127. || curBirthday != Double(myUserInfo.birthday / 1_000)
  128. || curBio != myUserInfo.intro
  129. || curInterest.map({ $0.code }) != myUserInfo.interests.map({ $0.code })
  130. if enable, !saveButton.isEnabled {
  131. saveButton.isEnabled = true
  132. saveButton.setBackgroundImage(.primary_8, for: .normal)
  133. } else if !enable, saveButton.isEnabled {
  134. saveButton.isEnabled = false
  135. saveButton.setBackgroundImage(nil, for: .normal)
  136. }
  137. }
  138. private func setupViews() {
  139. setupNavBar()
  140. let scrollView = UIScrollView()
  141. scrollView.showsVerticalScrollIndicator = false
  142. scrollView.showsHorizontalScrollIndicator = false
  143. scrollView.clipsToBounds = false
  144. scrollView.contentInset = .init(top: 0, left: 0, bottom: -view.commonBottomInset, right: 0)
  145. view.addSubview(scrollView)
  146. scrollView.snp.makeConstraints { make in
  147. make.top.equalToSuperview()
  148. make.horizontalEdges.equalToSuperview()
  149. make.bottom.equalToSuperview()
  150. }
  151. let stackView = UIStackView()
  152. stackView.axis = .vertical
  153. stackView.spacing = 24
  154. scrollView.addSubview(stackView)
  155. stackView.snp.makeConstraints { make in
  156. make.verticalEdges.equalToSuperview()
  157. make.horizontalEdges.equalToSuperview().inset(16)
  158. make.width.equalToSuperview().offset(-32)
  159. }
  160. stackView.addArrangedSubview(buildProfilePhoto())
  161. stackView.addArrangedSubview(buildPhotoWall())
  162. stackView.addArrangedSubview(buildUserInfo())
  163. stackView.addArrangedSubview(buildSkillInfo())
  164. }
  165. private func setupNavBar() {
  166. customBack = { [weak self] in
  167. guard let self else { return }
  168. if saveButton.isEnabled {
  169. LNCommonAlertView.showProfileEditEnsaveAlert { [weak self] in
  170. guard let self else { return }
  171. navigationController?.popViewController(animated: true)
  172. }
  173. } else {
  174. navigationController?.popViewController(animated: true)
  175. }
  176. }
  177. title = .init(key: "A00202")
  178. saveButton.backgroundColor = .fill_4
  179. saveButton.layer.cornerRadius = 16
  180. saveButton.setBackgroundImage(.primary_8, for: .normal)
  181. saveButton.clipsToBounds = true
  182. saveButton.addAction(UIAction(handler: { [weak self] _ in
  183. guard let self else { return }
  184. let config = LNProfileUpdateConfig()
  185. if profilePhoto.imageUrl != myUserInfo.avatar {
  186. config.avatar = profilePhoto.imageUrl
  187. }
  188. let photos = photoWall.curPhotos
  189. if photos != myUserInfo.photos {
  190. config.photos = photos
  191. }
  192. if curName != myUserInfo.nickname {
  193. config.nickName = curName
  194. }
  195. if curGender != myUserInfo.gender {
  196. config.gender = curGender
  197. }
  198. if curBirthday != Double(myUserInfo.birthday / 1_000) {
  199. config.birthday = curBirthday.formattedFullDate("-", normal: true)
  200. }
  201. if curBio != myUserInfo.intro {
  202. config.bio = curBio
  203. }
  204. if curInterest.map({ $0.code }) != myUserInfo.interests.map({ $0.code }) {
  205. config.interest = curInterest.map({ $0.code })
  206. }
  207. LNProfileManager.shared.modifyMyProfile(config: config) { [weak self] success in
  208. guard let self else { return }
  209. guard success else { return }
  210. navigationController?.popViewController(animated: true)
  211. }
  212. }), for: .touchUpInside)
  213. setRightButton(saveButton)
  214. saveButton.snp.makeConstraints { make in
  215. make.height.equalTo(32)
  216. }
  217. let saveLabel = UILabel()
  218. saveLabel.text = .init(key: "A00185")
  219. saveLabel.font = .heading_h4
  220. saveLabel.textColor = .text_1
  221. saveButton.addSubview(saveLabel)
  222. saveLabel.snp.makeConstraints { make in
  223. make.center.equalToSuperview()
  224. make.leading.equalToSuperview().offset(17)
  225. }
  226. }
  227. private func buildProfilePhoto() -> UIView {
  228. let container = UIView()
  229. let titleLabel = UILabel()
  230. titleLabel.font = .heading_h4
  231. titleLabel.textColor = .text_5
  232. titleLabel.text = .init(key: "A00203")
  233. container.addSubview(titleLabel)
  234. titleLabel.snp.makeConstraints { make in
  235. make.horizontalEdges.equalToSuperview()
  236. make.top.equalToSuperview().offset(10)
  237. }
  238. profilePhoto.uploadType = .avatar
  239. profilePhoto.backgroundColor = .fill_2
  240. profilePhoto.layer.cornerRadius = 11
  241. profilePhoto.clipsToBounds = true
  242. profilePhoto.contentMode = .scaleAspectFill
  243. profilePhoto.showClearButton = false
  244. profilePhoto.showDefault = true
  245. profilePhoto.delegate = self
  246. profilePhoto.onTap { [weak self] in
  247. guard let self else { return }
  248. LNBottomSheetMenu.showImageSelectMenu(view: view, options: .init(allowEdit: true))
  249. { [weak self] image, _ in
  250. guard let self else { return }
  251. guard let image = image?.compress(type: .avatar) else { return }
  252. profilePhoto.uploadImage(image: image)
  253. }
  254. }
  255. container.addSubview(profilePhoto)
  256. profilePhoto.snp.makeConstraints { make in
  257. make.leading.equalToSuperview()
  258. make.top.equalTo(titleLabel.snp.bottom).offset(10)
  259. make.bottom.equalToSuperview()
  260. make.height.width.equalTo(110)
  261. }
  262. return container
  263. }
  264. private func buildPhotoWall() -> UIView {
  265. photoWall.delegate = self
  266. photoWall.loadImages(myUserInfo.photos)
  267. return photoWall
  268. }
  269. private func buildUserInfo() -> UIView {
  270. let container = UIView()
  271. let titleLabel = UILabel()
  272. titleLabel.font = .heading_h4
  273. titleLabel.textColor = .text_5
  274. titleLabel.text = .init(key: "A00204")
  275. container.addSubview(titleLabel)
  276. titleLabel.snp.makeConstraints { make in
  277. make.horizontalEdges.equalToSuperview()
  278. make.top.equalToSuperview().offset(10)
  279. }
  280. let stackView = UIStackView()
  281. stackView.axis = .vertical
  282. stackView.spacing = 22
  283. container.addSubview(stackView)
  284. stackView.snp.makeConstraints { make in
  285. make.horizontalEdges.equalToSuperview()
  286. make.top.equalTo(titleLabel.snp.bottom).offset(10)
  287. make.bottom.equalToSuperview()
  288. }
  289. let nameView = buildInfoLine(title: .init(key: "A00188"), label: nameLabel)
  290. nameView.onTap { [weak self] in
  291. guard let self else { return }
  292. let panel = LNEditNickNamePanel()
  293. panel.inputField.text = curName
  294. panel.handler = { [weak self] name in
  295. guard let self else { return }
  296. curName = name
  297. checkSaveButton()
  298. }
  299. panel.popup()
  300. }
  301. stackView.addArrangedSubview(nameView)
  302. let genderView = buildInfoLine(title: .init(key: "A00032"), label: genderLabel)
  303. genderView.onTap { [weak self] in
  304. guard let self else { return }
  305. let panel = LNEditGenderPanel()
  306. panel.curGender = curGender
  307. panel.handler = { [weak self] gender in
  308. guard let self else { return }
  309. curGender = gender
  310. checkSaveButton()
  311. }
  312. panel.popup()
  313. }
  314. stackView.addArrangedSubview(genderView)
  315. let birthdayView = buildInfoLine(title: .init(key: "A00205"), label: birthdayLabel)
  316. birthdayView.onTap { [weak self] in
  317. guard let self else { return }
  318. let panel = LNDatePickerPanel()
  319. panel.titleLabel.text = .init(key: "A00007")
  320. panel.datePicker.maximumDate = Date().yearAgo(-18)
  321. panel.setDefault(curBirthday)
  322. panel.handler = { [weak self] date in
  323. guard let self else { return }
  324. curBirthday = date
  325. checkSaveButton()
  326. }
  327. panel.popup()
  328. }
  329. stackView.addArrangedSubview(birthdayView)
  330. let bioView = buildInfoLine(title: .init(key: "A00184"), label: bioLabel)
  331. bioView.onTap { [weak self] in
  332. guard let self else { return }
  333. let panel = LNEditBioPanel()
  334. panel.update(curBio)
  335. panel.handler = { [weak self] bio in
  336. guard let self else { return }
  337. curBio = bio
  338. checkSaveButton()
  339. }
  340. panel.popup()
  341. }
  342. stackView.addArrangedSubview(bioView)
  343. let interestView = buildInfoLine(title: .init(key: "A00206"), label: interestLabel)
  344. interestView.onTap { [weak self] in
  345. guard let self else { return }
  346. let panel = LNEditInterestPanel(selecteds: curInterest.map({ $0.code }))
  347. panel.handler = { [weak self] items in
  348. guard let self else { return }
  349. curInterest = items.map({
  350. LNUserInterestVO(code: $0.code, name: $0.name)
  351. })
  352. checkSaveButton()
  353. }
  354. panel.popup()
  355. }
  356. stackView.addArrangedSubview(interestView)
  357. let line = buildLine()
  358. container.addSubview(line)
  359. line.snp.makeConstraints { make in
  360. make.horizontalEdges.equalToSuperview().inset(16)
  361. make.top.equalToSuperview()
  362. }
  363. return container
  364. }
  365. private func buildSkillInfo() -> UIView {
  366. let container = UIView()
  367. let titleLabel = UILabel()
  368. titleLabel.font = .heading_h4
  369. titleLabel.textColor = .text_5
  370. titleLabel.text = .init(key: "B00004")
  371. container.addSubview(titleLabel)
  372. titleLabel.snp.makeConstraints { make in
  373. make.horizontalEdges.equalToSuperview()
  374. make.top.equalToSuperview().offset(10)
  375. }
  376. let stackView = UIStackView()
  377. stackView.axis = .vertical
  378. stackView.spacing = 22
  379. container.addSubview(stackView)
  380. stackView.snp.makeConstraints { make in
  381. make.horizontalEdges.equalToSuperview()
  382. make.top.equalTo(titleLabel.snp.bottom).offset(10)
  383. make.bottom.equalToSuperview()
  384. }
  385. let voiceView = buildInfoLine(title: .init(key: "B00005"), label: voiceLabel)
  386. voiceView.onTap { [weak self] in
  387. guard let self else { return }
  388. let panel = LNEditVoicePanel()
  389. panel.popup()
  390. }
  391. stackView.addArrangedSubview(voiceView)
  392. return container
  393. }
  394. private func buildInfoLine(title: String, label: UILabel) -> UIView {
  395. let container = UIView()
  396. let titleLabel = UILabel()
  397. titleLabel.text = title
  398. titleLabel.font = .body_m
  399. titleLabel.textColor = .text_4
  400. titleLabel.setContentHuggingPriority(.required, for: .horizontal)
  401. titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
  402. container.addSubview(titleLabel)
  403. titleLabel.snp.makeConstraints { make in
  404. make.verticalEdges.equalToSuperview()
  405. make.leading.equalToSuperview()
  406. }
  407. let arrow = UIImageView.arrowImageView(size: 14)
  408. arrow.tintColor = .text_4
  409. container.addSubview(arrow)
  410. arrow.snp.makeConstraints { make in
  411. make.centerY.equalToSuperview()
  412. make.trailing.equalToSuperview()
  413. }
  414. label.font = .body_m
  415. label.textColor = .text_3
  416. container.addSubview(label)
  417. label.snp.makeConstraints { make in
  418. make.centerY.equalToSuperview()
  419. make.trailing.equalTo(arrow.snp.leading).offset(-5)
  420. make.leading.greaterThanOrEqualTo(titleLabel.snp.trailing).offset(50)
  421. }
  422. return container
  423. }
  424. private func buildLine() -> UIView {
  425. let line = UIView()
  426. line.backgroundColor = .fill_2
  427. line.snp.makeConstraints { make in
  428. make.height.equalTo(1)
  429. }
  430. return line
  431. }
  432. }
  433. #if DEBUG
  434. import SwiftUI
  435. struct LNEditProfileViewControllerPreview: UIViewControllerRepresentable {
  436. func makeUIViewController(context: Context) -> some UIViewController {
  437. LNEditProfileViewController()
  438. }
  439. func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
  440. }
  441. #Preview(body: {
  442. LNEditProfileViewControllerPreview()
  443. })
  444. #endif