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

578 lines
20 KiB
Swift
Raw Normal View History

2025-10-09 10:29:35 +00:00
//
// 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")
}
}