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
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
}
|