// // PhotoBrowserController.swift // Crush // // Created by Leon on 2025/7/26. // import Photos import SnapKit import UIKit import Combine enum PhotoBrowserType { /// 普通查看大图,无其他额外操作 case normal /// 我的AI角色图片 case roleMine /// 他人的AI角色图片 case roleOthersInIm /// 浏览他人相册 & Meet 查看大图 case roleOthersInAlbum /// 聊天背景图选择(AI生图) case chatBackgroundGeneratedSelect /// 设置聊天背景图 case chatBackgroundSet } class PhotoBrowserController: UIViewController, UIScrollViewDelegate, BrowseImageZoomViewDelegate { private let kScrollLeftAndRightSpace: CGFloat = 0 @Published var currentIndex: Int = 0 private var imageCount: Int = 0 private var scrollView: UIScrollView! var titleView: NavigationView! private var titleTitleStackH: UIStackView! var titleLockIcon: EPIconTertiaryButton! // 🔒 var titleunlockedIcon: EPIconPrimaryButton! // 🔓 + 主题背景色 private var countLabel: UILabel? private var deleteButton: UIButton? // Data private var visibleZoomViews: Set = [] private var reusableZoomViews: Set = [] var imageModels: [PhotoBrowserModel] = [] @Published var type: PhotoBrowserType = .normal private var cancellables = Set() // Flag var isRequesting = false // MARK: - Commone通用业务Views var bottomGradientContainer: GradientView! var bottomGradientOperateStackV: UIStackView! // MARK: Role Mine /// 解锁方式:免费或coin解锁 var rolePhotoUnlockEntry: RolePhotoUnlockEntryView! var setDefaultEntry: RolePhotoSetDefaultEntryView! var moreButton: EPIconGhostButton? // MARK: Role See others var roleOthersContainer: SelectiveDeliveryEventsView? var roleOthersCenterLock: UIImageView? var iconLabel: CLIconLabel? var iconUnlockButton: StyleButton? // MARK: 通用的地步操作view /// 居中底部显示一个chipButton var bottomCommonOperateContainer: SelectiveDeliveryEventsView? var operateChipButton : EPChipContrastButton? var likeView: HeartLikeCountView? // MARK: Chatbackground set var setBackgroundDisableButton: StyleButton! override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .black setupScrollView() loadTitleView() // 其他业务通用的一些容器、View setupCommonContainers() setupOprateViews() setupEvent() } private func setupEvent(){ $type.sink {[weak self] type in if type == .roleMine || type == .chatBackgroundSet{ self?.moreButton?.isHidden = false }else{ self?.moreButton?.isHidden = true } }.store(in: &cancellables) $currentIndex.sink {[weak self] index in self?.reloadStates(index: index) }.store(in: &cancellables) WalletCore.shared.$balance.sink {[weak self] balance in if let priceLabel = self?.iconLabel { // let balance = balance.balance ?? 0 // let coin = Coin(cents: balance) priceLabel.contentLabel.text = balance.displayBalance() } }.store(in: &cancellables) } // MARK: - SetupViews private func setupScrollView() { scrollView = UIScrollView() scrollView.isHidden = true view.addSubview(scrollView) view.sendSubviewToBack(scrollView) scrollView.showsVerticalScrollIndicator = false scrollView.showsHorizontalScrollIndicator = false scrollView.contentInsetAdjustmentBehavior = .never scrollView.isPagingEnabled = true scrollView.bounces = true scrollView.backgroundColor = .clear scrollView.delegate = self scrollView.snp.makeConstraints { make in make.edges.equalTo(UIEdgeInsets(top: 0, left: -kScrollLeftAndRightSpace, bottom: 0, right: kScrollLeftAndRightSpace)) make.width.equalTo(UIScreen.main.bounds.width + kScrollLeftAndRightSpace) make.height.equalTo(UIScreen.main.bounds.height) } } private func loadTitleView() { guard titleView == nil else { titleView?.alpha = 1 return } titleView = NavigationView() titleView.bgView.alpha = 0 titleView.setupBackButtonCloseIcon() titleView.clipsToBounds = false view.addSubview(titleView) titleView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() make.height.equalTo(UIWindow.statusBarHeight + 44) } let gradient = CLSystemToken.gradient(token: .cob) let gradientUnderTitle = GradientView(colors: [gradient.secondColor!, gradient.firstColor!], gradientType: .topToBottom) // [UIColor.c.cob.withAlphaComponent(0), UIColor.c.cbd] titleView.insertSubview(gradientUnderTitle, at: 0) gradientUnderTitle.snp.makeConstraints { make in make.leading.trailing.top.equalToSuperview() make.height.equalTo(140) } titleView.tapBackButtonAction = {[weak self] in self?.backButtonAction() } titleTitleStackH = { let v = UIStackView() v.spacing = 8 v.alignment = .center titleView.addSubview(v) v.snp.makeConstraints { make in make.height.equalTo(44) make.centerX.equalToSuperview() make.bottom.equalToSuperview() } return v }() titleLockIcon = { // EPIconTertiaryButton let v = EPIconTertiaryButton(radius: .rectangle, iconSize: .small, iconCode: .iconPrivate) titleTitleStackH.addArrangedSubview(v) v.isHidden = true return v }() titleunlockedIcon = { let v = EPIconPrimaryButton(radius: .rectangle, iconSize: .small, iconCode: .iconPublic) titleTitleStackH.addArrangedSubview(v) v.isHidden = true return v }() countLabel = { let v = UILabel() titleTitleStackH.addArrangedSubview(v) v.textColor = .white v.font = .t.ttm v.textAlignment = .center return v }() } // MARK: - 各项业务对应的view private func setupCommonContainers(){ bottomGradientContainer = { let gradient = CLSystemToken.gradient(token: .cob) let gradientUnderTitle = GradientView(colors: [gradient.firstColor!, gradient.secondColor!], gradientType: .topToBottom) // [UIColor.c.cob.withAlphaComponent(0), UIColor.c.cbd] view.addSubview(gradientUnderTitle) gradientUnderTitle.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview() } return gradientUnderTitle }() bottomGradientOperateStackV = { let v = UIStackView() v.axis = .vertical v.spacing = 16 bottomGradientContainer!.addSubview(v) v.snp.makeConstraints { make in make.top.equalToSuperview().offset(48) make.leading.equalToSuperview().offset(24) make.trailing.equalToSuperview().offset(-24) make.bottom.equalToSuperview().offset(-16 - UIWindow.safeAreaBottom * 0.5) } return v }() bottomGradientContainer?.isHidden = true } private func setupOprateViews() { switch type { case .roleMine: createOperateViewOfRoleMine() case .roleOthersInIm, .roleOthersInAlbum: createOperateViewOfOthers() createBottomCommonOperateView() case .chatBackgroundSet: createChatBackgroundSetViews() case .chatBackgroundGeneratedSelect: createBottomCommonOperateView() setupChatBackgroundSelectView() default: break } } private func createBottomCommonOperateView(){ bottomCommonOperateContainer = { let v = SelectiveDeliveryEventsView() view.insertSubview(v, belowSubview: titleView) v.snp.makeConstraints { make in make.edges.equalToSuperview() } return v }() operateChipButton = { let v = EPChipContrastButton() v.iconCode = .like v.addTarget(self, action: #selector(tapOperateChipButton(_:)), for: .touchUpInside) bottomCommonOperateContainer?.addSubview(v) v.snp.makeConstraints { make in make.centerX.equalToSuperview() make.bottom.equalToSuperview().offset(-16-UIWindow.safeAreaBottom*0.5) } v.isHidden = true return v }() likeView = { let v = HeartLikeCountView(viewSize: .xxl) bottomCommonOperateContainer?.addSubview(v) v.snp.makeConstraints { make in make.centerX.equalToSuperview() make.bottom.equalToSuperview().offset(-16-UIWindow.safeAreaBottom*0.5) } v.likeButton.addTarget(self, action: #selector(tapLikeButton), for: .touchUpInside) v.isHidden = true return v }() bottomCommonOperateContainer?.isHidden = true roleOthersContainer?.isHidden = true } private func setupChatBackgroundSelectView(){ #warning("to do") //operateChipButton.text = "Select" } // MARK: - 看图控件 func setupViews() { visibleZoomViews.forEach { view in view.prepareForReuse() view.removeFromSuperview() reusableZoomViews.insert(view) } visibleZoomViews.removeAll() imageCount = imageModels.count let width = UIScreen.main.bounds.width let height = UIScreen.main.bounds.height scrollView.contentOffset = CGPoint(x: (width + kScrollLeftAndRightSpace) * CGFloat(currentIndex), y: 0) scrollView.contentSize = CGSize(width: (width + kScrollLeftAndRightSpace) * CGFloat(imageCount), height: height) setupZoomView(at: currentIndex) reloadViews() } private func setupZoomView(at index: Int) { guard index < imageModels.count, index >= 0 else { return } let model = imageModels[index] let zoomView = BrowseImageZoomView(imageModel: model) zoomView.delegate = self let width = UIScreen.main.bounds.width let height = UIScreen.main.bounds.height zoomView.frame = CGRect( x: (width + kScrollLeftAndRightSpace) * CGFloat(index) + kScrollLeftAndRightSpace, y: 0, width: width, height: height ) zoomView.displayIndex = index scrollView.addSubview(zoomView) visibleZoomViews.insert(zoomView) } private func reloadZoomView(_ zoomView: BrowseImageZoomView, at index: Int) { let width = UIScreen.main.bounds.width let height = UIScreen.main.bounds.height guard !imageModels.isEmpty else { return } let safeIndex = min(index, imageModels.count - 1) zoomView.frame = CGRect( x: (width + kScrollLeftAndRightSpace) * CGFloat(safeIndex) + kScrollLeftAndRightSpace, y: 0, width: width, height: height ) zoomView.reloadImage(imageModels[safeIndex]) zoomView.delegate = self zoomView.displayIndex = safeIndex scrollView.addSubview(zoomView) visibleZoomViews.insert(zoomView) reusableZoomViews.remove(zoomView) } // MARK: - Public func reloadViews() { guard imageCount > 1 else { return } var hasFrontDisplay = false var hasAfterDisplay = false visibleZoomViews.forEach { zoomView in if zoomView.displayIndex == currentIndex + 1 { hasAfterDisplay = true } if zoomView.displayIndex == currentIndex - 1 { hasFrontDisplay = true } if abs(zoomView.displayIndex - currentIndex) > 1 { zoomView.prepareForReuse() zoomView.removeFromSuperview() reusableZoomViews.insert(zoomView) } } visibleZoomViews.subtract(reusableZoomViews) if currentIndex + 1 < imageCount, !hasAfterDisplay { if let zoomView = reusableZoomViews.first { reloadZoomView(zoomView, at: currentIndex + 1) } else { setupZoomView(at: currentIndex + 1) } } if currentIndex - 1 >= 0, !hasFrontDisplay { if let zoomView = reusableZoomViews.first { reloadZoomView(zoomView, at: currentIndex - 1) } else { setupZoomView(at: currentIndex - 1) } } } func reloadCurrentZoomImage(){ if let zoomView = visibleZoomViews.first { reloadZoomView(zoomView, at: currentIndex) } } @objc func backButtonAction() { let zoomView = visibleZoomViews.first { view in guard currentIndex < imageModels.count else { return false } return view.imageModel == imageModels[currentIndex] } if let zoomView = zoomView { zoomView.alpha = 0 hideAnimation(with: zoomView.imageView, toRect: zoomView.imageModel?.sourceRect ?? .zero) } else { UIView.animate(withDuration: 0.3) { [weak self] in self?.setVCViewBackgroundColor(0) } completion: { _ in PhotoBrowserManager.shared.hidePhotoBrowser() } } } private func showAnimation(_ model: PhotoBrowserModel) { let imageView = UIImageView(frame: model.sourceRect) view.addSubview(imageView) imageView.contentMode = model.imageContentMode imageView.image = model.image ?? model.placeHolder imageView.backgroundColor = .black imageView.clipsToBounds = true if imageView.image == nil { imageView.isHidden = true } var imageWidth = imageView.image?.size.width ?? 1 var imageHeight = imageView.image?.size.height ?? 1 let scale = imageHeight / imageWidth if imageWidth < imageWidth && imageHeight >= UIScreen.main.bounds.height { imageHeight = UIScreen.main.bounds.height imageWidth = imageHeight / scale } else { imageWidth = UIScreen.main.bounds.width imageHeight = imageWidth * scale } let hasSourceRect = model.sourceRect.size.width > 0 if hasSourceRect { UIView.animate(withDuration: 0.3) { [weak self] in guard let self = self else { return } imageView.backgroundColor = .black self.view.backgroundColor = .black imageView.center = CGPoint(x: UIScreen.main.bounds.width * 0.5, y: UIScreen.main.bounds.height * 0.5) imageView.bounds = CGRect(x: 0, y: 0, width: imageWidth, height: imageHeight) } completion: { [weak self] _ in guard let self = self else { return } self.scrollView.isHidden = false self.view.backgroundColor = .black imageView.removeFromSuperview() self.reloadCountLabel(hidden: self.type == .normal) } } else { imageView.backgroundColor = .black view.backgroundColor = .black imageView.center = CGPoint(x: UIScreen.main.bounds.width * 0.5, y: UIScreen.main.bounds.height * 0.5) imageView.bounds = CGRect(x: 0, y: 0, width: imageWidth, height: imageHeight) scrollView.isHidden = false view.backgroundColor = .black imageView.removeFromSuperview() reloadCountLabel(hidden: type == .normal) } } private func hideAnimation(with imageView: UIImageView, toRect rect: CGRect) { let newImageView = UIImageView(frame: PhotoBrowserModel.getViewRectForScreen(with: imageView)) view.addSubview(newImageView) newImageView.contentMode = .scaleAspectFit newImageView.image = imageView.image newImageView.backgroundColor = .black newImageView.clipsToBounds = true UIView.animate(withDuration: 0.3) { [weak self] in self?.setVCViewBackgroundColor(0) newImageView.contentMode = .scaleAspectFill newImageView.backgroundColor = .black newImageView.frame = rect.size.width > 0 ? rect : newImageView.frame newImageView.alpha = rect.size.width > 0 ? 0.6 : 0 } completion: { _ in PhotoBrowserManager.shared.hidePhotoBrowser() newImageView.removeFromSuperview() } } // MARK: - Helper /// ⚠️此方法好像有问题 private func getCurrentZoomView() -> BrowseImageZoomView? { let zoomView = visibleZoomViews.first { view in guard currentIndex < imageModels.count else { return false } return view.imageModel == imageModels[currentIndex] } return zoomView } func getCurrentZoomView(byModel: PhotoBrowserModel) -> BrowseImageZoomView? { let zoomView = visibleZoomViews.first { view in guard currentIndex < imageModels.count else { return false } return view.imageModel == byModel } return zoomView } // MARK: - Functions func reloadCountLabel(hidden: Bool) { if hidden || imageCount <= 1 { countLabel?.alpha = 0 return } countLabel?.alpha = 1 countLabel?.text = "\(currentIndex + 1)/\(imageCount)" view.bringSubviewToFront(countLabel!) } func reloadStates(index: Int){ guard imageModels.count > 0 else{ return } let model = imageModels[index] //let album = model.aiAlbum reloadStatesByModel(model: model) } // MARK: - Public func setupView(with imageModels: [PhotoBrowserModel], currentIndex: Int) { guard !imageModels.isEmpty else { return } self.currentIndex = currentIndex imageCount = imageModels.count self.imageModels = imageModels showAnimation(imageModels[currentIndex]) setupViews() } // MARK: - RoleBrowse about // MARK: - UIScrollViewDelegate func scrollViewDidScroll(_ scrollView: UIScrollView) { let index = Int(scrollView.contentOffset.x / (UIScreen.main.bounds.width + kScrollLeftAndRightSpace)) // dlog("照片index: \(index)") guard index != currentIndex else { return } currentIndex = min(index, imageCount - 1) reloadViews() reloadCountLabel(hidden: false) } // MARK: - BrowseImageZoomViewDelegate func dismisAnimation(_ zoomImageView: UIImageView, toFrame frame: CGRect) { hideAnimation(with: zoomImageView, toRect: frame) } func setVCViewBackgroundColor(_ alpha: CGFloat) { view.backgroundColor = UIColor.black.withAlphaComponent(alpha) titleView?.alpha = alpha bottomGradientContainer?.alpha = alpha deleteButton?.alpha = alpha roleOthersContainer?.alpha = alpha bottomCommonOperateContainer?.alpha = alpha } func singleTapZoomView(_ zoomImageView: UIImageView) -> Bool { return true } @discardableResult func longPressZoomView(_ zoomView: BrowseImageZoomView) -> Bool { return true } deinit { print("♻️EGPhotoBrowserController dealloc") } }