Visual_Novel_iOS/crush/Crush/Src/Components/Photo/PhotoBrowser/PhotoBrowserController.swift

578 lines
20 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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: - CommoneViews
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")
}
}