523 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Swift
		
	
	
	
		
		
			
		
	
	
			523 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Swift
		
	
	
	
|  | // | |||
|  | //  RoleHomeAlbumController.swift | |||
|  | //  Crush | |||
|  | // | |||
|  | //  Created by Leon on 2025/7/24. | |||
|  | // | |||
|  | 
 | |||
|  | import JXPagingView | |||
|  | import SnapKit | |||
|  | import UIKit | |||
|  | import Combine | |||
|  | class RoleHomeAlbumController: CLBaseGridController { | |||
|  |      | |||
|  |     var aiId: Int? | |||
|  |     var userId: Int? | |||
|  |      | |||
|  |     private var cancellables = Set<AnyCancellable>() | |||
|  |      | |||
|  |     override func viewDidLoad() { | |||
|  |         super.viewDidLoad() | |||
|  | 
 | |||
|  |         // Do any additional setup after loading the view. | |||
|  |         setupViews() | |||
|  |         setupDats() | |||
|  |         setupEvents() | |||
|  |     } | |||
|  | 
 | |||
|  |     override func viewWillAppear(_ animated: Bool) { | |||
|  |         super.viewWillAppear(animated) | |||
|  |         layout.invalidateLayout() | |||
|  |     } | |||
|  | 
 | |||
|  |     private func setupViews() { | |||
|  |         collectionView.snp.remakeConstraints { make in | |||
|  |             make.edges.equalToSuperview() | |||
|  |         } | |||
|  | 
 | |||
|  |         navigationView.isHidden = true | |||
|  |         collectionView.register(RoleHomeAlbumGridCell.self, forCellWithReuseIdentifier: "RoleHomeAlbumGridCell") | |||
|  |         let itemW = floor((UIScreen.width - 24 * 2 - 16) * 0.5) as CGFloat | |||
|  |         let itemH = itemW | |||
|  |         // layout = UICollectionViewFlowLayout() | |||
|  |         layout.itemSize = CGSize(width: itemW, height: itemH) | |||
|  |         layout.minimumLineSpacing = 16 | |||
|  |         layout.minimumInteritemSpacing = 16 | |||
|  |         layout.sectionInset = UIEdgeInsets(top: 12, left: 24, bottom: 12 + UIWindow.safeAreaBottom, right: 24) | |||
|  |         layout.invalidateLayout() | |||
|  |          | |||
|  |         addRefreshHeader() | |||
|  |         addRefreshFooter() | |||
|  |          | |||
|  |         collectionView.mj_header?.beginRefreshing() | |||
|  |          | |||
|  |     } | |||
|  | 
 | |||
|  |     private func setupDats() { | |||
|  |          | |||
|  |     } | |||
|  |      | |||
|  |     override func loadData() { | |||
|  |         guard let id = aiId else {return } | |||
|  |         let pageData = RequestPageData() | |||
|  |         pageData.pn = page | |||
|  |          | |||
|  |         let pageParams = pageData.toNonNilDictionary() | |||
|  |         var params = [String: Any]() | |||
|  |         params.updateValue(id, forKey: "aiId") | |||
|  |         params.updateValue(pageParams, forKey: "page") | |||
|  |          | |||
|  |          | |||
|  |         AIRoleProvider.request(.aiRoleAlbumList(params: params), modelType: ResponseContentPageData<AlbumPhotoItem>.self) {[weak self] result in | |||
|  |             self?.collectionView.mj_header?.endRefreshing() | |||
|  |             self?.collectionView.mj_footer?.endRefreshing() | |||
|  |             switch result { | |||
|  |             case .success(let success): | |||
|  |                 if let albums = success?.datas{ | |||
|  |                     if(pageData.pn == 1){ | |||
|  |                         self?.datas = albums | |||
|  |                         self?.collectionView.mj_footer?.resetNoMoreData() | |||
|  |                         self?.view.setupEmpty(empty: albums.count <= 0, msg: "暂无图片") | |||
|  |                     }else{ | |||
|  |                         self?.datas.append(contentsOf: albums) | |||
|  |                         if albums.count <= 0{ | |||
|  |                             self?.collectionView.mj_footer?.endRefreshingWithNoMoreData() | |||
|  |                         } | |||
|  |                     } | |||
|  |                     self?.collectionView.reloadData() | |||
|  |                 } | |||
|  |             case .failure: | |||
|  |                 break | |||
|  |             } | |||
|  |         } | |||
|  |          | |||
|  |     } | |||
|  | 
 | |||
|  |     private func setupEvents() { | |||
|  |         NotificationCenter.default.addObserver(self, selector: #selector(notiAlbumsInfoChanged), name: AppNotificationName.aiRoleAlbumPhotoInfoChanged.notificationName, object: nil) | |||
|  |         NotificationCenter.default.addObserver(self, selector: #selector(notiAlbumsAddOrDeleted), name: AppNotificationName.aiRoleAlbumAddOrDelete.notificationName, object: nil) | |||
|  |          | |||
|  |         PhotosViewModel.shared.$album.sink {[weak self] photo in | |||
|  |             guard let self = self else{return} | |||
|  |             guard let album = photo , let albumId = photo?.albumId else {return} | |||
|  |              | |||
|  |             for per in self.datas{ | |||
|  |                 guard let perAlbum = per as? AlbumPhotoItem else {return} | |||
|  |                 if albumId == perAlbum.albumId{ | |||
|  |                     perAlbum.updateFrom(album) | |||
|  |                     self.collectionView.reloadData() | |||
|  |                     break | |||
|  |                 } | |||
|  |             } | |||
|  |         }.store(in: &cancellables) | |||
|  |     } | |||
|  | 
 | |||
|  |     override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { | |||
|  |         return datas.count | |||
|  |     } | |||
|  | 
 | |||
|  |     override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { | |||
|  |         let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RoleHomeAlbumGridCell", for: indexPath) as! RoleHomeAlbumGridCell | |||
|  |         let data = datas[indexPath.item] | |||
|  |         cell.isSelf = UserCore.shared.isSelfByUid(uid: userId) | |||
|  |         cell.config(data: data as? AlbumPhotoItem) | |||
|  |         return cell | |||
|  |     } | |||
|  | 
 | |||
|  |     override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { | |||
|  |         guard let album = datas[indexPath.item] as? AlbumPhotoItem else {return} | |||
|  |          | |||
|  | //        guard let uid = userId else{return} | |||
|  |          | |||
|  | //        if album.lockStatus == .locked && !UserCore.shared.isSelf(id: userId){ | |||
|  | //            return | |||
|  | //        } | |||
|  | 
 | |||
|  |         var photoModels = [PhotoBrowserModel]() | |||
|  |          | |||
|  |         for (index, per) in datas.enumerated() { | |||
|  |             guard let perAlbum = per as? AlbumPhotoItem else{ | |||
|  |                 return | |||
|  |             } | |||
|  |             let model = PhotoBrowserModel() | |||
|  |             model.aiAlbum = perAlbum | |||
|  |             model.aiId = aiId | |||
|  |             model.imageUrl = perAlbum.getImgUrl() | |||
|  |             if index == indexPath.item{ | |||
|  |                 if let cell = collectionView.cellForItem(at: indexPath) as? RoleHomeAlbumGridCell{ | |||
|  |                     model.image = cell.imageView.image | |||
|  |                     model.sourceRect = cell.screenRect ?? .zero | |||
|  |                 } | |||
|  |             } | |||
|  |             model.deleteTapBlock = { [weak self] model, completeBlock in | |||
|  |                 // req api to delete photo | |||
|  | //                #warning("test") | |||
|  | //                Hud.showIndicator() | |||
|  | //                DispatchQueue.main.asyncAfter(deadline: .now() + 1) { | |||
|  | //                    Hud.hideInidcator() | |||
|  | //                    completeBlock(true) | |||
|  | //                } | |||
|  |                  | |||
|  |                 Hud.showIndicator() | |||
|  |                 AIRoleProvider.request(.aiRoleAlbumDel(userId: self?.userId, albumId: model.aiAlbum.albumId), modelType: EmptyModel.self) { result in | |||
|  |                     Hud.hideIndicator() | |||
|  |                     switch result { | |||
|  |                     case .success: | |||
|  |                         completeBlock(true) | |||
|  |                         NotificationCenter.post(name: .aiRoleAlbumAddOrDelete) | |||
|  |                     case .failure: | |||
|  |                         completeBlock(false) | |||
|  |                     } | |||
|  |                 } | |||
|  | 
 | |||
|  |             } | |||
|  |             photoModels.append(model) | |||
|  |         } | |||
|  |          | |||
|  |         if UserCore.shared.isSelf(id: userId){ | |||
|  |             ImageBrowser.show(models: photoModels, index: indexPath.item, type: .roleMine) | |||
|  |         }else{ | |||
|  |             ImageBrowser.show(models: photoModels, index: indexPath.item, type: .roleOthersInAlbum) | |||
|  |         } | |||
|  |          | |||
|  |     } | |||
|  |      | |||
|  |      | |||
|  |     @objc private func notiAlbumsInfoChanged(){ | |||
|  |         collectionView.mj_header?.beginRefreshing() | |||
|  |     } | |||
|  |      | |||
|  |     @objc private func notiAlbumsAddOrDeleted(){ | |||
|  |         collectionView.mj_header?.beginRefreshing() | |||
|  |     } | |||
|  | } | |||
|  | 
 | |||
|  | extension RoleHomeAlbumController: JXPagingViewListViewDelegate { | |||
|  |     func listView() -> UIView { | |||
|  |         return view | |||
|  |     } | |||
|  | 
 | |||
|  |     func listScrollView() -> UIScrollView { | |||
|  |         return collectionView | |||
|  |     } | |||
|  | 
 | |||
|  |     func listViewDidScrollCallback(callback: @escaping (UIScrollView) -> Void) { | |||
|  |         listViewDidScrollCallback = callback | |||
|  |     } | |||
|  | } | |||
|  | 
 | |||
|  | class RoleHomeAlbumGridCell: UICollectionViewCell { | |||
|  |     var imageView: AutoRatioImageView! | |||
|  | 
 | |||
|  |     /// 🚩正常图片 | |||
|  |     var normalContainer: UIView! | |||
|  |     /// 左上、默认flag | |||
|  |     var flagOfDefault: EPTagLabel! | |||
|  |     /// 右上角,黑色标识是否带锁(对于自己)。 | |||
|  |     var lockFlag: EPIconFlagView! | |||
|  |     var unlockedFlag: EPIconPrimaryButton! // 🔓 + 主题背景色 | |||
|  |     /// 左下 | |||
|  |     var likeView: HeartLikeCountView! | |||
|  | 
 | |||
|  |     /// 🚩LockContainer | |||
|  |     var lockContainer: UIView! | |||
|  |     var unlockCoinLabel: UILabel! | |||
|  | 
 | |||
|  |     // Flag | |||
|  |     public var usedInMeetCard : Bool = false | |||
|  |     // @Required | |||
|  |     public var data: AlbumPhotoItem? | |||
|  |     // @Required | |||
|  |     public var isSelf: Bool = false | |||
|  |      | |||
|  |     // 添加请求状态标记 | |||
|  |     private var isRequesting = false | |||
|  |      | |||
|  |     override init(frame: CGRect) { | |||
|  |         super.init(frame: frame) | |||
|  |         setupViews() | |||
|  |         setupData() | |||
|  |         setupEvent() | |||
|  |     } | |||
|  | 
 | |||
|  |     required init?(coder: NSCoder) { | |||
|  |         fatalError("init(coder:) has not been implemented") | |||
|  |     } | |||
|  |      | |||
|  |     // | |||
|  |     func config(data:AlbumPhotoItem?){ | |||
|  |         self.data = data; | |||
|  |         guard let photo = data else {return} | |||
|  |          | |||
|  |        | |||
|  |         //imageView.loadImage(data?.getImgUrl()) | |||
|  |         imageView.setImage(with: data?.getImgUrl().urlValue) | |||
|  |          | |||
|  |         likeView.isLike = photo.likedStatus == .liked | |||
|  |         let likedCount = photo.likedCount ?? 0 | |||
|  |         let likeCountDisplay = String.displayNumber(NSNumber(value: likedCount), scale: 1) | |||
|  |         likeView.countLabel.text = likeCountDisplay// String.displayInteger(photo.likedCount ?? 0) | |||
|  |          | |||
|  |         if isSelf{ | |||
|  |             lockContainer.isHidden = true | |||
|  |             normalContainer.isHidden = false | |||
|  |              | |||
|  |             unlockedFlag.isHidden = true | |||
|  |             lockFlag.isHidden = true | |||
|  |             if let lockStatus = photo.lockStatus{ | |||
|  |                 lockFlag.isHidden = lockStatus != .unlock | |||
|  |             } | |||
|  |             flagOfDefault.isHidden = !photo.isDefault.boolValue | |||
|  |         }else{ | |||
|  |             flagOfDefault.isHidden = true | |||
|  |             lockFlag.isHidden = true | |||
|  |             unlockedFlag.isHidden = true | |||
|  |              | |||
|  |             if let lockStatus = photo.lockStatus{ | |||
|  |                 if lockStatus == .unlock{ | |||
|  |                     // 解锁了的 | |||
|  |                     lockContainer.isHidden = true | |||
|  |                     normalContainer.isHidden = false | |||
|  |                     unlockedFlag.isHidden = false | |||
|  |                     flagOfDefault.isHidden = !photo.isDefault.boolValue | |||
|  |                      | |||
|  |                 }else if lockStatus == .locked{ | |||
|  |                     // 他人带锁 | |||
|  |                     lockContainer.isHidden = false | |||
|  |                     normalContainer.isHidden = true | |||
|  |                     let coin = Coin(cents: (data?.unlockPrice ?? 0)) | |||
|  |                     unlockCoinLabel.text = "\(coin.formatted) unlock" | |||
|  |                 } | |||
|  |             }else{ | |||
|  |                 // 未上锁Open | |||
|  |                 lockContainer.isHidden = true | |||
|  |                 normalContainer.isHidden = false | |||
|  |                 flagOfDefault.isHidden = !photo.isDefault.boolValue | |||
|  |             } | |||
|  |              | |||
|  |             if usedInMeetCard{ | |||
|  |                 flagOfDefault.isHidden = true | |||
|  |             } | |||
|  |              | |||
|  |         } | |||
|  |     } | |||
|  | 
 | |||
|  |     private func setupViews() { | |||
|  |         contentView.backgroundColor = .c.csnn | |||
|  |         contentView.layer.cornerRadius = 16 | |||
|  |         contentView.layer.masksToBounds = true | |||
|  | 
 | |||
|  |         imageView = { | |||
|  |             let v = AutoRatioImageView() | |||
|  |             v.layer.cornerRadius = 16 | |||
|  |             v.layer.masksToBounds = true | |||
|  |             contentView.addSubview(v) | |||
|  |             v.snp.makeConstraints { make in | |||
|  |                 make.top.leading.trailing.equalToSuperview() | |||
|  |             } | |||
|  |             return v | |||
|  |         }() | |||
|  | 
 | |||
|  |         normalContainer = { | |||
|  |             let v = UIView() | |||
|  |             contentView.addSubview(v) | |||
|  |             v.snp.makeConstraints { make in | |||
|  |                 make.edges.equalToSuperview() | |||
|  |             } | |||
|  |             return v | |||
|  |         }() | |||
|  | 
 | |||
|  |         flagOfDefault = { | |||
|  |             let v = EPTagLabel(style: .blackOnColor) | |||
|  |             normalContainer.addSubview(v) | |||
|  |             v.snp.makeConstraints { make in | |||
|  |                 make.leading.equalToSuperview().offset(8) | |||
|  |                 make.top.equalToSuperview().offset(8) | |||
|  |             } | |||
|  |             return v | |||
|  |         }() | |||
|  | 
 | |||
|  |         lockFlag = { | |||
|  |             let v = EPIconFlagView(frame: .zero) | |||
|  |             v.setupRightTopLockStyle() | |||
|  |             normalContainer.addSubview(v) | |||
|  |             v.snp.makeConstraints { make in | |||
|  |                 make.top.equalToSuperview().offset(8) | |||
|  |                 make.trailing.equalToSuperview().offset(-8) | |||
|  |             } | |||
|  |             v.isHidden = true | |||
|  |             return v | |||
|  |         }() | |||
|  |          | |||
|  |         unlockedFlag = { | |||
|  |             let v = EPIconPrimaryButton(radius: .rectangle, iconSize: .xs, iconCode: .iconPublic) | |||
|  |             v.layer.cornerRadius = 4 | |||
|  |             normalContainer.addSubview(v) | |||
|  |             v.snp.makeConstraints { make in | |||
|  |                 make.top.equalToSuperview().offset(8) | |||
|  |                 make.trailing.equalToSuperview().offset(-8) | |||
|  |             } | |||
|  |             v.isHidden = true | |||
|  |             return v | |||
|  |         }() | |||
|  | 
 | |||
|  |         likeView = { | |||
|  |             let v = HeartLikeCountView() | |||
|  |             normalContainer.addSubview(v) | |||
|  |             v.snp.makeConstraints { make in | |||
|  |                 make.leading.equalToSuperview().offset(8) | |||
|  |                 make.bottom.equalToSuperview().offset(-8) | |||
|  |             } | |||
|  |             return v | |||
|  |         }() | |||
|  | 
 | |||
|  |         setupLockStateViews() | |||
|  | 
 | |||
|  |         flagOfDefault.text = "Default" | |||
|  |         likeView.countLabel.text = "0" | |||
|  | 
 | |||
|  |          | |||
|  | 
 | |||
|  | //        #warning("test") | |||
|  | //        testData() | |||
|  |     } | |||
|  | 
 | |||
|  |     private func setupLockStateViews() { | |||
|  |         lockContainer = { | |||
|  |             let v = UIView() | |||
|  |             contentView.addSubview(v) | |||
|  |             v.snp.makeConstraints { make in | |||
|  |                 make.edges.equalToSuperview() | |||
|  |             } | |||
|  |             return v | |||
|  |         }() | |||
|  | 
 | |||
|  |         let stackV = { | |||
|  |             let v = UIStackView() | |||
|  |             v.axis = .vertical | |||
|  |             v.alignment = .center | |||
|  |             v.spacing = 16 | |||
|  |             lockContainer.addSubview(v) | |||
|  |             v.snp.makeConstraints { make in | |||
|  |                 make.center.equalToSuperview() | |||
|  |                 make.left.greaterThanOrEqualToSuperview() | |||
|  |                 make.right.lessThanOrEqualToSuperview() | |||
|  |             } | |||
|  |             return v | |||
|  |         }() | |||
|  | 
 | |||
|  |         let lockIcon = { | |||
|  |             let v = UIImageView() | |||
|  |             v.image = MWIconFont.image(fromIcon: .iconPrivate, size: CGSize(width: 24, height: 24), color: .white) | |||
|  |             stackV.addArrangedSubview(v) | |||
|  |             return v | |||
|  |         }() | |||
|  |         lockIcon.isHidden = false | |||
|  | 
 | |||
|  |         let stackH = { | |||
|  |             let v = UIStackView() | |||
|  |             v.spacing = 4 | |||
|  |             v.alignment = .center | |||
|  |             stackV.addArrangedSubview(v) | |||
|  |             return v | |||
|  |         }() | |||
|  | 
 | |||
|  |         let coinIv = { | |||
|  |             let v = UIImageView() | |||
|  |             v.image = UIImage(named: "icon_16_diamond") | |||
|  |             stackH.addArrangedSubview(v) | |||
|  |             return v | |||
|  |         }() | |||
|  |         coinIv.isHidden = false | |||
|  | 
 | |||
|  |         unlockCoinLabel = { | |||
|  |             let v = UILabel() | |||
|  |             v.textColor = .text | |||
|  |             v.font = .t.tlm | |||
|  |             stackH.addArrangedSubview(v) | |||
|  |             return v | |||
|  |         }() | |||
|  |     } | |||
|  | 
 | |||
|  |     private func setupData(){ | |||
|  |          | |||
|  |     } | |||
|  |      | |||
|  |     private func setupEvent(){ | |||
|  |         likeView.likeButton.addTarget(self, action: #selector(tapLikeHeart), for: .touchUpInside) | |||
|  |     } | |||
|  |      | |||
|  |     private func testData() { | |||
|  |         imageView.image = UIImage(named: "egpic") | |||
|  |         unlockCoinLabel.text = "123 unlock" | |||
|  | 
 | |||
|  |         lockContainer.backgroundColor = .random | |||
|  |         normalContainer.isHidden = true | |||
|  |         lockContainer.isHidden = false | |||
|  |     } | |||
|  | 
 | |||
|  |     @objc private func tapLikeHeart() { | |||
|  |         guard UserCore.shared.checkUserLoginIfNotPushUserToLogin() else{return} | |||
|  |          | |||
|  |         // 防止重复点击 | |||
|  |         guard !isRequesting else { return } | |||
|  |         guard let album = self.data else { return } | |||
|  |          | |||
|  |         let albumId = album.albumId | |||
|  |         isRequesting = true | |||
|  |          | |||
|  |         // 禁用按钮 | |||
|  |         likeView.likeButton.isEnabled = false | |||
|  |          | |||
|  |         if album.likedStatus == .liked { | |||
|  |             // 取消点赞 | |||
|  |             album.likedStatus = .cancel | |||
|  |             album.likedCount = max((album.likedCount ?? 0) - 1, 0) | |||
|  |             config(data: album) | |||
|  |              | |||
|  |             AIRoleProvider.request(.aiRolePhotoLikeOrNo(albumId: albumId, likedStatus: .cancel), modelType: EmptyModel.self) { [weak self] result in | |||
|  |                 self?.handleLikeRequestResult(result: result, albumId: albumId, isLike: false) | |||
|  |             } | |||
|  |         } else { | |||
|  |             // 点赞 | |||
|  |             likeView.playLotteLike { [weak self] completed in | |||
|  |                 guard let album = self?.data else { return } | |||
|  |                 album.likedStatus = .liked | |||
|  |                 album.likedCount = (album.likedCount ?? 0) + 1 | |||
|  |                 self?.config(data: album) | |||
|  |             } | |||
|  |              | |||
|  |             AIRoleProvider.request(.aiRolePhotoLikeOrNo(albumId: albumId, likedStatus: .liked), modelType: EmptyModel.self) { [weak self] result in | |||
|  |                 self?.handleLikeRequestResult(result: result, albumId: albumId, isLike: true) | |||
|  |             } | |||
|  |         } | |||
|  |     } | |||
|  |      | |||
|  |     // 统一处理请求结果 | |||
|  |     private func handleLikeRequestResult(result: Result<EmptyModel?, ResponseError>, albumId: Int, isLike: Bool) { | |||
|  |         isRequesting = false | |||
|  |         likeView.likeButton.isEnabled = true | |||
|  |          | |||
|  |         switch result { | |||
|  |         case .success: | |||
|  |             // 请求成功,无需额外处理 | |||
|  |             break | |||
|  |         case .failure: | |||
|  |             // 请求失败,恢复状态 | |||
|  |             if let currentId = self.data?.albumId, currentId == albumId { | |||
|  |                 guard let album = self.data else { return } | |||
|  |                 if isLike { | |||
|  |                     // 点赞失败,恢复为未点赞状态 | |||
|  |                     album.likedStatus = .cancel | |||
|  |                     album.likedCount = max((album.likedCount ?? 0) - 1, 0) | |||
|  |                 } else { | |||
|  |                     // 取消点赞失败,恢复为点赞状态 | |||
|  |                     album.likedStatus = .liked | |||
|  |                     album.likedCount = (album.likedCount ?? 0) + 1 | |||
|  |                 } | |||
|  |                 config(data: album) | |||
|  |             } | |||
|  |         } | |||
|  |     } | |||
|  | } |