447 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Swift
		
	
	
	
			
		
		
	
	
			447 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Swift
		
	
	
	
| //
 | ||
| //  RoleHomePagerController.swift
 | ||
| //  Crush
 | ||
| //
 | ||
| //  Created by Leon on 2025/7/24.
 | ||
| //
 | ||
| 
 | ||
| import Combine
 | ||
| import UIKit
 | ||
| class RoleHomePagerController: CLViewController<RoleHomePagerView> {
 | ||
|     var navRightButton: EPIconGhostButton!
 | ||
|     var likeView : HeartLikeCountView!
 | ||
|     
 | ||
|     var aiId: Int = 0
 | ||
|     
 | ||
|     @Published var info: AIRoleInfo?
 | ||
|     
 | ||
|     /// 待修改的头像
 | ||
|     var avatarModel: UploadPhotoM?
 | ||
|     
 | ||
|     // 添加请求状态标记
 | ||
|     private var isRequesting = false
 | ||
|     
 | ||
|     private var cancellables = Set<AnyCancellable>()
 | ||
|     override func viewDidLoad() {
 | ||
|         super.viewDidLoad()
 | ||
|         
 | ||
|         setupViews()
 | ||
|         setupDats()
 | ||
|         setupEvents()
 | ||
|     }
 | ||
|     
 | ||
|     override func viewWillAppear(_ animated: Bool) {
 | ||
|         super.viewWillAppear(animated)
 | ||
|         disabledFullScreenPan()
 | ||
|     }
 | ||
|     
 | ||
|     override func viewWillDisappear(_ animated: Bool) {
 | ||
|         super.viewWillDisappear(animated)
 | ||
|         enabledFullScreenPan()
 | ||
|     }
 | ||
|     
 | ||
|     private func setupViews() {
 | ||
|         navigationView.alpha0Title = "User Name"
 | ||
|         navigationView.bgView.alpha = 0
 | ||
|         
 | ||
|         
 | ||
|     }
 | ||
|     
 | ||
|     private func setupDats() {
 | ||
|         loadAIInfo(block: nil)
 | ||
|         container.aboutVc.aiId = aiId
 | ||
|         container.albumVc.aiId = aiId
 | ||
|     }
 | ||
|     
 | ||
|     private func loadAIInfo(block: (() -> Void)?) {
 | ||
|         Hud.showIndicator()
 | ||
|         AIRoleProvider.request(.queryAIBaseInfo(aiId: aiId), modelType: AIRoleInfo.self) { [weak self] result in
 | ||
|             Hud.hideIndicator()
 | ||
|             switch result {
 | ||
|             case let .success(success):
 | ||
|                 self?.info = success
 | ||
|             case let .failure(failure):
 | ||
|                 switch failure {
 | ||
|                 case let .serviceError(code, _):
 | ||
|                     if code == .aiRoleNotExist{
 | ||
|                         let alert = Alert(title: "The Role has been deleted", text: "The role has been deleted and cannot be accessed and interacted.")
 | ||
|                         let action1 = AlertAction(title: "Got it", actionStyle: .confirm) {[weak self] in
 | ||
|                             self?.close()
 | ||
|                         }
 | ||
|                         alert.addAction(action1)
 | ||
|                         alert.show()
 | ||
|                     }
 | ||
|                 default:
 | ||
|                     break
 | ||
|                 }
 | ||
|             }
 | ||
|             block?()
 | ||
|         }
 | ||
|     }
 | ||
|     
 | ||
|     private func setupEvents() {
 | ||
|         NotificationCenter.default.addObserver(self, selector: #selector(notificationAIRoleInfoUpdated(noti:)), name: AppNotificationName.aiRoleInfoChanged.notificationName, object: nil)
 | ||
|         container.createButton.addTarget(self, action: #selector(tapCreateButton), for: .touchUpInside)
 | ||
|         container.headerView.avatarIv.tapAction = {[weak self] in
 | ||
|             self?.tapEditAvatar()
 | ||
|         }
 | ||
|         
 | ||
|         container.headerView.tapChatAction = { aiId in
 | ||
|             AppRouter.goChatVC(aiId: aiId)
 | ||
|         }
 | ||
|         
 | ||
|         container.headerView.tapEditAction = {[weak self] in
 | ||
|             self?.tapEditAI()
 | ||
|         }
 | ||
|         
 | ||
|         $info.sink { [weak self] result in
 | ||
|             self?.navigationView.alpha0Title = result?.nickname ?? ""
 | ||
|             self?.container.config(info: result)
 | ||
|             self?.container.headerView.config(info: result)
 | ||
|             self?.container.aboutVc.config(info: result)
 | ||
|             
 | ||
|             if let user = result{
 | ||
|                 let uid = user.userId
 | ||
|                 self?.setupNaviRightViews(UserCore.shared.user?.userId == uid)
 | ||
|             }
 | ||
|             
 | ||
|             if self?.likeView != nil{
 | ||
|                 self?.likeView.isLike = result?.liked ?? false
 | ||
|             }
 | ||
|         }.store(in: &cancellables)
 | ||
|     }
 | ||
|     
 | ||
|     // MARK: - Helper
 | ||
|     private func setupNaviRightViews(_ isSelf: Bool = false){
 | ||
| //        if navRightButton != nil{
 | ||
| //            return
 | ||
| //        }
 | ||
|         
 | ||
|         navigationView.rightStackH.removeSubviews()
 | ||
|         
 | ||
|         navigationView.paddingRightForRightStack = 16
 | ||
|         
 | ||
|         likeView = {
 | ||
|             let v = HeartLikeCountView(viewSize: .xl)
 | ||
|             v.purIconStyle()
 | ||
|             v.likeButton.addTarget(self, action: #selector(tapLikeButton), for: .touchUpInside)
 | ||
|             navigationView.rightStackH.addArrangedSubview(v)
 | ||
|             return v
 | ||
|         }()
 | ||
|         
 | ||
|         if let user = info {
 | ||
|             likeView.isLike = user.liked ?? false
 | ||
|         }
 | ||
|         
 | ||
|         if isSelf{
 | ||
|             navRightButton = {
 | ||
|                 let v = EPIconGhostButton(radius: .none, iconSize: .medium, iconCode: .more)
 | ||
|                 v.addTarget(self, action: #selector(tapNaviMore(_:)), for: .touchUpInside)
 | ||
|                 navigationView.rightStackH.addArrangedSubview(v)
 | ||
|                 v.snp.makeConstraints { make in
 | ||
|                     make.size.equalTo(v.bgImageSize())
 | ||
|                 }
 | ||
|                 return v
 | ||
|             }()
 | ||
|         }else{
 | ||
|             navRightButton = {
 | ||
|                 let v = EPIconGhostButton(radius: .none, iconSize: .medium, iconCode: .shareBorder)
 | ||
|                 v.addTarget(self, action: #selector(tapShare), for: .touchUpInside)
 | ||
|                 navigationView.rightStackH.addArrangedSubview(v)
 | ||
|                 v.snp.makeConstraints { make in
 | ||
|                     make.size.equalTo(v.bgImageSize())
 | ||
|                 }
 | ||
|                 return v
 | ||
|             }()
 | ||
|         }
 | ||
|     }
 | ||
|     
 | ||
|     // MARK: - Action
 | ||
|     
 | ||
|     @objc private func tapNaviMore(_ sender: UIButton) {
 | ||
| //        let sheet = Sheet()
 | ||
| //        let share = SheetAction(title: "Share", autoDismiss: true) { [weak self] in
 | ||
| //            self?.tapShare()
 | ||
| //        }
 | ||
| //        let delete = SheetAction(title: "Delete", autoDismiss: true) { [weak self] in
 | ||
| //            self?.alertDeleteRole()
 | ||
| //        }
 | ||
| //        sheet.addAction(share)
 | ||
| //        sheet.addAction(delete)
 | ||
| //        sheet.show()
 | ||
|         
 | ||
|         let pop = CLPopoverListView()
 | ||
|         let rect = view.convert(sender.frame, from: sender.superview)
 | ||
|         var items = [CLPopoverListTextItem]()
 | ||
|         do {
 | ||
|             let listItem = CLPopoverListTextItem()
 | ||
|             listItem.title = "Share"
 | ||
|             listItem.image = MWIconFont.image(fromIcon: .shareBorder, size: CGSize(width: 20, height: 20), color: .text)
 | ||
|             listItem.updateLayout()
 | ||
|             listItem.selectedHandler = {[weak self] _ in
 | ||
|                 self?.tapShare()
 | ||
|             }
 | ||
|             items.append(listItem)
 | ||
|         }
 | ||
|         do {
 | ||
|             let listItem = CLPopoverListTextItem()
 | ||
|             listItem.title = "Delete"
 | ||
|             listItem.image = MWIconFont.image(fromIcon: .iconDelete, size: CGSize(width: 20, height: 20), color: .text)
 | ||
|             listItem.updateLayout()
 | ||
|             listItem.selectedHandler = {[weak self] _ in
 | ||
|                 self?.alertDeleteRole()
 | ||
|             }
 | ||
|             items.append(listItem)
 | ||
|         }
 | ||
| 
 | ||
|         pop.setupCommonPopover(rect, inView: view, items: items, block: nil)
 | ||
|     }
 | ||
|     
 | ||
|     @objc private func tapShare(){
 | ||
|         guard let aiId = info?.aiId else{return}
 | ||
|         let content = "Come to Crushlevel for chat, Crush, and AI - chat."
 | ||
|         let urlString = "\(AppConst.h5urlRoot)/@\(aiId)"
 | ||
|         
 | ||
|         guard let url = URL(string: urlString) else { return }
 | ||
|         
 | ||
|         // 分享内容(文字 + 链接)
 | ||
|         let items: [Any] = [url, content]
 | ||
|         
 | ||
|         let activityVC = UIActivityViewController(activityItems: items,
 | ||
|                                                   applicationActivities: nil)
 | ||
|         
 | ||
|         // iPad 需要设置弹出位置,否则会崩溃
 | ||
|         if let popover = activityVC.popoverPresentationController {
 | ||
|             popover.sourceView = view
 | ||
|             popover.sourceRect = CGRect(x: view.bounds.midX,
 | ||
|                                         y: view.bounds.midY,
 | ||
|                                         width: 0, height: 0)
 | ||
|             popover.permittedArrowDirections = []
 | ||
|         }
 | ||
|         
 | ||
|         present(activityVC, animated: true, completion: nil)
 | ||
|     }
 | ||
|     
 | ||
|     @objc private func tapLikeButton(){
 | ||
|         guard UserCore.shared.checkUserLoginIfNotPushUserToLogin() else{return}
 | ||
|         
 | ||
|         // 防止重复点击
 | ||
|         guard !isRequesting else { return }
 | ||
|         guard let user = info, let aiId = user.aiId else { return }
 | ||
|         
 | ||
|         let isliked = user.liked.boolValue
 | ||
|         let likedCount = user.likedNum ?? 0
 | ||
|         
 | ||
|         isRequesting = true
 | ||
|         
 | ||
|         // 禁用按钮
 | ||
|         likeView.likeButton.isEnabled = false
 | ||
|         
 | ||
|         if isliked {
 | ||
|             // 取消点赞
 | ||
|             let likeNewStatus = LikeOrCancelStatus.cancel
 | ||
|             let newLikedCount = max(likedCount - 1, 0)
 | ||
|             
 | ||
|             // 立即更新UI状态
 | ||
|             info?.liked = false
 | ||
|             info?.likedNum = newLikedCount
 | ||
|             likeView.isLike = false
 | ||
|             likeView.countLabel.text = String.displayNumber(NSNumber(value: newLikedCount), scale: 1)
 | ||
|             
 | ||
|             AIRoleProvider.request(.aiUserLikeOrCancel(aiId: aiId, likedStatus: likeNewStatus), modelType: EmptyModel.self) { [weak self] result in
 | ||
|                 self?.handleLikeRequestResult(result: result, aiId: aiId, isLike: false, originalLikedCount: likedCount)
 | ||
|             }
 | ||
|         } else {
 | ||
|             // 点赞
 | ||
|             let likeNewStatus = LikeOrCancelStatus.liked
 | ||
|             let newLikedCount = likedCount + 1
 | ||
|             
 | ||
|             // 播放点赞动画
 | ||
|             likeView.playLotteLike { [weak self] completed in
 | ||
|                 // 立即更新UI状态
 | ||
|                 self?.info?.liked = true
 | ||
|                 self?.info?.likedNum = newLikedCount
 | ||
|                 self?.likeView.isLike = true
 | ||
|                 self?.likeView.countLabel.text = String.displayNumber(NSNumber(value: newLikedCount), scale: 1)
 | ||
|             }
 | ||
|             
 | ||
|             AIRoleProvider.request(.aiUserLikeOrCancel(aiId: aiId, likedStatus: likeNewStatus), modelType: EmptyModel.self) { [weak self] result in
 | ||
|                 self?.handleLikeRequestResult(result: result, aiId: aiId, isLike: true, originalLikedCount: likedCount)
 | ||
|             }
 | ||
|         }
 | ||
|     }
 | ||
|     
 | ||
|     // 统一处理请求结果
 | ||
|     private func handleLikeRequestResult(result: Result<EmptyModel?, ResponseError>, aiId: Int, isLike: Bool, originalLikedCount: Int) {
 | ||
|         isRequesting = false
 | ||
|         likeView.likeButton.isEnabled = true
 | ||
|         
 | ||
|         switch result {
 | ||
|         case .success:
 | ||
|             // 请求成功,无需额外处理
 | ||
|             break
 | ||
|         case .failure:
 | ||
|             // 请求失败,恢复状态
 | ||
|             if let currentAiId = self.info?.aiId, currentAiId == aiId {
 | ||
|                 if isLike {
 | ||
|                     // 点赞失败,恢复为未点赞状态
 | ||
|                     info?.liked = false
 | ||
|                     info?.likedNum = originalLikedCount
 | ||
|                     likeView.isLike = false
 | ||
|                     likeView.countLabel.text = String.displayNumber(NSNumber(value: originalLikedCount), scale: 1)
 | ||
|                 } else {
 | ||
|                     // 取消点赞失败,恢复为点赞状态
 | ||
|                     info?.liked = true
 | ||
|                     info?.likedNum = originalLikedCount
 | ||
|                     likeView.isLike = true
 | ||
|                     likeView.countLabel.text = String.displayNumber(NSNumber(value: originalLikedCount), scale: 1)
 | ||
|                 }
 | ||
|             }
 | ||
|         }
 | ||
|         // 刷新统计
 | ||
|         container.aboutVc.loadStatistics()
 | ||
|     }
 | ||
|     
 | ||
|     private func alertDeleteRole() {
 | ||
|         let alert = Alert(title: "Delete Role", text: "删除角色不可恢复。为保障用户体验,角色删除后,已经与该角色发生过聊天的或者付费行为的用户,还可以正常与该角色互动。")
 | ||
|         let delete = AlertAction(title: "Delete", actionStyle: .destructive) { [weak self] in
 | ||
|             self?.doDeleteRole()
 | ||
|         }
 | ||
|         let cancel = AlertAction(title: "Cancel", actionStyle: .cancel)
 | ||
|         alert.addAction(delete)
 | ||
|         alert.addAction(cancel)
 | ||
|         alert.show()
 | ||
|     }
 | ||
|     
 | ||
|     @objc private func tapCreateButton() {
 | ||
| //        let sheet = Sheet()
 | ||
| //        sheet.title = "Creation Option"
 | ||
| //        let option1 = SheetAction(title: "Static Image", autoDismiss: true) {[weak self] in
 | ||
| //            self?.goGenerateMoreAlbums()
 | ||
| //        }
 | ||
| //        let option2 = SheetAction(title: "Dynamic Image", autoDismiss: true) {
 | ||
| //            Hud.comingsoon()
 | ||
| //        }
 | ||
| //        sheet.addAction(option1)
 | ||
| //        sheet.addAction(option2)
 | ||
| //        sheet.show()
 | ||
|         
 | ||
|         goGenerateMoreAlbums()
 | ||
|     }
 | ||
|     
 | ||
|     @objc private func tapEditAI(){
 | ||
|         // Convert AIRoleInfo to
 | ||
|         AppRouter.goCreateEditAIRole(aiId: aiId)
 | ||
|     }
 | ||
|     
 | ||
|     @objc private func tapEditAvatar(){
 | ||
|         guard UserCore.shared.isSelf(id: info?.userId) else{
 | ||
|             return
 | ||
|         }
 | ||
|         
 | ||
|         guard let imgUrl = info?.homeImageUrl else {return}
 | ||
|         
 | ||
|         ImageDownloader.downloadImage(from: imgUrl) {[weak self] img in
 | ||
|             guard let image = img else{return}
 | ||
|             self?.cropAIAvatar(image: image)
 | ||
|         }
 | ||
| 
 | ||
|     }
 | ||
|     
 | ||
| 
 | ||
|     // MARK: - Functions
 | ||
|     
 | ||
|     private func goGenerateMoreAlbums(){
 | ||
|         guard let aiId = info?.aiId, info != nil else {return}
 | ||
|         
 | ||
|         let vc = RolePhotoGenerateController(type: .album)
 | ||
|         vc.aiId = aiId
 | ||
|         vc.introduction = info?.introduction
 | ||
|         vc.sex = info?.sex
 | ||
|         vc.birthday = info?.birthday
 | ||
|         presentNaviRootVc(vc: vc)
 | ||
|     }
 | ||
| 
 | ||
|     private func doDeleteRole() {
 | ||
|         guard let aiInfo = info else { return }
 | ||
| 
 | ||
|         Hud.showIndicator()
 | ||
|         AIRoleProvider.request(.aiDel(id: aiInfo.aiId!)) { [weak self] result in
 | ||
|             Hud.hideIndicator()
 | ||
|             switch result {
 | ||
|             case .success:
 | ||
|                 self?.alertDeleteOK()
 | ||
|                 NotificationCenter.post(name: .aiRoleCreatedOrDelete)
 | ||
|                 UserCore.shared.refreshUserInfo(block: nil)
 | ||
|             case let .failure(failure):
 | ||
|                 dlog(failure)
 | ||
|             }
 | ||
|         }
 | ||
|     }
 | ||
| 
 | ||
|     private func alertDeleteOK() {
 | ||
|         let alert = Alert(title: "The Role has been deleted", text: "The role has been deleted and cannot be accessed and interacted")
 | ||
|         let gotit = AlertAction(title: "OK", actionStyle: .confirm) { [weak self] in
 | ||
|             self?.navigationController?.popViewController(animated: true)
 | ||
|         }
 | ||
|         alert.addAction(gotit)
 | ||
|         alert.show()
 | ||
|     }
 | ||
| 
 | ||
|     @objc private func notificationAIRoleInfoUpdated(noti: Notification) {
 | ||
|         loadAIInfo(block: nil)
 | ||
|     }
 | ||
|     
 | ||
|     private func cropAIAvatar(image:UIImage?){
 | ||
|         guard let avatar = image else {return}
 | ||
|         
 | ||
|         let image = avatar
 | ||
|         
 | ||
| //        let image = UIImage(named: "egpic")!
 | ||
|         let cropViewController = CropViewController(image: image) { [unowned self] croppedImage in
 | ||
|             self.container.headerView.avatarIv.bindImage(img: croppedImage)
 | ||
|             
 | ||
|             // 裁剪后的图片
 | ||
|             let photo = UploadPhotoM()
 | ||
|             photo.image = croppedImage
 | ||
|             photo.addThisItemTimeStamp = Date().timeStamp
 | ||
|             avatarModel = photo
 | ||
|             
 | ||
|             Hud.showIndicator()
 | ||
|             CloudStorage.shared.s3BatchAddPhotos([photo], bucket: .ROLE) {[weak self] result in
 | ||
|                 
 | ||
|                 if result{
 | ||
|                     //self?.doPublishRole()
 | ||
|                     self?.doUpdateRoleAvatar()
 | ||
|                 }else{
 | ||
|                     Hud.hideIndicator()
 | ||
|                 }
 | ||
|             }
 | ||
|         }
 | ||
|         let navc = CLNavigationController(rootViewController: cropViewController)
 | ||
|         UIWindow.getTopViewController()?.present(navc, animated: true, completion: nil)
 | ||
|         
 | ||
|     }
 | ||
|     
 | ||
|     private func doUpdateRoleAvatar(){
 | ||
|         guard let photo = avatarModel, let remoteFullPath = photo.remoteFullPath else{
 | ||
|             Hud.hideIndicator()
 | ||
|             return
 | ||
|         }
 | ||
|         
 | ||
|         AIRoleProvider.request(.modifyAIAvatar(aiId: aiId, userHead: remoteFullPath), modelType: EmptyModel.self) {[weak self] result in
 | ||
|             Hud.hideIndicator()
 | ||
|             switch result {
 | ||
|             case .success:
 | ||
|                 self?.info?.headImg = remoteFullPath
 | ||
|                 self?.container.headerView.config(info: self?.info)
 | ||
|                 self?.avatarModel = nil
 | ||
|             case .failure:
 | ||
|                 break
 | ||
|             }
 | ||
|         }
 | ||
|         
 | ||
|         
 | ||
|     }
 | ||
| }
 |