// // LNEditProfileViewController.swift // Lanu // // Created by OneeChan on 2025/11/26. // import Foundation import UIKit import SnapKit extension UIView { func pushToEditProfile() { let vc = LNEditProfileViewController() navigationController?.pushViewController(vc, animated: true) } } class LNEditProfileViewController: LNViewController { private let saveButton = UIButton() private let profilePhoto = LNImageUploadView() private var curProfilePhoto = "" { didSet { profilePhoto.loadImage(url: curProfilePhoto) } } private let photoWall = LNEditProfilePhotoWallView() private let nameLabel = UILabel() private var curName = "" { didSet { nameLabel.text = curName } } private let genderLabel = UILabel() private var curGender: LNUserGender = .unknow { didSet { genderLabel.text = curGender.desc } } private let birthdayLabel = UILabel() private var curBirthday: Double = 0 { didSet { birthdayLabel.text = curBirthday.formattedFullDate("-") } } private let bioLabel = UILabel() private var curBio = "" { didSet { if curBio.isEmpty { bioLabel.text = nil bioLabel.attributedText = .init(string: .init(key: "A00200"), attributes: [.foregroundColor: UIColor.text_6]) } else { bioLabel.attributedText = nil bioLabel.text = curBio } } } private let interestLabel = UILabel() private var curInterest: [LNUserInterestVO] = [] { didSet { if curInterest.isEmpty { interestLabel.text = nil interestLabel.attributedText = .init(string: .init(key: "A00201"), attributes: [.foregroundColor: UIColor.text_6]) } else { interestLabel.attributedText = nil interestLabel.text = curInterest.map({ $0.name }).joined(separator: ", ") } } } private let voiceLabel = UILabel() override func viewDidLoad() { super.viewDidLoad() setupViews() loadUserInfo() checkSaveButton() LNEventDeliver.addObserver(self) } } extension LNEditProfileViewController { private func loadUserInfo() { curBio = myUserInfo.intro curInterest = myUserInfo.interests curProfilePhoto = myUserInfo.avatar curName = myUserInfo.nickname curGender = myUserInfo.gender curBirthday = Double(myUserInfo.birthday / 1_000) updateVoiceText() } } extension LNEditProfileViewController: LNProfileManagerNotify { func onUserVoiceBarInfoChanged() { updateVoiceText() } func onUserInfoChanged(userInfo: LNUserProfileVO) { guard userInfo.userNo.isMyUid else { return } updateVoiceText() } private func updateVoiceText() { if myVoiceBarInfo.status == .done { voiceLabel.attributedText = nil voiceLabel.text = .init(key: "B00002") } else if myVoiceBarInfo.status == .review { voiceLabel.attributedText = nil voiceLabel.text = .init(key: "B00003") } else if myUserInfo.voiceBar.isEmpty { voiceLabel.text = nil voiceLabel.attributedText = .init(string: .init(key: "B00001"), attributes: [.foregroundColor: UIColor.text_6]) } } } extension LNEditProfileViewController: LNImageUploadViewDelegate { func onImageUploadView(view: LNImageUploadView, didUploadImage url: String) { checkSaveButton() } func onImageUploadViewDidClickDelete(view: LNImageUploadView) { checkSaveButton() } } extension LNEditProfileViewController: LNEditProfilePhotoWallViewDelegate { func onEditProfilePhotoWallViewDidChanged(view: LNEditProfilePhotoWallView) { checkSaveButton() } } extension LNEditProfileViewController { private func checkSaveButton() { let enable = profilePhoto.imageUrl != myUserInfo.avatar || photoWall.curPhotos != myUserInfo.photos || curName != myUserInfo.nickname || curGender != myUserInfo.gender || curBirthday != Double(myUserInfo.birthday / 1_000) || curBio != myUserInfo.intro || curInterest.map({ $0.code }) != myUserInfo.interests.map({ $0.code }) if enable, !saveButton.isEnabled { saveButton.isEnabled = true saveButton.setBackgroundImage(.primary_8, for: .normal) } else if !enable, saveButton.isEnabled { saveButton.isEnabled = false saveButton.setBackgroundImage(nil, for: .normal) } } private func setupViews() { setupNavBar() let scrollView = UIScrollView() scrollView.showsVerticalScrollIndicator = false scrollView.showsHorizontalScrollIndicator = false scrollView.clipsToBounds = false scrollView.contentInset = .init(top: 0, left: 0, bottom: -view.commonBottomInset, right: 0) view.addSubview(scrollView) scrollView.snp.makeConstraints { make in make.top.equalToSuperview() make.horizontalEdges.equalToSuperview() make.bottom.equalToSuperview() } let stackView = UIStackView() stackView.axis = .vertical stackView.spacing = 24 scrollView.addSubview(stackView) stackView.snp.makeConstraints { make in make.verticalEdges.equalToSuperview() make.horizontalEdges.equalToSuperview().inset(16) make.width.equalToSuperview().offset(-32) } stackView.addArrangedSubview(buildProfilePhoto()) stackView.addArrangedSubview(buildPhotoWall()) stackView.addArrangedSubview(buildUserInfo()) stackView.addArrangedSubview(buildSkillInfo()) } private func setupNavBar() { customBack = { [weak self] in guard let self else { return } if saveButton.isEnabled { LNCommonAlertView.showProfileEditEnsaveAlert { [weak self] in guard let self else { return } navigationController?.popViewController(animated: true) } } else { navigationController?.popViewController(animated: true) } } title = .init(key: "A00202") saveButton.backgroundColor = .fill_4 saveButton.layer.cornerRadius = 16 saveButton.setBackgroundImage(.primary_8, for: .normal) saveButton.clipsToBounds = true saveButton.addAction(UIAction(handler: { [weak self] _ in guard let self else { return } let config = LNProfileUpdateConfig() if profilePhoto.imageUrl != myUserInfo.avatar { config.avatar = profilePhoto.imageUrl } let photos = photoWall.curPhotos if photos != myUserInfo.photos { config.photos = photos } if curName != myUserInfo.nickname { config.nickName = curName } if curGender != myUserInfo.gender { config.gender = curGender } if curBirthday != Double(myUserInfo.birthday / 1_000) { config.birthday = curBirthday.formattedFullDate("-", normal: true) } if curBio != myUserInfo.intro { config.bio = curBio } if curInterest.map({ $0.code }) != myUserInfo.interests.map({ $0.code }) { config.interest = curInterest.map({ $0.code }) } LNProfileManager.shared.modifyMyProfile(config: config) { [weak self] success in guard let self else { return } guard success else { return } navigationController?.popViewController(animated: true) } }), for: .touchUpInside) setRightButton(saveButton) saveButton.snp.makeConstraints { make in make.height.equalTo(32) } let saveLabel = UILabel() saveLabel.text = .init(key: "A00185") saveLabel.font = .heading_h4 saveLabel.textColor = .text_1 saveButton.addSubview(saveLabel) saveLabel.snp.makeConstraints { make in make.center.equalToSuperview() make.leading.equalToSuperview().offset(17) } } private func buildProfilePhoto() -> UIView { let container = UIView() let titleLabel = UILabel() titleLabel.font = .heading_h4 titleLabel.textColor = .text_5 titleLabel.text = .init(key: "A00203") container.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview() make.top.equalToSuperview().offset(10) } profilePhoto.uploadType = .avatar profilePhoto.backgroundColor = .fill_2 profilePhoto.layer.cornerRadius = 11 profilePhoto.clipsToBounds = true profilePhoto.contentMode = .scaleAspectFill profilePhoto.showClearButton = false profilePhoto.showDefault = true profilePhoto.delegate = self profilePhoto.onTap { [weak self] in guard let self else { return } LNBottomSheetMenu.showImageSelectMenu(view: view, options: .init(allowEdit: true)) { [weak self] image, _ in guard let self else { return } guard let image = image?.compress(type: .avatar) else { return } profilePhoto.uploadImage(image: image) } } container.addSubview(profilePhoto) profilePhoto.snp.makeConstraints { make in make.leading.equalToSuperview() make.top.equalTo(titleLabel.snp.bottom).offset(10) make.bottom.equalToSuperview() make.height.width.equalTo(110) } return container } private func buildPhotoWall() -> UIView { photoWall.delegate = self photoWall.loadImages(myUserInfo.photos) return photoWall } private func buildUserInfo() -> UIView { let container = UIView() let titleLabel = UILabel() titleLabel.font = .heading_h4 titleLabel.textColor = .text_5 titleLabel.text = .init(key: "A00204") container.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview() make.top.equalToSuperview().offset(10) } let stackView = UIStackView() stackView.axis = .vertical stackView.spacing = 22 container.addSubview(stackView) stackView.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview() make.top.equalTo(titleLabel.snp.bottom).offset(10) make.bottom.equalToSuperview() } let nameView = buildInfoLine(title: .init(key: "A00188"), label: nameLabel) nameView.onTap { [weak self] in guard let self else { return } let panel = LNEditNickNamePanel() panel.inputField.text = curName panel.handler = { [weak self] name in guard let self else { return } curName = name checkSaveButton() } panel.popup() } stackView.addArrangedSubview(nameView) let genderView = buildInfoLine(title: .init(key: "A00032"), label: genderLabel) genderView.onTap { [weak self] in guard let self else { return } let panel = LNEditGenderPanel() panel.curGender = curGender panel.handler = { [weak self] gender in guard let self else { return } curGender = gender checkSaveButton() } panel.popup() } stackView.addArrangedSubview(genderView) let birthdayView = buildInfoLine(title: .init(key: "A00205"), label: birthdayLabel) birthdayView.onTap { [weak self] in guard let self else { return } let panel = LNDatePickerPanel() panel.titleLabel.text = .init(key: "A00007") panel.datePicker.maximumDate = Date().yearAgo(-18) panel.setDefault(curBirthday) panel.handler = { [weak self] date in guard let self else { return } curBirthday = date checkSaveButton() } panel.popup() } stackView.addArrangedSubview(birthdayView) let bioView = buildInfoLine(title: .init(key: "A00184"), label: bioLabel) bioView.onTap { [weak self] in guard let self else { return } let panel = LNEditBioPanel() panel.update(curBio) panel.handler = { [weak self] bio in guard let self else { return } curBio = bio checkSaveButton() } panel.popup() } stackView.addArrangedSubview(bioView) let interestView = buildInfoLine(title: .init(key: "A00206"), label: interestLabel) interestView.onTap { [weak self] in guard let self else { return } let panel = LNEditInterestPanel(selecteds: curInterest.map({ $0.code })) panel.handler = { [weak self] items in guard let self else { return } curInterest = items.map({ LNUserInterestVO(code: $0.code, name: $0.name) }) checkSaveButton() } panel.popup() } stackView.addArrangedSubview(interestView) let line = buildLine() container.addSubview(line) line.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview().inset(16) make.top.equalToSuperview() } return container } private func buildSkillInfo() -> UIView { let container = UIView() let titleLabel = UILabel() titleLabel.font = .heading_h4 titleLabel.textColor = .text_5 titleLabel.text = .init(key: "B00004") container.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview() make.top.equalToSuperview().offset(10) } let stackView = UIStackView() stackView.axis = .vertical stackView.spacing = 22 container.addSubview(stackView) stackView.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview() make.top.equalTo(titleLabel.snp.bottom).offset(10) make.bottom.equalToSuperview() } let voiceView = buildInfoLine(title: .init(key: "B00005"), label: voiceLabel) voiceView.onTap { [weak self] in guard let self else { return } let panel = LNEditVoicePanel() panel.popup() } stackView.addArrangedSubview(voiceView) return container } private func buildInfoLine(title: String, label: UILabel) -> UIView { let container = UIView() let titleLabel = UILabel() titleLabel.text = title titleLabel.font = .body_m titleLabel.textColor = .text_4 titleLabel.setContentHuggingPriority(.required, for: .horizontal) titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal) container.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.verticalEdges.equalToSuperview() make.leading.equalToSuperview() } let arrow = UIImageView.arrowImageView(size: 14) arrow.tintColor = .text_4 container.addSubview(arrow) arrow.snp.makeConstraints { make in make.centerY.equalToSuperview() make.trailing.equalToSuperview() } label.font = .body_m label.textColor = .text_3 container.addSubview(label) label.snp.makeConstraints { make in make.centerY.equalToSuperview() make.trailing.equalTo(arrow.snp.leading).offset(-5) make.leading.greaterThanOrEqualTo(titleLabel.snp.trailing).offset(50) } return container } private func buildLine() -> UIView { let line = UIView() line.backgroundColor = .fill_2 line.snp.makeConstraints { make in make.height.equalTo(1) } return line } } #if DEBUG import SwiftUI struct LNEditProfileViewControllerPreview: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> some UIViewController { LNEditProfileViewController() } func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { } } #Preview(body: { LNEditProfileViewControllerPreview() }) #endif