578 lines
20 KiB
Swift
578 lines
20 KiB
Swift
//
|
||
// 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<BrowseImageZoomView> = []
|
||
private var reusableZoomViews: Set<BrowseImageZoomView> = []
|
||
var imageModels: [PhotoBrowserModel] = []
|
||
|
||
@Published var type: PhotoBrowserType = .normal
|
||
private var cancellables = Set<AnyCancellable>()
|
||
|
||
// 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")
|
||
}
|
||
}
|