Files
mobile-ios/Rosetta/Features/Chats/ChatDetail/Gallery/GalleryOverlayView.swift

333 lines
13 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
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.
import UIKit
// MARK: - GalleryOverlayView
/// UIKit overlay for gallery controls top bar, bottom bar, counter badge, caption.
/// Bottom bar: Telegram parity Forward (left) + Draw+Captions (center capsule) + Delete (right).
/// "..." menu: custom popup (Telegram parity) not system UIMenu.
final class GalleryOverlayView: UIView {
var onBack: (() -> Void)?
var onMenu: (() -> Void)?
var onForward: (() -> Void)?
var onDraw: (() -> Void)?
var onCaptions: (() -> Void)?
var onDelete: (() -> Void)?
private(set) var isControlsVisible = true
// Top bar
private let topContainer = UIView()
private let backButton = UIButton(type: .system)
private let menuButton = UIButton(type: .system)
private let titleLabel = UILabel()
private let subtitleLabel = UILabel()
private let titleGlass = TelegramGlassUIView(frame: .zero)
private let backGlass = TelegramGlassUIView(frame: .zero)
private let menuGlass = TelegramGlassUIView(frame: .zero)
// Counter badge
private let counterLabel = UILabel()
private let counterGlass = TelegramGlassUIView(frame: .zero)
private let counterContainer = UIView()
// Bottom bar Telegram parity: Forward (left) + Draw+Captions (center) + Delete (right)
private let bottomContainer = UIView()
private let leftGlass = TelegramGlassUIView(frame: .zero)
private let centerGlass = TelegramGlassUIView(frame: .zero)
private let rightGlass = TelegramGlassUIView(frame: .zero)
private let forwardButton = UIButton(type: .system)
private let drawButton = UIButton(type: .system)
private let captionsButton = UIButton(type: .system)
private let deleteButton = UIButton(type: .system)
private let centerSeparator = UIView()
// Caption (above bottom bar)
private let captionContainer = UIView()
private let captionLabel = UILabel()
private let topButtonSize: CGFloat = 44
private let bottomBarH: CGFloat = 44
// MARK: - Adaptive Colors
private static let primaryColor = UIColor { traits in
traits.userInterfaceStyle == .dark ? .white : .black
}
private static let secondaryColor = UIColor { traits in
traits.userInterfaceStyle == .dark
? UIColor.white.withAlphaComponent(0.6)
: UIColor.black.withAlphaComponent(0.5)
}
private static let separatorColor = UIColor { traits in
traits.userInterfaceStyle == .dark
? UIColor.white.withAlphaComponent(0.2)
: UIColor.black.withAlphaComponent(0.15)
}
// MARK: - Init
override init(frame: CGRect) {
super.init(frame: frame)
isUserInteractionEnabled = true
setupTopBar()
setupCounter()
setupBottomBar()
setupCaption()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Setup Top Bar
private func setupTopBar() {
topContainer.isUserInteractionEnabled = true
addSubview(topContainer)
backGlass.isCircle = true
backGlass.isUserInteractionEnabled = false
topContainer.addSubview(backGlass)
configureButton(backButton, icon: "chevron.left", size: 18, weight: .semibold, action: #selector(backTapped))
topContainer.addSubview(backButton)
menuGlass.isCircle = true
menuGlass.isUserInteractionEnabled = false
topContainer.addSubview(menuGlass)
let menuConfig = UIImage.SymbolConfiguration(pointSize: 18, weight: .medium)
menuButton.setImage(UIImage(systemName: "ellipsis", withConfiguration: menuConfig), for: .normal)
menuButton.tintColor = Self.primaryColor
menuButton.addTarget(self, action: #selector(menuTapped), for: .touchUpInside)
topContainer.addSubview(menuButton)
titleGlass.isUserInteractionEnabled = false
topContainer.addSubview(titleGlass)
titleLabel.font = .systemFont(ofSize: 17, weight: .semibold)
titleLabel.textColor = Self.primaryColor
titleLabel.textAlignment = .center
titleLabel.lineBreakMode = .byTruncatingTail
topContainer.addSubview(titleLabel)
subtitleLabel.font = .systemFont(ofSize: 12)
subtitleLabel.textColor = Self.secondaryColor
subtitleLabel.textAlignment = .center
topContainer.addSubview(subtitleLabel)
}
// MARK: - Setup Counter
private func setupCounter() {
counterContainer.isUserInteractionEnabled = false
addSubview(counterContainer)
counterGlass.isUserInteractionEnabled = false
counterContainer.addSubview(counterGlass)
counterLabel.font = .systemFont(ofSize: 12, weight: .semibold)
counterLabel.textColor = Self.primaryColor
counterLabel.textAlignment = .center
counterContainer.addSubview(counterLabel)
}
// MARK: - Setup Bottom Bar (Telegram parity)
private func setupBottomBar() {
bottomContainer.isUserInteractionEnabled = true
addSubview(bottomContainer)
// Left: Forward (single circle)
leftGlass.isCircle = true
leftGlass.isUserInteractionEnabled = false
bottomContainer.addSubview(leftGlass)
configureButton(forwardButton, icon: "arrowshape.turn.up.forward", size: 17, weight: .regular, action: #selector(forwardTapped))
bottomContainer.addSubview(forwardButton)
// Center: Draw + Captions (grouped capsule)
centerGlass.isUserInteractionEnabled = false
bottomContainer.addSubview(centerGlass)
configureButton(drawButton, icon: "pencil.tip.crop.circle", size: 17, weight: .regular, action: #selector(drawTapped))
bottomContainer.addSubview(drawButton)
centerSeparator.backgroundColor = Self.separatorColor
centerSeparator.isUserInteractionEnabled = false
bottomContainer.addSubview(centerSeparator)
configureButton(captionsButton, icon: "captions.bubble", size: 17, weight: .regular, action: #selector(captionsTapped))
bottomContainer.addSubview(captionsButton)
// Right: Delete (single circle)
rightGlass.isCircle = true
rightGlass.isUserInteractionEnabled = false
bottomContainer.addSubview(rightGlass)
configureButton(deleteButton, icon: "trash", size: 17, weight: .regular, action: #selector(deleteTapped))
bottomContainer.addSubview(deleteButton)
}
// MARK: - Setup Caption
private func setupCaption() {
captionContainer.isUserInteractionEnabled = false
captionContainer.isHidden = true
addSubview(captionContainer)
captionLabel.font = .systemFont(ofSize: 16)
captionLabel.textColor = Self.primaryColor
captionLabel.numberOfLines = 3
captionLabel.lineBreakMode = .byTruncatingTail
captionContainer.addSubview(captionLabel)
}
private func configureButton(_ button: UIButton, icon: String, size: CGFloat, weight: UIImage.SymbolWeight, action: Selector) {
let config = UIImage.SymbolConfiguration(pointSize: size, weight: weight)
button.setImage(UIImage(systemName: icon, withConfiguration: config), for: .normal)
button.tintColor = Self.primaryColor
button.addTarget(self, action: action, for: .touchUpInside)
}
// MARK: - Layout
override func layoutSubviews() {
super.layoutSubviews()
let sa = safeAreaInsets
let topY = sa.top > 0 ? sa.top : 20
layoutTopBar(topY: topY)
layoutCounter(topY: topY)
layoutBottomBar(sa: sa)
layoutCaption()
}
private func layoutTopBar(topY: CGFloat) {
let hPad: CGFloat = 16
topContainer.frame = CGRect(x: 0, y: topY, width: bounds.width, height: topButtonSize)
backGlass.frame = CGRect(x: hPad, y: 0, width: topButtonSize, height: topButtonSize)
backButton.frame = backGlass.frame
menuGlass.frame = CGRect(x: bounds.width - hPad - topButtonSize, y: 0, width: topButtonSize, height: topButtonSize)
menuButton.frame = menuGlass.frame
let titleMaxW = bounds.width - (hPad + topButtonSize + 12) * 2
let titleTextSize = titleLabel.sizeThatFits(CGSize(width: titleMaxW, height: 20))
let subtitleTextSize = subtitleLabel.sizeThatFits(CGSize(width: titleMaxW, height: 16))
let capsuleW = max(titleTextSize.width, subtitleTextSize.width) + 28
let capsuleX = (bounds.width - capsuleW) / 2
titleGlass.frame = CGRect(x: capsuleX, y: 0, width: capsuleW, height: topButtonSize)
titleLabel.frame = CGRect(x: capsuleX + 14, y: 5, width: capsuleW - 28, height: 20)
subtitleLabel.frame = CGRect(x: capsuleX + 14, y: 25, width: capsuleW - 28, height: 14)
}
private func layoutCounter(topY: CGFloat) {
let counterY = topY + topButtonSize + 6
let counterText = counterLabel.text ?? ""
let counterW = counterText.isEmpty ? 0 : counterLabel.sizeThatFits(CGSize(width: 200, height: 20)).width + 24
let counterH: CGFloat = 24
counterContainer.frame = CGRect(x: (bounds.width - counterW) / 2, y: counterY, width: counterW, height: counterH)
counterGlass.frame = counterContainer.bounds
counterLabel.frame = counterContainer.bounds
}
private func layoutBottomBar(sa: UIEdgeInsets) {
let hPad: CGFloat = 8
let bottomInset = sa.bottom > 0 ? sa.bottom : 16
let bottomY = bounds.height - bottomInset - bottomBarH
bottomContainer.frame = CGRect(x: 0, y: bottomY, width: bounds.width, height: bottomBarH)
// Left: Forward (single circle, 44×44)
leftGlass.frame = CGRect(x: hPad, y: 0, width: bottomBarH, height: bottomBarH)
forwardButton.frame = leftGlass.frame
// Center: Draw + Captions (grouped capsule, 88×44)
let centerW: CGFloat = bottomBarH * 2 // 88pt
let centerX = (bounds.width - centerW) / 2
centerGlass.frame = CGRect(x: centerX, y: 0, width: centerW, height: bottomBarH)
drawButton.frame = CGRect(x: centerX, y: 0, width: bottomBarH, height: bottomBarH)
captionsButton.frame = CGRect(x: centerX + bottomBarH, y: 0, width: bottomBarH, height: bottomBarH)
// Thin separator between Draw and Captions
let sepH: CGFloat = 24
centerSeparator.frame = CGRect(
x: centerX + bottomBarH - 0.25,
y: (bottomBarH - sepH) / 2,
width: 0.5,
height: sepH
)
// Right: Delete (single circle, 44×44)
rightGlass.frame = CGRect(x: bounds.width - hPad - bottomBarH, y: 0, width: bottomBarH, height: bottomBarH)
deleteButton.frame = rightGlass.frame
}
private func layoutCaption() {
guard !captionContainer.isHidden else { return }
let hPad: CGFloat = 16
let bottomGap: CGFloat = 14
let maxH: CGFloat = 80
let availW = bounds.width - hPad * 2
let textSize = captionLabel.sizeThatFits(CGSize(width: availW, height: maxH))
let captionH = min(textSize.height, maxH)
let captionY = bottomContainer.frame.minY - bottomGap - captionH
captionContainer.frame = CGRect(x: hPad, y: captionY, width: availW, height: captionH)
captionLabel.frame = captionContainer.bounds
}
// MARK: - Public API
func update(title: String, subtitle: String, counter: String, showCounter: Bool, caption: String) {
titleLabel.text = title
subtitleLabel.text = subtitle
counterLabel.text = counter
counterContainer.isHidden = !showCounter
captionLabel.text = caption
captionContainer.isHidden = caption.isEmpty
setNeedsLayout()
}
func menuButtonFrame(in coordinateSpace: UICoordinateSpace) -> CGRect {
menuButton.convert(menuButton.bounds, to: coordinateSpace)
}
func setControlsVisible(_ visible: Bool, animated: Bool) {
guard visible != isControlsVisible else { return }
isControlsVisible = visible
let block = { [self] in
topContainer.alpha = visible ? 1 : 0
counterContainer.alpha = visible ? 1 : 0
bottomContainer.alpha = visible ? 1 : 0
captionContainer.alpha = visible ? 1 : 0
}
if animated {
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseInOut, animations: block)
} else {
block()
}
}
// MARK: - Actions
@objc private func backTapped() { onBack?() }
@objc private func menuTapped() { onMenu?() }
@objc private func forwardTapped() { onForward?() }
@objc private func drawTapped() { onDraw?() }
@objc private func captionsTapped() { onCaptions?() }
@objc private func deleteTapped() { onDelete?() }
// MARK: - Hit Test
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hit = super.hitTest(point, with: event)
return hit is UIButton ? hit : nil
}
}