mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-07-25 08:50:51 +02:00
chore: migrate to new version + fixed several critical bugs
- Migrated project to latest Telegram iOS base (v12.3.2+) - Fixed circular dependency between GhostModeManager and MiscSettingsManager - Fixed multiple Bazel build configuration errors (select() default conditions) - Fixed duplicate type definitions in PeerInfoScreen - Fixed swiftmodule directory resolution in build scripts - Added Ghostgram Settings tab in main Settings menu with all 5 features - Cleared sensitive credentials from config.json (template-only now) - Excluded bazel-cache from version control
This commit is contained in:
@@ -12,92 +12,236 @@ import BundleIconComponent
|
||||
import TextBadgeComponent
|
||||
import LiquidLens
|
||||
import AppBundle
|
||||
import SearchBarNode
|
||||
import TabSelectionRecognizer
|
||||
|
||||
private final class TabSelectionRecognizer: UIGestureRecognizer {
|
||||
private var initialLocation: CGPoint?
|
||||
private var currentLocation: CGPoint?
|
||||
|
||||
override init(target: Any?, action: Selector?) {
|
||||
super.init(target: target, action: action)
|
||||
|
||||
self.delaysTouchesBegan = false
|
||||
self.delaysTouchesEnded = false
|
||||
}
|
||||
|
||||
override func reset() {
|
||||
super.reset()
|
||||
|
||||
self.initialLocation = nil
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
if self.initialLocation == nil {
|
||||
self.initialLocation = touches.first?.location(in: self.view)
|
||||
}
|
||||
self.currentLocation = self.initialLocation
|
||||
|
||||
self.state = .began
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesEnded(touches, with: event)
|
||||
|
||||
self.state = .ended
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesCancelled(touches, with: event)
|
||||
|
||||
self.state = .cancelled
|
||||
}
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesMoved(touches, with: event)
|
||||
|
||||
self.currentLocation = touches.first?.location(in: self.view)
|
||||
|
||||
self.state = .changed
|
||||
}
|
||||
|
||||
func translation(in: UIView?) -> CGPoint {
|
||||
if let initialLocation = self.initialLocation, let currentLocation = self.currentLocation {
|
||||
return CGPoint(x: currentLocation.x - initialLocation.x, y: currentLocation.y - initialLocation.y)
|
||||
}
|
||||
return CGPoint()
|
||||
}
|
||||
}
|
||||
public final class NavigationSearchView: UIView {
|
||||
private struct Params: Equatable {
|
||||
let size: CGSize
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let isActive: Bool
|
||||
|
||||
init(size: CGSize, theme: PresentationTheme, strings: PresentationStrings, isActive: Bool) {
|
||||
self.size = size
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.isActive = isActive
|
||||
}
|
||||
|
||||
static func ==(lhs: Params, rhs: Params) -> Bool {
|
||||
if lhs.size != rhs.size {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.isActive != rhs.isActive {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private let action: () -> Void
|
||||
private let closeAction: () -> Void
|
||||
|
||||
public final class TabBarSearchView: UIView {
|
||||
private let backgroundView: GlassBackgroundView
|
||||
private let iconView: GlassBackgroundView.ContentImageView
|
||||
private let iconView: UIImageView
|
||||
private(set) var searchBarNode: SearchBarNode?
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
self.backgroundView = GlassBackgroundView()
|
||||
self.iconView = GlassBackgroundView.ContentImageView()
|
||||
private var close: (background: GlassBackgroundView, icon: UIImageView)?
|
||||
|
||||
super.init(frame: frame)
|
||||
private var params: Params?
|
||||
|
||||
public init(action: @escaping () -> Void, closeAction: @escaping () -> Void) {
|
||||
self.action = action
|
||||
self.closeAction = closeAction
|
||||
|
||||
self.backgroundView = GlassBackgroundView()
|
||||
self.backgroundView.contentView.clipsToBounds = true
|
||||
self.iconView = UIImageView()
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.addSubview(self.backgroundView)
|
||||
self.backgroundView.contentView.addSubview(self.iconView)
|
||||
|
||||
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:))))
|
||||
}
|
||||
|
||||
@objc private func onTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.action()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func onCloseTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.closeAction()
|
||||
}
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func update(size: CGSize, isDark: Bool, tintColor: GlassBackgroundView.TintColor, iconColor: UIColor, transition: ComponentTransition) {
|
||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size))
|
||||
self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: isDark, tintColor: tintColor, transition: transition)
|
||||
public func update(size: CGSize, theme: PresentationTheme, strings: PresentationStrings, isActive: Bool, transition: ComponentTransition) {
|
||||
let params = Params(size: size, theme: theme, strings: strings, isActive: isActive)
|
||||
if self.params == params {
|
||||
return
|
||||
}
|
||||
self.params = params
|
||||
self.update(params: params, transition: transition)
|
||||
}
|
||||
|
||||
private func update(params: Params, transition: ComponentTransition) {
|
||||
let backgroundSize: CGSize
|
||||
if params.isActive {
|
||||
backgroundSize = CGSize(width: params.size.width - 48.0 - 8.0, height: params.size.height)
|
||||
} else {
|
||||
backgroundSize = CGSize(width: params.size.width, height: params.size.height)
|
||||
}
|
||||
|
||||
let previousBackgroundFrame = self.backgroundView.frame
|
||||
|
||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: backgroundSize))
|
||||
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25)
|
||||
|
||||
self.backgroundView.update(size: backgroundSize, cornerRadius: backgroundSize.height * 0.5, isDark: params.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: params.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: transition)
|
||||
|
||||
if self.iconView.image == nil {
|
||||
self.iconView.image = UIImage(bundleImageName: "Navigation/Search")?.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
self.iconView.tintColor = iconColor
|
||||
transition.setTintColor(view: self.iconView, color: params.isActive ? params.theme.rootController.navigationSearchBar.inputIconColor : params.theme.chat.inputPanel.panelControlColor)
|
||||
|
||||
if let image = self.iconView.image {
|
||||
transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) * 0.5), y: floor((size.height - image.size.height) * 0.5)), size: image.size))
|
||||
let imageSize: CGSize
|
||||
let iconFrame: CGRect
|
||||
if params.isActive {
|
||||
let iconFraction: CGFloat = 0.8
|
||||
imageSize = CGSize(width: image.size.width * iconFraction, height: image.size.height * iconFraction)
|
||||
iconFrame = CGRect(origin: CGPoint(x: 12.0, y: floor((params.size.height - imageSize.height) * 0.5)), size: imageSize)
|
||||
} else {
|
||||
iconFrame = CGRect(origin: CGPoint(x: floor((backgroundSize.width - image.size.width) * 0.5), y: floor((params.size.height - image.size.height) * 0.5)), size: image.size)
|
||||
}
|
||||
transition.setPosition(view: self.iconView, position: iconFrame.center)
|
||||
transition.setBounds(view: self.iconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
|
||||
}
|
||||
|
||||
if params.isActive {
|
||||
let searchBarNode: SearchBarNode
|
||||
var searchBarNodeTransition = transition
|
||||
if let current = self.searchBarNode {
|
||||
searchBarNode = current
|
||||
} else {
|
||||
searchBarNodeTransition = searchBarNodeTransition.withAnimation(.none)
|
||||
searchBarNode = SearchBarNode(
|
||||
theme: SearchBarNodeTheme(
|
||||
background: .clear,
|
||||
separator: .clear,
|
||||
inputFill: .clear,
|
||||
primaryText: params.theme.chat.inputPanel.panelControlColor,
|
||||
placeholder: params.theme.chat.inputPanel.inputPlaceholderColor,
|
||||
inputIcon: params.theme.chat.inputPanel.inputControlColor,
|
||||
inputClear: params.theme.chat.inputPanel.panelControlColor,
|
||||
accent: params.theme.chat.inputPanel.panelControlAccentColor,
|
||||
keyboard: params.theme.rootController.keyboardColor
|
||||
),
|
||||
presentationTheme: params.theme,
|
||||
strings: params.strings,
|
||||
fieldStyle: .inlineNavigation,
|
||||
icon: .loupe,
|
||||
forceSeparator: false,
|
||||
displayBackground: false,
|
||||
cancelText: nil
|
||||
)
|
||||
searchBarNode.placeholderString = NSAttributedString(string: params.strings.Common_Search, font: Font.regular(17.0), textColor: params.theme.chat.inputPanel.inputPlaceholderColor)
|
||||
self.searchBarNode = searchBarNode
|
||||
self.backgroundView.contentView.addSubview(searchBarNode.view)
|
||||
searchBarNode.view.alpha = 0.0
|
||||
}
|
||||
let searchBarFrame = CGRect(origin: CGPoint(x: 36.0, y: 0.0), size: CGSize(width: backgroundSize.width - 36.0 - 4.0, height: params.size.height))
|
||||
transition.setFrame(view: searchBarNode.view, frame: searchBarFrame)
|
||||
searchBarNode.updateLayout(boundingSize: searchBarFrame.size, leftInset: 0.0, rightInset: 0.0, transition: transition.containedViewLayoutTransition)
|
||||
alphaTransition.setAlpha(view: searchBarNode.view, alpha: 1.0)
|
||||
} else {
|
||||
if let searchBarNode = self.searchBarNode {
|
||||
self.searchBarNode = nil
|
||||
let searchBarNodeView = searchBarNode.view
|
||||
alphaTransition.setAlpha(view: searchBarNode.view, alpha: 0.0, completion: { [weak searchBarNodeView] _ in
|
||||
searchBarNodeView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if params.isActive {
|
||||
let closeFrame = CGRect(origin: CGPoint(x: params.size.width - 48.0, y: 0.0), size: CGSize(width: 48.0, height: 48.0))
|
||||
|
||||
let close: (background: GlassBackgroundView, icon: UIImageView)
|
||||
var closeTransition = transition
|
||||
if let current = self.close {
|
||||
close = current
|
||||
} else {
|
||||
closeTransition = closeTransition.withAnimation(.none)
|
||||
close = (GlassBackgroundView(), UIImageView())
|
||||
self.close = close
|
||||
|
||||
close.icon.image = generateImage(CGSize(width: 40.0, height: 40.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setLineWidth(2.0)
|
||||
context.setLineCap(.round)
|
||||
context.setStrokeColor(UIColor.white.cgColor)
|
||||
|
||||
context.beginPath()
|
||||
context.move(to: CGPoint(x: 12.0, y: 12.0))
|
||||
context.addLine(to: CGPoint(x: size.width - 12.0, y: size.height - 12.0))
|
||||
context.move(to: CGPoint(x: size.width - 12.0, y: 12.0))
|
||||
context.addLine(to: CGPoint(x: 12.0, y: size.height - 12.0))
|
||||
context.strokePath()
|
||||
})?.withRenderingMode(.alwaysTemplate)
|
||||
|
||||
close.background.contentView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onCloseTapGesture(_:))))
|
||||
|
||||
close.background.contentView.addSubview(close.icon)
|
||||
self.insertSubview(close.background, at: 0)
|
||||
|
||||
if let image = close.icon.image {
|
||||
close.icon.frame = image.size.centered(in: CGRect(origin: CGPoint(), size: closeFrame.size))
|
||||
}
|
||||
|
||||
close.background.frame = closeFrame.size.centered(in: previousBackgroundFrame)
|
||||
close.background.update(size: close.background.bounds.size, cornerRadius: close.background.bounds.height * 0.5, isDark: params.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: params.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: .immediate)
|
||||
ComponentTransition.immediate.setScale(view: close.background, scale: 0.001)
|
||||
}
|
||||
|
||||
close.icon.tintColor = params.theme.chat.inputPanel.panelControlColor
|
||||
|
||||
transition.setPosition(view: close.background, position: closeFrame.center)
|
||||
transition.setBounds(view: close.background, bounds: CGRect(origin: CGPoint(), size: closeFrame.size))
|
||||
transition.setScale(view: close.background, scale: 1.0)
|
||||
|
||||
if let image = close.icon.image {
|
||||
transition.setFrame(view: close.icon, frame: image.size.centered(in: CGRect(origin: CGPoint(), size: closeFrame.size)))
|
||||
}
|
||||
|
||||
close.background.update(size: closeFrame.size, cornerRadius: closeFrame.height * 0.5, isDark: params.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: params.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: closeTransition)
|
||||
} else {
|
||||
if let close = self.close {
|
||||
self.close = nil
|
||||
let closeBackground = close.background
|
||||
let closeFrame = CGSize(width: 48.0, height: 48.0).centered(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: params.size))
|
||||
transition.setPosition(view: closeBackground, position: closeFrame.center)
|
||||
transition.setBounds(view: closeBackground, bounds: CGRect(origin: CGPoint(), size: closeFrame.size))
|
||||
closeBackground.update(size: closeFrame.size, cornerRadius: closeFrame.height * 0.5, isDark: params.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: params.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: transition)
|
||||
transition.setScale(view: closeBackground, scale: 0.001, completion: { [weak closeBackground] _ in
|
||||
closeBackground?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,46 +275,81 @@ public final class TabBarComponent: Component {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public final class Search: Equatable {
|
||||
public let isActive: Bool
|
||||
public let activate: () -> Void
|
||||
public let deactivate: () -> Void
|
||||
|
||||
public init(isActive: Bool, activate: @escaping () -> Void, deactivate: @escaping () -> Void) {
|
||||
self.isActive = isActive
|
||||
self.activate = activate
|
||||
self.deactivate = deactivate
|
||||
}
|
||||
|
||||
public static func ==(lhs: Search, rhs: Search) -> Bool {
|
||||
if lhs.isActive != rhs.isActive {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public let theme: PresentationTheme
|
||||
public let strings: PresentationStrings
|
||||
public let items: [Item]
|
||||
public let search: Search?
|
||||
public let selectedId: AnyHashable?
|
||||
public let isTablet: Bool
|
||||
public let outerInsets: UIEdgeInsets
|
||||
|
||||
public init(
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
items: [Item],
|
||||
search: Search?,
|
||||
selectedId: AnyHashable?,
|
||||
isTablet: Bool
|
||||
outerInsets: UIEdgeInsets
|
||||
) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.items = items
|
||||
self.search = search
|
||||
self.selectedId = selectedId
|
||||
self.isTablet = isTablet
|
||||
self.outerInsets = outerInsets
|
||||
}
|
||||
|
||||
public static func ==(lhs: TabBarComponent, rhs: TabBarComponent) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.items != rhs.items {
|
||||
return false
|
||||
}
|
||||
if lhs.search != rhs.search {
|
||||
return false
|
||||
}
|
||||
if lhs.selectedId != rhs.selectedId {
|
||||
return false
|
||||
}
|
||||
if lhs.isTablet != rhs.isTablet {
|
||||
if lhs.outerInsets != rhs.outerInsets {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView, UITabBarDelegate, UIGestureRecognizerDelegate {
|
||||
private let backgroundContainer: GlassBackgroundContainerView
|
||||
private let liquidLensView: LiquidLensView
|
||||
private let contextGestureContainerView: ContextControllerSourceView
|
||||
|
||||
private var measureItemViews: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
private var itemViews: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
private var selectedItemViews: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
|
||||
private var searchView: NavigationSearchView?
|
||||
|
||||
private var tabSelectionRecognizer: TabSelectionRecognizer?
|
||||
private var itemWithActiveContextGesture: AnyHashable?
|
||||
@@ -178,11 +357,16 @@ public final class TabBarComponent: Component {
|
||||
private var component: TabBarComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
private var selectionGestureState: (startX: CGFloat, currentX: CGFloat)?
|
||||
private var selectionGestureState: (startX: CGFloat, currentX: CGFloat, itemWidth: CGFloat, itemId: AnyHashable)?
|
||||
private var overrideSelectedItemId: AnyHashable?
|
||||
|
||||
public var currentSearchNode: ASDisplayNode? {
|
||||
return self.searchView?.searchBarNode
|
||||
}
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
self.liquidLensView = LiquidLensView()
|
||||
self.backgroundContainer = GlassBackgroundContainerView()
|
||||
self.liquidLensView = LiquidLensView(kind: .externalContainer)
|
||||
|
||||
self.contextGestureContainerView = ContextControllerSourceView()
|
||||
self.contextGestureContainerView.isGestureEnabled = true
|
||||
@@ -194,12 +378,13 @@ public final class TabBarComponent: Component {
|
||||
self.traitOverrides.horizontalSizeClass = .compact
|
||||
}
|
||||
|
||||
self.addSubview(self.contextGestureContainerView)
|
||||
self.addSubview(self.backgroundContainer)
|
||||
self.backgroundContainer.contentView.addSubview(self.contextGestureContainerView)
|
||||
|
||||
self.contextGestureContainerView.addSubview(self.liquidLensView)
|
||||
let tabSelectionRecognizer = TabSelectionRecognizer(target: self, action: #selector(self.onTabSelectionGesture(_:)))
|
||||
self.tabSelectionRecognizer = tabSelectionRecognizer
|
||||
self.addGestureRecognizer(tabSelectionRecognizer)
|
||||
self.contextGestureContainerView.addGestureRecognizer(tabSelectionRecognizer)
|
||||
|
||||
self.contextGestureContainerView.shouldBegin = { [weak self] point in
|
||||
guard let self, let component = self.component else {
|
||||
@@ -281,29 +466,41 @@ public final class TabBarComponent: Component {
|
||||
}
|
||||
|
||||
@objc private func onTabSelectionGesture(_ recognizer: TabSelectionRecognizer) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
if let itemId = self.item(at: recognizer.location(in: self)), let itemView = self.itemViews[itemId]?.view {
|
||||
if let search = component.search, search.isActive {
|
||||
} else if let itemId = self.item(at: recognizer.location(in: self)), let itemView = self.itemViews[itemId]?.view {
|
||||
let startX = itemView.frame.minX - 4.0
|
||||
self.selectionGestureState = (startX, startX)
|
||||
self.selectionGestureState = (startX, startX, itemView.bounds.width, itemId)
|
||||
self.state?.updated(transition: .spring(duration: 0.4), isLocal: true)
|
||||
}
|
||||
case .changed:
|
||||
if var selectionGestureState = self.selectionGestureState {
|
||||
if let search = component.search, search.isActive {
|
||||
} else if var selectionGestureState = self.selectionGestureState {
|
||||
selectionGestureState.currentX = selectionGestureState.startX + recognizer.translation(in: self).x
|
||||
if let itemId = self.item(at: recognizer.location(in: self)) {
|
||||
selectionGestureState.itemId = itemId
|
||||
}
|
||||
self.selectionGestureState = selectionGestureState
|
||||
self.state?.updated(transition: .immediate, isLocal: true)
|
||||
}
|
||||
case .ended, .cancelled:
|
||||
self.selectionGestureState = nil
|
||||
if let component = self.component, let itemId = self.item(at: recognizer.location(in: self)) {
|
||||
guard let item = component.items.first(where: { $0.id == itemId }) else {
|
||||
return
|
||||
if let search = component.search, search.isActive {
|
||||
search.deactivate()
|
||||
} else if let selectionGestureState = self.selectionGestureState {
|
||||
self.selectionGestureState = nil
|
||||
if case .ended = recognizer.state, let component = self.component {
|
||||
guard let item = component.items.first(where: { $0.id == selectionGestureState.itemId }) else {
|
||||
return
|
||||
}
|
||||
self.overrideSelectedItemId = selectionGestureState.itemId
|
||||
item.action(false)
|
||||
}
|
||||
self.overrideSelectedItemId = itemId
|
||||
item.action(false)
|
||||
self.state?.updated(transition: .spring(duration: 0.4), isLocal: true)
|
||||
}
|
||||
self.state?.updated(transition: .spring(duration: 0.4), isLocal: true)
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -355,29 +552,91 @@ public final class TabBarComponent: Component {
|
||||
}
|
||||
|
||||
func update(component: TabBarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25)
|
||||
let _ = alphaTransition
|
||||
|
||||
let innerInset: CGFloat = 4.0
|
||||
let availableSize = CGSize(width: min(500.0, availableSize.width), height: availableSize.height)
|
||||
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let _ = innerInset
|
||||
let _ = availableSize
|
||||
let _ = previousComponent
|
||||
|
||||
self.overrideUserInterfaceStyle = component.theme.overallDarkAppearance ? .dark : .light
|
||||
|
||||
let itemSize = CGSize(width: floor((availableSize.width - innerInset * 2.0) / CGFloat(component.items.count)), height: 56.0)
|
||||
let contentWidth: CGFloat = innerInset * 2.0 + CGFloat(component.items.count) * itemSize.width
|
||||
let size = CGSize(width: min(availableSize.width, contentWidth), height: itemSize.height + innerInset * 2.0)
|
||||
let barHeight: CGFloat = 56.0 + innerInset * 2.0
|
||||
|
||||
var availableItemsWidth: CGFloat = availableSize.width - innerInset * 2.0
|
||||
if component.search != nil {
|
||||
availableItemsWidth -= barHeight + 8.0
|
||||
}
|
||||
|
||||
var unboundItemWidths: [CGFloat] = []
|
||||
|
||||
var validIds: [AnyHashable] = []
|
||||
var selectionFrame: CGRect?
|
||||
var unboundItemWidthSum: CGFloat = 0.0
|
||||
for index in 0 ..< component.items.count {
|
||||
let item = component.items[index]
|
||||
validIds.append(item.id)
|
||||
|
||||
let measureItemView: ComponentView<Empty>
|
||||
if let current = self.measureItemViews[item.id] {
|
||||
measureItemView = current
|
||||
} else {
|
||||
measureItemView = ComponentView()
|
||||
self.measureItemViews[item.id] = measureItemView
|
||||
}
|
||||
|
||||
let itemSize = measureItemView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(ItemComponent(
|
||||
item: item,
|
||||
theme: component.theme,
|
||||
isCompact: false,
|
||||
isSelected: false,
|
||||
isUnconstrained: true
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 200.0, height: 56.0)
|
||||
)
|
||||
|
||||
unboundItemWidths.append(itemSize.width)
|
||||
unboundItemWidthSum += itemSize.width
|
||||
}
|
||||
|
||||
let itemWidths: [CGFloat]
|
||||
let totalItemsWidth: CGFloat
|
||||
|
||||
let equalWidth = floorToScreenPixels(availableItemsWidth / CGFloat(component.items.count))
|
||||
if unboundItemWidths.allSatisfy({ $0 <= equalWidth }) {
|
||||
// All items fit in equal width — use equal widths for optical alignment
|
||||
itemWidths = Array(repeating: equalWidth, count: component.items.count)
|
||||
totalItemsWidth = equalWidth * CGFloat(component.items.count)
|
||||
} else {
|
||||
// Some items need more space — use weighted fit
|
||||
let itemWeightNorm: CGFloat = availableItemsWidth / unboundItemWidthSum
|
||||
var widths: [CGFloat] = []
|
||||
var total: CGFloat = 0.0
|
||||
for index in 0 ..< component.items.count {
|
||||
let itemWidth = floorToScreenPixels(unboundItemWidths[index] * itemWeightNorm)
|
||||
widths.append(itemWidth)
|
||||
total += itemWidth
|
||||
}
|
||||
itemWidths = widths
|
||||
totalItemsWidth = total
|
||||
}
|
||||
|
||||
let itemHeight: CGFloat = 56.0
|
||||
let contentWidth: CGFloat = innerInset * 2.0 + totalItemsWidth
|
||||
let tabsSize = CGSize(width: min(availableSize.width, contentWidth), height: itemHeight + innerInset * 2.0)
|
||||
|
||||
var selectionFrame: CGRect?
|
||||
var nextItemX: CGFloat = innerInset
|
||||
for index in 0 ..< component.items.count {
|
||||
let item = component.items[index]
|
||||
|
||||
let itemSize = CGSize(width: itemWidths[index], height: itemHeight)
|
||||
|
||||
let itemView: ComponentView<Empty>
|
||||
var itemTransition = transition
|
||||
|
||||
@@ -409,7 +668,9 @@ public final class TabBarComponent: Component {
|
||||
component: AnyComponent(ItemComponent(
|
||||
item: item,
|
||||
theme: component.theme,
|
||||
isSelected: false
|
||||
isCompact: component.search?.isActive == true,
|
||||
isSelected: false,
|
||||
isUnconstrained: false
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: itemSize
|
||||
@@ -419,14 +680,23 @@ public final class TabBarComponent: Component {
|
||||
component: AnyComponent(ItemComponent(
|
||||
item: item,
|
||||
theme: component.theme,
|
||||
isSelected: true
|
||||
isCompact: component.search?.isActive == true,
|
||||
isSelected: true,
|
||||
isUnconstrained: false
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: itemSize
|
||||
)
|
||||
|
||||
let itemFrame = CGRect(origin: CGPoint(x: innerInset + CGFloat(index) * itemSize.width, y: floor((size.height - itemSize.height) * 0.5)), size: itemSize)
|
||||
var itemFrame = CGRect(origin: CGPoint(x: nextItemX, y: floor((tabsSize.height - itemSize.height) * 0.5)), size: itemSize)
|
||||
nextItemX += itemSize.width
|
||||
if isItemSelected {
|
||||
selectionFrame = itemFrame
|
||||
}
|
||||
|
||||
if let itemComponentView = itemView.view as? ItemComponent.View, let selectedItemComponentView = selectedItemView.view as? ItemComponent.View {
|
||||
let itemAlphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25)
|
||||
|
||||
if itemComponentView.superview == nil {
|
||||
itemComponentView.isUserInteractionEnabled = false
|
||||
selectedItemComponentView.isUserInteractionEnabled = false
|
||||
@@ -434,6 +704,27 @@ public final class TabBarComponent: Component {
|
||||
self.liquidLensView.contentView.addSubview(itemComponentView)
|
||||
self.liquidLensView.selectedContentView.addSubview(selectedItemComponentView)
|
||||
}
|
||||
|
||||
if let search = component.search, search.isActive {
|
||||
if isItemSelected {
|
||||
itemFrame.origin.x = floor((48.0 - itemSize.width) * 0.5)
|
||||
itemTransition.setAlpha(view: itemComponentView, alpha: 1.0)
|
||||
itemAlphaTransition.setBlur(layer: itemComponentView.layer, radius: 0.0)
|
||||
itemTransition.setAlpha(view: selectedItemComponentView, alpha: 1.0)
|
||||
itemAlphaTransition.setBlur(layer: selectedItemComponentView.layer, radius: 0.0)
|
||||
} else {
|
||||
itemTransition.setAlpha(view: itemComponentView, alpha: 0.0)
|
||||
itemAlphaTransition.setBlur(layer: itemComponentView.layer, radius: 10.0)
|
||||
itemTransition.setAlpha(view: selectedItemComponentView, alpha: 0.0)
|
||||
itemAlphaTransition.setBlur(layer: selectedItemComponentView.layer, radius: 10.0)
|
||||
}
|
||||
} else {
|
||||
itemTransition.setAlpha(view: itemComponentView, alpha: 1.0)
|
||||
itemAlphaTransition.setBlur(layer: itemComponentView.layer, radius: 0.0)
|
||||
itemTransition.setAlpha(view: selectedItemComponentView, alpha: 1.0)
|
||||
itemAlphaTransition.setBlur(layer: selectedItemComponentView.layer, radius: 0.0)
|
||||
}
|
||||
|
||||
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
|
||||
itemTransition.setPosition(view: selectedItemComponentView, position: itemFrame.center)
|
||||
itemTransition.setBounds(view: selectedItemComponentView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
|
||||
@@ -444,9 +735,6 @@ public final class TabBarComponent: Component {
|
||||
selectedItemComponentView.playSelectionAnimation()
|
||||
}
|
||||
}
|
||||
if isItemSelected {
|
||||
selectionFrame = itemFrame
|
||||
}
|
||||
}
|
||||
|
||||
var removeIds: [AnyHashable] = []
|
||||
@@ -460,22 +748,95 @@ public final class TabBarComponent: Component {
|
||||
for id in removeIds {
|
||||
self.itemViews.removeValue(forKey: id)
|
||||
self.selectedItemViews.removeValue(forKey: id)
|
||||
self.measureItemViews.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.contextGestureContainerView, frame: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
transition.setFrame(view: self.liquidLensView, frame: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let lensSelection: (x: CGFloat, width: CGFloat)
|
||||
if let selectionGestureState = self.selectionGestureState {
|
||||
lensSelection = (selectionGestureState.currentX, itemSize.width + innerInset * 2.0)
|
||||
} else if let selectionFrame {
|
||||
lensSelection = (selectionFrame.minX - innerInset, itemSize.width + innerInset * 2.0)
|
||||
} else {
|
||||
lensSelection = (0.0, itemSize.width)
|
||||
var tabsFrame = CGRect(origin: CGPoint(), size: tabsSize)
|
||||
if let search = component.search, search.isActive {
|
||||
tabsFrame.size = CGSize(width: 48.0, height: 48.0)
|
||||
tabsFrame.origin.y = tabsSize.height - 48.0
|
||||
tabsFrame.origin.x = -component.outerInsets.left - tabsFrame.width
|
||||
}
|
||||
|
||||
self.liquidLensView.update(size: size, selectionX: lensSelection.x, selectionWidth: lensSelection.width, isDark: component.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, transition: transition)
|
||||
transition.setFrame(view: self.contextGestureContainerView, frame: tabsFrame)
|
||||
transition.setFrame(view: self.liquidLensView, frame: CGRect(origin: CGPoint(), size: tabsSize))
|
||||
|
||||
var lensSelection: (x: CGFloat, width: CGFloat)
|
||||
if let selectionGestureState = self.selectionGestureState {
|
||||
lensSelection = (selectionGestureState.currentX, selectionGestureState.itemWidth + innerInset * 2.0)
|
||||
} else if let selectionFrame {
|
||||
lensSelection = (selectionFrame.minX - innerInset, selectionFrame.width + innerInset * 2.0)
|
||||
} else {
|
||||
lensSelection = (0.0, 56.0)
|
||||
}
|
||||
|
||||
var lensSize: CGSize = tabsSize
|
||||
var isLensCollapsed = false
|
||||
if let search = component.search, search.isActive {
|
||||
isLensCollapsed = true
|
||||
lensSize = CGSize(width: 48.0, height: 48.0)
|
||||
lensSelection = (0.0, 48.0)
|
||||
}
|
||||
|
||||
lensSelection.x = max(0.0, min(lensSelection.x, lensSize.width - lensSelection.width))
|
||||
|
||||
self.liquidLensView.update(size: lensSize, selectionOrigin: CGPoint(x: lensSelection.x, y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: lensSize.height), inset: 4.0, isDark: component.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: isLensCollapsed, transition: transition)
|
||||
|
||||
var size = tabsSize
|
||||
|
||||
if let search = component.search {
|
||||
let searchSize: CGSize
|
||||
let searchFrame: CGRect
|
||||
if search.isActive {
|
||||
size.width = availableSize.width
|
||||
searchSize = CGSize(width: availableSize.width, height: 48.0)
|
||||
searchFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - searchSize.height), size: searchSize)
|
||||
} else {
|
||||
searchSize = CGSize(width: barHeight, height: barHeight)
|
||||
size.width += barHeight + 8.0
|
||||
searchFrame = CGRect(origin: CGPoint(x: availableSize.width - searchSize.width, y: 0.0), size: searchSize)
|
||||
}
|
||||
|
||||
let searchView: NavigationSearchView
|
||||
var searchViewTransition = transition
|
||||
if let current = self.searchView {
|
||||
searchView = current
|
||||
} else {
|
||||
searchViewTransition = searchViewTransition.withAnimation(.none)
|
||||
searchView = NavigationSearchView(
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.search?.activate()
|
||||
},
|
||||
closeAction: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.search?.deactivate()
|
||||
}
|
||||
)
|
||||
self.searchView = searchView
|
||||
self.backgroundContainer.contentView.addSubview(searchView)
|
||||
searchView.frame = CGRect(origin: CGPoint(x: availableSize.width + 50.0, y: 0.0), size: searchSize)
|
||||
}
|
||||
searchView.update(size: searchSize, theme: component.theme, strings: component.strings, isActive: search.isActive, transition: searchViewTransition)
|
||||
transition.setFrame(view: searchView, frame: searchFrame)
|
||||
} else {
|
||||
if let searchView = self.searchView {
|
||||
self.searchView = nil
|
||||
transition.setFrame(view: searchView, frame: CGRect(origin: CGPoint(x: availableSize.width + 50.0, y: 0.0), size: searchView.bounds.size), completion: { [weak searchView] completed in
|
||||
guard let searchView, completed else {
|
||||
return
|
||||
}
|
||||
searchView.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: CGPoint(), size: size))
|
||||
self.backgroundContainer.update(size: size, isDark: component.theme.overallDarkAppearance, transition: transition)
|
||||
|
||||
return size
|
||||
}
|
||||
@@ -493,12 +854,16 @@ public final class TabBarComponent: Component {
|
||||
private final class ItemComponent: Component {
|
||||
let item: TabBarComponent.Item
|
||||
let theme: PresentationTheme
|
||||
let isCompact: Bool
|
||||
let isSelected: Bool
|
||||
let isUnconstrained: Bool
|
||||
|
||||
init(item: TabBarComponent.Item, theme: PresentationTheme, isSelected: Bool) {
|
||||
init(item: TabBarComponent.Item, theme: PresentationTheme, isCompact: Bool, isSelected: Bool, isUnconstrained: Bool) {
|
||||
self.item = item
|
||||
self.theme = theme
|
||||
self.isCompact = isCompact
|
||||
self.isSelected = isSelected
|
||||
self.isUnconstrained = isUnconstrained
|
||||
}
|
||||
|
||||
static func ==(lhs: ItemComponent, rhs: ItemComponent) -> Bool {
|
||||
@@ -508,9 +873,15 @@ private final class ItemComponent: Component {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.isCompact != rhs.isCompact {
|
||||
return false
|
||||
}
|
||||
if lhs.isSelected != rhs.isSelected {
|
||||
return false
|
||||
}
|
||||
if lhs.isUnconstrained != rhs.isUnconstrained {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -562,6 +933,8 @@ private final class ItemComponent: Component {
|
||||
}
|
||||
|
||||
func update(component: ItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25)
|
||||
|
||||
let previousComponent = self.component
|
||||
|
||||
if previousComponent?.item.item !== component.item.item {
|
||||
@@ -619,7 +992,7 @@ private final class ItemComponent: Component {
|
||||
content: LottieComponent.AppBundleContent(
|
||||
name: animationName
|
||||
),
|
||||
color: component.isSelected ? component.theme.rootController.tabBar.selectedTextColor : component.theme.rootController.tabBar.textColor,
|
||||
color: (component.isSelected && !component.isCompact) ? component.theme.rootController.tabBar.selectedTextColor : component.theme.rootController.tabBar.textColor,
|
||||
placeholderColor: nil,
|
||||
startingPosition: .end,
|
||||
size: CGSize(width: 48.0, height: 48.0),
|
||||
@@ -692,6 +1065,7 @@ private final class ItemComponent: Component {
|
||||
self.contextContainerView.contentView.addSubview(titleView)
|
||||
}
|
||||
titleView.frame = titleFrame
|
||||
alphaTransition.setAlpha(view: titleView, alpha: component.isCompact ? 0.0 : 1.0)
|
||||
}
|
||||
|
||||
if let badgeText = component.item.item.badgeValue, !badgeText.isEmpty {
|
||||
@@ -711,7 +1085,7 @@ private final class ItemComponent: Component {
|
||||
font: Font.regular(13.0),
|
||||
background: component.theme.rootController.tabBar.badgeBackgroundColor,
|
||||
foreground: component.theme.rootController.tabBar.badgeTextColor,
|
||||
insets: UIEdgeInsets(top: 0.0, left: 6.0, bottom: 1.0, right: 6.0)
|
||||
insets: UIEdgeInsets(top: 0.0, left: 5.0, bottom: 1.0, right: 5.0)
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||
@@ -723,6 +1097,7 @@ private final class ItemComponent: Component {
|
||||
self.contextContainerView.contentView.addSubview(badgeView)
|
||||
}
|
||||
badgeTransition.setFrame(view: badgeView, frame: badgeFrame)
|
||||
alphaTransition.setAlpha(view: badgeView, alpha: component.isCompact ? 0.0 : 1.0)
|
||||
}
|
||||
} else if let badge = self.badge {
|
||||
self.badge = nil
|
||||
@@ -733,7 +1108,11 @@ private final class ItemComponent: Component {
|
||||
transition.setFrame(view: self.contextContainerView.contentView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
self.contextContainerView.contentRect = CGRect(origin: CGPoint(), size: availableSize)
|
||||
|
||||
return availableSize
|
||||
if component.isUnconstrained {
|
||||
return CGSize(width: titleSize.width + 10.0 * 2.0, height: availableSize.height)
|
||||
} else {
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user