mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-03 05:03:45 +00:00
Merge branch 'beta'
This commit is contained in:
commit
e16f2ff47f
@ -4613,44 +4613,46 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
|
||||
let _ = (combineLatest(self.chatListDisplayNode.mainContainerNode.currentItemNode.contentsReady |> take(1), self.context.account.postbox.tailChatListView(groupId: .root, count: 16, summaryComponents: ChatListEntrySummaryComponents(components: [:])) |> take(1))
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] _, chatListView in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
/*if let scrollToTop = strongSelf.scrollToTop {
|
||||
scrollToTop()
|
||||
}*/
|
||||
|
||||
let tabsIsEmpty: Bool
|
||||
if let (resolvedItems, displayTabsAtBottom, _) = strongSelf.tabContainerData {
|
||||
tabsIsEmpty = resolvedItems.count <= 1 || displayTabsAtBottom
|
||||
} else {
|
||||
tabsIsEmpty = true
|
||||
}
|
||||
let _ = tabsIsEmpty
|
||||
//TODO:swap tabs
|
||||
|
||||
let displaySearchFilters = true
|
||||
|
||||
if let filterContainerNodeAndActivate = strongSelf.chatListDisplayNode.activateSearch(placeholderNode: searchContentNode.placeholderNode, displaySearchFilters: displaySearchFilters, hasDownloads: strongSelf.hasDownloads, initialFilter: filter, navigationController: strongSelf.navigationController as? NavigationController) {
|
||||
let (filterContainerNode, activate) = filterContainerNodeAndActivate
|
||||
if displaySearchFilters {
|
||||
let searchTabsNode = SparseNode()
|
||||
strongSelf.searchTabsNode = searchTabsNode
|
||||
searchTabsNode.addSubnode(filterContainerNode)
|
||||
Task { @MainActor in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
activate(filter != .downloads)
|
||||
/*if let scrollToTop = strongSelf.scrollToTop {
|
||||
scrollToTop()
|
||||
}*/
|
||||
|
||||
if let searchContentNode = strongSelf.chatListDisplayNode.searchDisplayController?.contentNode as? ChatListSearchContainerNode {
|
||||
searchContentNode.search(filter: filter, query: query)
|
||||
let tabsIsEmpty: Bool
|
||||
if let (resolvedItems, displayTabsAtBottom, _) = strongSelf.tabContainerData {
|
||||
tabsIsEmpty = resolvedItems.count <= 1 || displayTabsAtBottom
|
||||
} else {
|
||||
tabsIsEmpty = true
|
||||
}
|
||||
let _ = tabsIsEmpty
|
||||
//TODO:swap tabs
|
||||
|
||||
let displaySearchFilters = true
|
||||
|
||||
if let filterContainerNodeAndActivate = await strongSelf.chatListDisplayNode.activateSearch(placeholderNode: searchContentNode.placeholderNode, displaySearchFilters: displaySearchFilters, hasDownloads: strongSelf.hasDownloads, initialFilter: filter, navigationController: strongSelf.navigationController as? NavigationController) {
|
||||
let (filterContainerNode, activate) = filterContainerNodeAndActivate
|
||||
if displaySearchFilters {
|
||||
let searchTabsNode = SparseNode()
|
||||
strongSelf.searchTabsNode = searchTabsNode
|
||||
searchTabsNode.addSubnode(filterContainerNode)
|
||||
}
|
||||
|
||||
activate(filter != .downloads)
|
||||
|
||||
if let searchContentNode = strongSelf.chatListDisplayNode.searchDisplayController?.contentNode as? ChatListSearchContainerNode {
|
||||
searchContentNode.search(filter: filter, query: query)
|
||||
}
|
||||
}
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
|
||||
strongSelf.setDisplayNavigationBar(false, transition: transition)
|
||||
|
||||
(strongSelf.parent as? TabBarController)?.updateIsTabBarHidden(true, transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
|
||||
strongSelf.setDisplayNavigationBar(false, transition: transition)
|
||||
|
||||
(strongSelf.parent as? TabBarController)?.updateIsTabBarHidden(true, transition: .animated(duration: 0.4, curve: .spring))
|
||||
})
|
||||
|
||||
self.isSearchActive = true
|
||||
|
||||
@ -20,6 +20,7 @@ import ComponentFlow
|
||||
import ChatFolderLinkPreviewScreen
|
||||
import ChatListHeaderComponent
|
||||
import StoryPeerListComponent
|
||||
import TelegramNotices
|
||||
|
||||
public enum ChatListContainerNodeFilter: Equatable {
|
||||
case all
|
||||
@ -1649,7 +1650,8 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func activateSearch(placeholderNode: SearchBarPlaceholderNode, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter, navigationController: NavigationController?) -> (ASDisplayNode, (Bool) -> Void)? {
|
||||
@MainActor
|
||||
func activateSearch(placeholderNode: SearchBarPlaceholderNode, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter, navigationController: NavigationController?) async -> (ASDisplayNode, (Bool) -> Void)? {
|
||||
guard let (containerLayout, _, _, cleanNavigationBarHeight, _) = self.containerLayout, self.searchDisplayController == nil else {
|
||||
return nil
|
||||
}
|
||||
@ -1688,6 +1690,9 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
|
||||
self?.controller?.openAdInfo(node: node, adPeer: adPeer)
|
||||
}
|
||||
|
||||
let searchTips = await ApplicationSpecificNotice.getGlobalPostsSearch(accountManager: self.context.sharedContext.accountManager).get()
|
||||
contentNode.displayGlobalPostsNewBadge = searchTips < 3
|
||||
|
||||
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, mode: .list, contentNode: contentNode, cancel: { [weak self] in
|
||||
if let requestDeactivateSearch = self?.requestDeactivateSearch {
|
||||
requestDeactivateSearch()
|
||||
|
||||
@ -131,6 +131,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
private var forumPeer: EnginePeer?
|
||||
private var hasPublicPostsTab = false
|
||||
private var showPublicPostsTab = false
|
||||
public var displayGlobalPostsNewBadge = false
|
||||
|
||||
private var shareStatusDisposable: MetaDisposable?
|
||||
|
||||
@ -715,7 +716,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
|
||||
filters = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: !isForum && self.hasDownloads, hasPublicPosts: self.showPublicPostsTab).map(\.filter)
|
||||
}
|
||||
self.filterContainerNode.update(size: CGSize(width: layout.size.width - 40.0, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, displayGlobalPostsNewBadge: true, selectedFilter: self.selectedFilter?.id, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: transition)
|
||||
self.filterContainerNode.update(size: CGSize(width: layout.size.width - 40.0, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, displayGlobalPostsNewBadge: self.displayGlobalPostsNewBadge, selectedFilter: self.selectedFilter?.id, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
@ -786,7 +787,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
}
|
||||
|
||||
let overflowInset: CGFloat = 20.0
|
||||
self.filterContainerNode.update(size: CGSize(width: layout.size.width - overflowInset * 2.0, height: 38.0), sideInset: layout.safeInsets.left - overflowInset, filters: filters.map { .filter($0) }, displayGlobalPostsNewBadge: true, selectedFilter: self.selectedFilter?.id, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring))
|
||||
self.filterContainerNode.update(size: CGSize(width: layout.size.width - overflowInset * 2.0, height: 38.0), sideInset: layout.safeInsets.left - overflowInset, filters: filters.map { .filter($0) }, displayGlobalPostsNewBadge: self.displayGlobalPostsNewBadge, selectedFilter: self.selectedFilter?.id, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring))
|
||||
|
||||
if isFirstTime {
|
||||
self.filterContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
|
||||
@ -2061,7 +2061,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
}
|
||||
|
||||
var defaultFoundRemoteMessagesSignal: Signal<([FoundRemoteMessages], Bool), NoError> = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false))
|
||||
if key == .globalPosts {
|
||||
if key == .globalPosts, let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["ios_load_empty_global_posts"] as? Double, value != 0.0 {
|
||||
let searchSignal = context.engine.messages.searchMessages(location: .general(scope: .globalPosts(allowPaidStars: nil), tags: nil, minDate: nil, maxDate: nil), query: "", state: nil, limit: 50)
|
||||
|> map { resultData -> ChatListSearchMessagesResult in
|
||||
let (result, updatedState) = resultData
|
||||
|
||||
@ -9,6 +9,7 @@ import AccountContext
|
||||
import ContextUI
|
||||
import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
import TelegramNotices
|
||||
|
||||
protocol ChatListSearchPaneNode: ASDisplayNode {
|
||||
var isReady: Signal<Bool, NoError> { get }
|
||||
@ -238,6 +239,11 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if key == .globalPosts {
|
||||
let _ = ApplicationSpecificNotice.incrementGlobalPostsSearch(accountManager: self.context.sharedContext.accountManager).startStandalone()
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
#else
|
||||
self.isAdjacentLoadingEnabled = true
|
||||
|
||||
@ -4,6 +4,23 @@ import Display
|
||||
import ComponentFlow
|
||||
import TelegramPresentationData
|
||||
|
||||
extension ComponentTransition {
|
||||
func animateBlur(layer: CALayer, from: CGFloat, to: CGFloat, delay: Double = 0.0, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
|
||||
if let blurFilter = CALayer.blur() {
|
||||
blurFilter.setValue(to as NSNumber, forKey: "inputRadius")
|
||||
layer.filters = [blurFilter]
|
||||
layer.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.3, delay: delay, removeOnCompletion: removeOnCompletion, completion: { [weak layer] _ in
|
||||
guard let layer else {
|
||||
return
|
||||
}
|
||||
if to == 0.0 && removeOnCompletion {
|
||||
layer.filters = nil
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class AnimatedTextComponent: Component {
|
||||
public struct Item: Equatable {
|
||||
public enum Content: Equatable {
|
||||
@ -27,19 +44,25 @@ public final class AnimatedTextComponent: Component {
|
||||
public let items: [Item]
|
||||
public let noDelay: Bool
|
||||
public let animateScale: Bool
|
||||
public let preferredDirectionIsDown: Bool
|
||||
public let blur: Bool
|
||||
|
||||
public init(
|
||||
font: UIFont,
|
||||
color: UIColor,
|
||||
items: [Item],
|
||||
noDelay: Bool = false,
|
||||
animateScale: Bool = true
|
||||
animateScale: Bool = true,
|
||||
preferredDirectionIsDown: Bool = false,
|
||||
blur: Bool = false
|
||||
) {
|
||||
self.font = font
|
||||
self.color = color
|
||||
self.items = items
|
||||
self.noDelay = noDelay
|
||||
self.animateScale = animateScale
|
||||
self.preferredDirectionIsDown = preferredDirectionIsDown
|
||||
self.blur = blur
|
||||
}
|
||||
|
||||
public static func ==(lhs: AnimatedTextComponent, rhs: AnimatedTextComponent) -> Bool {
|
||||
@ -58,6 +81,12 @@ public final class AnimatedTextComponent: Component {
|
||||
if lhs.animateScale != rhs.animateScale {
|
||||
return false
|
||||
}
|
||||
if lhs.preferredDirectionIsDown != rhs.preferredDirectionIsDown {
|
||||
return false
|
||||
}
|
||||
if lhs.blur != rhs.blur {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -88,8 +117,14 @@ public final class AnimatedTextComponent: Component {
|
||||
var size = CGSize()
|
||||
|
||||
let delayNorm: CGFloat = 0.002
|
||||
var offsetNorm: CGFloat = 0.4
|
||||
let transitionBlurRadius: CGFloat = 6.0
|
||||
|
||||
var firstDelayWidth: CGFloat?
|
||||
if component.preferredDirectionIsDown {
|
||||
firstDelayWidth = 0.0
|
||||
offsetNorm = 0.8
|
||||
}
|
||||
|
||||
var validKeys: [CharacterKey] = []
|
||||
for item in component.items {
|
||||
@ -119,6 +154,57 @@ public final class AnimatedTextComponent: Component {
|
||||
index += 1
|
||||
|
||||
validKeys.append(characterKey)
|
||||
}
|
||||
}
|
||||
|
||||
var outLastDelayWidth: CGFloat?
|
||||
var outFirstDelayWidth: CGFloat?
|
||||
if component.preferredDirectionIsDown {
|
||||
for (key, characterView) in self.characters {
|
||||
if !validKeys.contains(key), let characterView = characterView.view {
|
||||
if let outFirstDelayWidthValue = outFirstDelayWidth {
|
||||
outFirstDelayWidth = max(outFirstDelayWidthValue, characterView.frame.center.x)
|
||||
} else {
|
||||
outFirstDelayWidth = characterView.frame.center.x
|
||||
}
|
||||
|
||||
if let outLastDelayWidthValue = outLastDelayWidth {
|
||||
outLastDelayWidth = min(outLastDelayWidthValue, characterView.frame.center.x)
|
||||
} else {
|
||||
outLastDelayWidth = characterView.frame.center.x
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if outLastDelayWidth != nil {
|
||||
firstDelayWidth = outLastDelayWidth
|
||||
}
|
||||
|
||||
for item in component.items {
|
||||
var itemText: [String] = []
|
||||
switch item.content {
|
||||
case let .text(text):
|
||||
if item.isUnbreakable {
|
||||
itemText = [text]
|
||||
} else {
|
||||
itemText = text.map(String.init)
|
||||
}
|
||||
case let .number(value, minDigits):
|
||||
var valueText: String = "\(value)"
|
||||
while valueText.count < minDigits {
|
||||
valueText.insert("0", at: valueText.startIndex)
|
||||
}
|
||||
|
||||
if item.isUnbreakable {
|
||||
itemText = [valueText]
|
||||
} else {
|
||||
itemText = valueText.map(String.init)
|
||||
}
|
||||
}
|
||||
var index = 0
|
||||
for character in itemText {
|
||||
let characterKey = CharacterKey(itemId: item.id, index: index, value: character)
|
||||
index += 1
|
||||
|
||||
var characterTransition = transition
|
||||
let characterView: ComponentView<Empty>
|
||||
@ -155,7 +241,11 @@ public final class AnimatedTextComponent: Component {
|
||||
} else {
|
||||
var delayWidth: Double = 0.0
|
||||
if let firstDelayWidth {
|
||||
delayWidth = size.width - firstDelayWidth
|
||||
if characterFrame.midX > characterComponentView.frame.midX {
|
||||
delayWidth = 0.0
|
||||
} else {
|
||||
delayWidth = abs(size.width - firstDelayWidth)
|
||||
}
|
||||
} else {
|
||||
firstDelayWidth = size.width
|
||||
}
|
||||
@ -170,7 +260,7 @@ public final class AnimatedTextComponent: Component {
|
||||
|
||||
if animateIn, !transition.animation.isImmediate {
|
||||
var delayWidth: Double = 0.0
|
||||
if !component.noDelay {
|
||||
if !component.noDelay || component.preferredDirectionIsDown {
|
||||
if let firstDelayWidth {
|
||||
delayWidth = size.width - firstDelayWidth
|
||||
} else {
|
||||
@ -181,7 +271,10 @@ public final class AnimatedTextComponent: Component {
|
||||
if component.animateScale {
|
||||
characterComponentView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
characterComponentView.layer.animatePosition(from: CGPoint(x: 0.0, y: characterSize.height * 0.5), to: CGPoint(), duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
if component.blur {
|
||||
ComponentTransition.easeInOut(duration: 0.2).animateBlur(layer: characterComponentView.layer, from: transitionBlurRadius, to: 0.0, delay: delayNorm * delayWidth)
|
||||
}
|
||||
characterComponentView.layer.animatePosition(from: CGPoint(x: 0.0, y: characterSize.height * offsetNorm), to: CGPoint(), duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
characterComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18, delay: delayNorm * delayWidth)
|
||||
}
|
||||
}
|
||||
@ -194,8 +287,6 @@ public final class AnimatedTextComponent: Component {
|
||||
let outScaleTransition: ComponentTransition = .spring(duration: 0.4)
|
||||
let outAlphaTransition: ComponentTransition = .easeInOut(duration: 0.18)
|
||||
|
||||
var outFirstDelayWidth: CGFloat?
|
||||
|
||||
var removedKeys: [CharacterKey] = []
|
||||
for (key, characterView) in self.characters {
|
||||
if !validKeys.contains(key) {
|
||||
@ -205,18 +296,28 @@ public final class AnimatedTextComponent: Component {
|
||||
if !transition.animation.isImmediate {
|
||||
var delayWidth: Double = 0.0
|
||||
if let outFirstDelayWidth {
|
||||
delayWidth = characterComponentView.frame.minX - outFirstDelayWidth
|
||||
delayWidth = abs(characterComponentView.frame.midX - outFirstDelayWidth)
|
||||
} else {
|
||||
outFirstDelayWidth = characterComponentView.frame.minX
|
||||
outFirstDelayWidth = characterComponentView.frame.midX
|
||||
}
|
||||
delayWidth = max(0.0, delayWidth)
|
||||
|
||||
if component.animateScale {
|
||||
outScaleTransition.setScale(view: characterComponentView, scale: 0.01, delay: delayNorm * delayWidth)
|
||||
}
|
||||
outScaleTransition.setPosition(view: characterComponentView, position: CGPoint(x: characterComponentView.center.x, y: characterComponentView.center.y - characterComponentView.bounds.height * 0.4), delay: delayNorm * delayWidth)
|
||||
let targetY: CGFloat
|
||||
if component.preferredDirectionIsDown {
|
||||
targetY = characterComponentView.center.y - characterComponentView.bounds.height * offsetNorm
|
||||
} else {
|
||||
targetY = characterComponentView.center.y - characterComponentView.bounds.height * offsetNorm
|
||||
}
|
||||
outScaleTransition.setPosition(view: characterComponentView, position: CGPoint(x: characterComponentView.center.x, y: targetY), delay: delayNorm * delayWidth)
|
||||
outAlphaTransition.setAlpha(view: characterComponentView, alpha: 0.0, delay: delayNorm * delayWidth, completion: { [weak characterComponentView] _ in
|
||||
characterComponentView?.removeFromSuperview()
|
||||
})
|
||||
if component.blur {
|
||||
outAlphaTransition.animateBlur(layer: characterComponentView.layer, from: 0.0, to: transitionBlurRadius, delay: delayNorm * delayWidth, removeOnCompletion: false)
|
||||
}
|
||||
} else {
|
||||
characterComponentView.removeFromSuperview()
|
||||
}
|
||||
|
||||
@ -338,6 +338,10 @@ public final class PeerInfoRatingComponent: Component {
|
||||
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
if level < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if let url = Bundle.main.url(forResource: "profile_level\(levelIndex)_outer", withExtension: "svg"), let data = try? Data(contentsOf: url) {
|
||||
if let image = generateTintedImage(image: drawSvgImage(data, size, nil, nil, 0.0, false), color: component.borderColor) {
|
||||
image.draw(in: CGRect(origin: CGPoint(x: 0.0, y: backgroundOffsetsY[levelIndex] ?? 0.0), size: size), blendMode: .normal, alpha: 1.0)
|
||||
@ -360,6 +364,13 @@ public final class PeerInfoRatingComponent: Component {
|
||||
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
if level < 0 {
|
||||
if let image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/InlineRatingWarning"), color: component.backgroundColor) {
|
||||
image.draw(in: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - image.size.width) * 0.5), y: floorToScreenPixels((size.height - image.size.height) * 0.5)), size: image.size))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let url = Bundle.main.url(forResource: "profile_level\(levelIndex)_inner", withExtension: "svg"), let data = try? Data(contentsOf: url) {
|
||||
if let image = generateTintedImage(image: drawSvgImage(data, size, nil, nil, 0.0, false), color: component.backgroundColor) {
|
||||
image.draw(in: CGRect(origin: CGPoint(x: 0.0, y: backgroundOffsetsY[levelIndex] ?? 0.0), size: size), blendMode: .normal, alpha: 1.0)
|
||||
|
||||
@ -781,6 +781,13 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
self.avatarClippingNode.clipsToBounds = true
|
||||
}
|
||||
|
||||
let accentRatingBackgroundColor: UIColor
|
||||
if let currentStarRating = self.currentStarRating, currentStarRating.level < 0 {
|
||||
accentRatingBackgroundColor = UIColor(rgb: 0xFF3B30)
|
||||
} else {
|
||||
accentRatingBackgroundColor = presentationData.theme.list.itemCheckColors.fillColor
|
||||
}
|
||||
|
||||
let ratingBackgroundColor: UIColor
|
||||
let ratingBorderColor: UIColor
|
||||
let ratingForegroundColor: UIColor
|
||||
@ -796,7 +803,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
|
||||
headerButtonBackgroundColor = collapsedHeaderButtonBackgroundColor
|
||||
|
||||
ratingBackgroundColor = presentationData.theme.list.itemCheckColors.fillColor
|
||||
ratingBackgroundColor = accentRatingBackgroundColor
|
||||
ratingBorderColor = .clear
|
||||
ratingForegroundColor = presentationData.theme.list.itemCheckColors.foregroundColor
|
||||
} else if self.isAvatarExpanded {
|
||||
@ -833,7 +840,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
|
||||
if let status = peer?.emojiStatus, case let .starGift(_, _, _, _, _, innerColor, outerColor, patternColorValue, _) = status.content {
|
||||
let _ = innerColor
|
||||
ratingBackgroundColor = UIColor(white: 1.0, alpha: 1.0).mixedWith(presentationData.theme.list.itemCheckColors.fillColor, alpha: effectiveTransitionFraction)
|
||||
ratingBackgroundColor = UIColor(white: 1.0, alpha: 1.0).mixedWith(accentRatingBackgroundColor, alpha: effectiveTransitionFraction)
|
||||
|
||||
let innerColor = UIColor(rgb: UInt32(bitPattern: innerColor))
|
||||
let outerColor = UIColor(rgb: UInt32(bitPattern: outerColor))
|
||||
@ -855,29 +862,10 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
ratingBorderColor = patternColor.withAlphaComponent(0.1).blendOver(background: backgroundColor).mixedWith(.clear, alpha: effectiveTransitionFraction)
|
||||
ratingForegroundColor = ratingBorderColor.mixedWith(presentationData.theme.list.itemCheckColors.foregroundColor, alpha: effectiveTransitionFraction)
|
||||
} else {
|
||||
ratingBackgroundColor = presentationData.theme.list.itemCheckColors.fillColor
|
||||
ratingBackgroundColor = accentRatingBackgroundColor
|
||||
ratingBorderColor = UIColor.clear
|
||||
ratingForegroundColor = presentationData.theme.list.itemCheckColors.foregroundColor
|
||||
}
|
||||
|
||||
/*if let status = peer?.emojiStatus, case let .starGift(_, _, _, _, _, innerColor, outerColor, _, _) = status.content {
|
||||
let _ = outerColor
|
||||
let mainColor = UIColor(rgb: UInt32(bitPattern: innerColor))
|
||||
|
||||
ratingBackgroundColor = UIColor(white: 1.0, alpha: 1.0).mixedWith(presentationData.theme.list.itemCheckColors.fillColor, alpha: effectiveTransitionFraction)
|
||||
ratingForegroundColor = mainColor.withMultiplied(hue: 1.0, saturation: 1.1, brightness: 0.9).mixedWith(UIColor.clear, alpha: effectiveTransitionFraction)
|
||||
ratingBorderColor = ratingForegroundColor.mixedWith(presentationData.theme.list.itemCheckColors.foregroundColor, alpha: effectiveTransitionFraction)
|
||||
} else if let profileColor = peer?.profileColor {
|
||||
let backgroundColors = self.context.peerNameColors.getProfile(profileColor, dark: presentationData.theme.overallDarkAppearance)
|
||||
|
||||
ratingBackgroundColor = UIColor(white: 1.0, alpha: 1.0).mixedWith(presentationData.theme.list.itemCheckColors.fillColor, alpha: effectiveTransitionFraction)
|
||||
ratingForegroundColor = backgroundColors.main.withMultiplied(hue: 1.0, saturation: 1.1, brightness: 0.9).mixedWith(UIColor.clear, alpha: effectiveTransitionFraction)
|
||||
ratingBorderColor = ratingForegroundColor.mixedWith(presentationData.theme.list.itemCheckColors.foregroundColor, alpha: effectiveTransitionFraction)
|
||||
} else {
|
||||
ratingBackgroundColor = presentationData.theme.list.itemCheckColors.fillColor
|
||||
ratingBorderColor = UIColor.clear
|
||||
ratingForegroundColor = presentationData.theme.list.itemCheckColors.foregroundColor
|
||||
}*/
|
||||
}
|
||||
|
||||
do {
|
||||
@ -1986,20 +1974,30 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
if let cachedData = cachedData as? CachedUserData, let starRating = cachedData.starRating {
|
||||
self.currentStarRating = starRating
|
||||
self.currentPendingStarRating = cachedData.pendingStarRating
|
||||
|
||||
#if DEBUG
|
||||
if let _ = starRating.nextLevelStars {
|
||||
self.currentPendingStarRating = TelegramStarPendingRating(rating: TelegramStarRating(level: starRating.level, currentLevelStars: starRating.currentLevelStars, stars: starRating.stars + 234, nextLevelStars: starRating.nextLevelStars), timestamp: Int32(Date().timeIntervalSince1970) + 60 * 60 * 24 * 3)
|
||||
self.currentPendingStarRating = TelegramStarPendingRating(rating: TelegramStarRating(level: starRating.level + 1, currentLevelStars: starRating.nextLevelStars!, stars: starRating.nextLevelStars! + starRating.nextLevelStars! / 2 + starRating.nextLevelStars! / 4, nextLevelStars: starRating.nextLevelStars! * 2), timestamp: Int32(Date().timeIntervalSince1970) + 60 * 60 * 24 * 3)
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
self.currentStarRating = nil
|
||||
self.currentPendingStarRating = nil
|
||||
}
|
||||
|
||||
if let cachedData = cachedData as? CachedUserData, let starRating = cachedData.starRating {
|
||||
//if "".isEmpty {
|
||||
#if DEBUG
|
||||
if "".isEmpty {
|
||||
let starRating: TelegramStarRating
|
||||
|
||||
if self.context.account.peerId.id._internalGetInt64Value() == 654152421 {
|
||||
starRating = TelegramStarRating(level: -1, currentLevelStars: -1, stars: -100, nextLevelStars: 0)
|
||||
} else {
|
||||
starRating = TelegramStarRating(level: 2, currentLevelStars: 1000, stars: 2000, nextLevelStars: 3000)
|
||||
}
|
||||
self.currentStarRating = starRating
|
||||
|
||||
if let _ = starRating.nextLevelStars {
|
||||
//self.currentPendingStarRating = TelegramStarPendingRating(rating: TelegramStarRating(level: starRating.level, currentLevelStars: starRating.currentLevelStars, stars: starRating.stars + 234, nextLevelStars: starRating.nextLevelStars), timestamp: Int32(Date().timeIntervalSince1970) + 60 * 60 * 24 * 3)
|
||||
self.currentPendingStarRating = TelegramStarPendingRating(rating: TelegramStarRating(level: starRating.level + 2, currentLevelStars: starRating.nextLevelStars!, stars: max(500, starRating.nextLevelStars! + starRating.nextLevelStars! / 2 - starRating.nextLevelStars! / 4), nextLevelStars: max(1000, starRating.nextLevelStars! * 2)), timestamp: Int32(Date().timeIntervalSince1970) + 60 * 60 * 24 * 3)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if let starRating = self.currentStarRating {
|
||||
let subtitleRating: ComponentView<Empty>
|
||||
var subtitleRatingTransition = ComponentTransition(transition)
|
||||
if let current = self.subtitleRating {
|
||||
@ -2018,7 +2016,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
foregroundColor: ratingForegroundColor,
|
||||
level: Int(starRating.level),
|
||||
action: { [weak self] in
|
||||
guard let self, let peer, let currentStarRating = self.currentStarRating else {
|
||||
guard let self, let peer = self.peer, let currentStarRating = self.currentStarRating else {
|
||||
return
|
||||
}
|
||||
self.controller?.push(ProfileLevelInfoScreen(
|
||||
|
||||
@ -17,6 +17,7 @@ import PlainButtonComponent
|
||||
import Markdown
|
||||
import PremiumUI
|
||||
import LottieComponent
|
||||
import AnimatedTextComponent
|
||||
|
||||
private final class ProfileLevelInfoScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
@ -376,10 +377,38 @@ private final class ProfileLevelInfoScreenComponent: Component {
|
||||
descriptionTextString = environment.strings.ProfileLevelInfo_OtherDescription(component.peer.compactDisplayTitle).string
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
var titleItems: [AnimatedTextComponent.Item] = []
|
||||
if self.isPreviewingPendingRating {
|
||||
titleItems.append(AnimatedTextComponent.Item(
|
||||
id: AnyHashable(0),
|
||||
isUnbreakable: false,
|
||||
content: .text("Future ")
|
||||
))
|
||||
titleItems.append(AnimatedTextComponent.Item(
|
||||
id: AnyHashable(1),
|
||||
isUnbreakable: true,
|
||||
content: .text("Rating")
|
||||
))
|
||||
} else {
|
||||
titleItems.append(AnimatedTextComponent.Item(
|
||||
id: AnyHashable(1),
|
||||
isUnbreakable: true,
|
||||
content: .text("Rating")
|
||||
))
|
||||
}
|
||||
|
||||
let _ = titleString
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: titleString, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor))
|
||||
transition: transition,
|
||||
component: AnyComponent(AnimatedTextComponent(
|
||||
font: Font.semibold(17.0),
|
||||
color: environment.theme.list.itemPrimaryTextColor,
|
||||
items: titleItems,
|
||||
noDelay: true,
|
||||
animateScale: false,
|
||||
preferredDirectionIsDown: true,
|
||||
blur: true
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
|
||||
@ -389,7 +418,7 @@ private final class ProfileLevelInfoScreenComponent: Component {
|
||||
if titleView.superview == nil {
|
||||
self.navigationBarContainer.addSubview(titleView)
|
||||
}
|
||||
titleView.frame = titleFrame
|
||||
transition.setFrame(view: titleView, frame: titleFrame)
|
||||
}
|
||||
contentHeight += 56.0
|
||||
|
||||
@ -433,47 +462,25 @@ private final class ProfileLevelInfoScreenComponent: Component {
|
||||
if let nextLevelStars = component.starRating.nextLevelStars {
|
||||
badgeTextSuffix = " / \(starCountString(Int64(nextLevelStars), decimalSeparator: "."))"
|
||||
}
|
||||
if let nextLevelStars = component.starRating.nextLevelStars {
|
||||
if component.starRating.stars < 0 {
|
||||
levelFraction = 0.5
|
||||
} else if let nextLevelStars = component.starRating.nextLevelStars {
|
||||
levelFraction = Double(component.starRating.stars - component.starRating.currentLevelStars) / Double(nextLevelStars - component.starRating.currentLevelStars)
|
||||
} else if component.starRating.stars > 0 {
|
||||
levelFraction = 1.0
|
||||
} else {
|
||||
levelFraction = 0.0
|
||||
levelFraction = 1.0
|
||||
}
|
||||
}
|
||||
|
||||
levelFraction = max(0.0, levelFraction)
|
||||
|
||||
/*let levelInfoSize = self.levelInfo.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(PremiumLimitDisplayComponent(
|
||||
inactiveColor: environment.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.5),
|
||||
activeColors: gradientColors,
|
||||
inactiveTitle: environment.strings.ProfileLevelInfo_LevelIndex(Int32(currentLevel)),
|
||||
inactiveValue: "",
|
||||
inactiveTitleColor: environment.theme.list.itemPrimaryTextColor,
|
||||
activeTitle: "",
|
||||
activeValue: nextLevel.flatMap { environment.strings.ProfileLevelInfo_LevelIndex(Int32($0)) } ?? "",
|
||||
activeTitleColor: .white,
|
||||
badgeIconName: "Peer Info/ProfileLevelProgressIcon",
|
||||
badgeText: badgeText,
|
||||
badgeTextSuffix: badgeTextSuffix,
|
||||
badgePosition: levelFraction,
|
||||
badgeGraphPosition: levelFraction,
|
||||
invertProgress: true,
|
||||
isPremiumDisabled: false
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 200.0)
|
||||
)*/
|
||||
let _ = levelFraction
|
||||
|
||||
//TODO:localize
|
||||
let levelInfoSize = self.levelInfo.update(
|
||||
transition: isChangingPreview ? ComponentTransition.immediate.withUserData(ProfileLevelRatingBarComponent.TransitionHint(animate: true)) : .immediate,
|
||||
component: AnyComponent(ProfileLevelRatingBarComponent(
|
||||
theme: environment.theme,
|
||||
value: levelFraction,
|
||||
leftLabel: environment.strings.ProfileLevelInfo_LevelIndex(Int32(currentLevel)),
|
||||
rightLabel: nextLevel.flatMap { environment.strings.ProfileLevelInfo_LevelIndex(Int32($0)) } ?? "",
|
||||
leftLabel: currentLevel < 0 ? "" : environment.strings.ProfileLevelInfo_LevelIndex(Int32(currentLevel)),
|
||||
rightLabel: currentLevel < 0 ? "Negative rating" : nextLevel.flatMap { environment.strings.ProfileLevelInfo_LevelIndex(Int32($0)) } ?? "",
|
||||
badgeValue: badgeText,
|
||||
badgeTotal: badgeTextSuffix,
|
||||
level: Int(currentLevel)
|
||||
@ -491,12 +498,21 @@ private final class ProfileLevelInfoScreenComponent: Component {
|
||||
contentHeight += 129.0
|
||||
|
||||
if let secondaryDescriptionTextString {
|
||||
let changingPreviewAnimationOffset: CGFloat = self.isPreviewingPendingRating ? -100.0 : 100.0
|
||||
let transitionBlurRadius: CGFloat = 10.0
|
||||
|
||||
if isChangingPreview, let secondaryDescriptionTextView = self.secondaryDescriptionText?.view {
|
||||
self.secondaryDescriptionText = nil
|
||||
transition.setTransform(view: secondaryDescriptionTextView, transform: CATransform3DMakeScale(0.9, 0.9, 1.0))
|
||||
transition.setTransform(view: secondaryDescriptionTextView, transform: CATransform3DMakeTranslation(changingPreviewAnimationOffset, 0.0, 0.0))
|
||||
alphaTransition.setAlpha(view: secondaryDescriptionTextView, alpha: 0.0, completion: { [weak secondaryDescriptionTextView] _ in
|
||||
secondaryDescriptionTextView?.removeFromSuperview()
|
||||
})
|
||||
|
||||
if let blurFilter = CALayer.blur() {
|
||||
blurFilter.setValue(transitionBlurRadius as NSNumber, forKey: "inputRadius")
|
||||
secondaryDescriptionTextView.layer.filters = [blurFilter]
|
||||
secondaryDescriptionTextView.layer.animate(from: 0.0 as NSNumber, to: transitionBlurRadius as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.3, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
|
||||
contentHeight -= 8.0
|
||||
@ -510,9 +526,16 @@ private final class ProfileLevelInfoScreenComponent: Component {
|
||||
self.secondaryDescriptionText = secondaryDescriptionText
|
||||
}
|
||||
|
||||
let secondaryTextColor: UIColor
|
||||
if currentLevel < 0 {
|
||||
secondaryTextColor = UIColor(rgb: 0xFF3B30)
|
||||
} else {
|
||||
secondaryTextColor = environment.theme.list.itemSecondaryTextColor
|
||||
}
|
||||
|
||||
let secondaryDescriptionAttributedString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(secondaryDescriptionTextString, attributes: MarkdownAttributes(
|
||||
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor),
|
||||
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.itemSecondaryTextColor),
|
||||
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: secondaryTextColor),
|
||||
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: secondaryTextColor),
|
||||
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor),
|
||||
linkAttribute: { url in
|
||||
return ("URL", url)
|
||||
@ -563,8 +586,16 @@ private final class ProfileLevelInfoScreenComponent: Component {
|
||||
if secondaryDescriptionTextView.superview == nil {
|
||||
self.scrollContentView.addSubview(secondaryDescriptionTextView)
|
||||
if isChangingPreview {
|
||||
transition.animateScale(view: secondaryDescriptionTextView, from: 0.9, to: 1.0)
|
||||
transition.animatePosition(view: secondaryDescriptionTextView, from: CGPoint(x: -changingPreviewAnimationOffset, y: 0.0), to: CGPoint(), additive: true)
|
||||
alphaTransition.animateAlpha(view: secondaryDescriptionTextView, from: 0.0, to: 1.0)
|
||||
|
||||
if let blurFilter = CALayer.blur() {
|
||||
blurFilter.setValue(transitionBlurRadius as NSNumber, forKey: "inputRadius")
|
||||
secondaryDescriptionTextView.layer.filters = [blurFilter]
|
||||
secondaryDescriptionTextView.layer.animate(from: transitionBlurRadius as NSNumber, to: 0.0 as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.3, removeOnCompletion: false, completion: { [weak secondaryDescriptionTextView] _ in
|
||||
secondaryDescriptionTextView?.layer.filters = nil
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
secondaryDescriptionTextTransition.setPosition(view: secondaryDescriptionTextView, position: secondaryDescriptionTextFrame.center)
|
||||
|
||||
@ -47,11 +47,12 @@ final class ProfileLevelRatingBarBadge: Component {
|
||||
final class View: UIView {
|
||||
private let badgeView: UIView
|
||||
private let badgeMaskView: UIView
|
||||
private let badgeShapeLayer = SimpleShapeLayer()
|
||||
private let badgeShapeView: UIImageView
|
||||
private let badgeShapeAnimation = ComponentView<Empty>()
|
||||
|
||||
private let badgeForeground: SimpleLayer
|
||||
let badgeIcon: UIImageView
|
||||
private let badgeIcon: UIImageView
|
||||
private var disappearingBadgeIcon: UIImageView?
|
||||
private let badgeLabel = ComponentView<Empty>()
|
||||
private let suffixLabel = ComponentView<Empty>()
|
||||
|
||||
@ -68,11 +69,10 @@ final class ProfileLevelRatingBarBadge: Component {
|
||||
self.badgeView.alpha = 0.0
|
||||
self.badgeView.layer.anchorPoint = CGPoint()
|
||||
|
||||
self.badgeShapeLayer.fillColor = UIColor.white.cgColor
|
||||
self.badgeShapeLayer.transform = CATransform3DMakeScale(1.0, -1.0, 1.0)
|
||||
self.badgeShapeView = UIImageView()
|
||||
|
||||
self.badgeMaskView = UIView()
|
||||
self.badgeMaskView.layer.addSublayer(self.badgeShapeLayer)
|
||||
self.badgeMaskView.addSubview(self.badgeShapeView)
|
||||
self.badgeView.mask = self.badgeMaskView
|
||||
|
||||
self.badgeForeground = SimpleLayer()
|
||||
@ -108,17 +108,49 @@ final class ProfileLevelRatingBarBadge: Component {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
if self.component == nil {
|
||||
self.badgeIcon.image = UIImage(bundleImageName: "Peer Info/ProfileLevelProgressIcon")?.withRenderingMode(.alwaysTemplate)
|
||||
var labelsTransition = transition
|
||||
if let hint = transition.userData(TransitionHint.self), hint.animateText {
|
||||
labelsTransition = .spring(duration: 0.5)
|
||||
}
|
||||
|
||||
let previousComponent = self.component
|
||||
if previousComponent == nil || (previousComponent?.title == "") != (component.title == "") {
|
||||
if !labelsTransition.animation.isImmediate, self.badgeIcon.image != nil {
|
||||
if let disappearingBadgeIcon = self.disappearingBadgeIcon {
|
||||
self.disappearingBadgeIcon = nil
|
||||
disappearingBadgeIcon.removeFromSuperview()
|
||||
}
|
||||
let disappearingBadgeIcon = UIImageView()
|
||||
disappearingBadgeIcon.contentMode = self.badgeIcon.contentMode
|
||||
disappearingBadgeIcon.frame = self.badgeIcon.frame
|
||||
disappearingBadgeIcon.image = self.badgeIcon.image
|
||||
disappearingBadgeIcon.tintColor = self.badgeIcon.tintColor
|
||||
|
||||
self.badgeView.insertSubview(disappearingBadgeIcon, aboveSubview: self.badgeIcon)
|
||||
labelsTransition.setScale(view: disappearingBadgeIcon, scale: 0.001)
|
||||
labelsTransition.setAlpha(view: disappearingBadgeIcon, alpha: 0.0, completion: { [weak self, weak disappearingBadgeIcon] _ in
|
||||
guard let self, let disappearingBadgeIcon else {
|
||||
return
|
||||
}
|
||||
disappearingBadgeIcon.removeFromSuperview()
|
||||
if self.disappearingBadgeIcon === disappearingBadgeIcon {
|
||||
self.disappearingBadgeIcon = nil
|
||||
}
|
||||
})
|
||||
labelsTransition.animateScale(view: self.badgeIcon, from: 0.001, to: 1.0)
|
||||
labelsTransition.animateAlpha(view: self.badgeIcon, from: 0.0, to: 1.0)
|
||||
}
|
||||
|
||||
if component.title.isEmpty {
|
||||
self.badgeIcon.image = UIImage(bundleImageName: "Peer Info/ProfileLevelWarningIcon")?.withRenderingMode(.alwaysTemplate)
|
||||
} else {
|
||||
self.badgeIcon.image = UIImage(bundleImageName: "Peer Info/ProfileLevelProgressIcon")?.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.badgeIcon.tintColor = component.theme.list.itemCheckColors.foregroundColor
|
||||
|
||||
var labelsTransition = transition
|
||||
if let hint = transition.userData(TransitionHint.self), hint.animateText {
|
||||
labelsTransition = .spring(duration: 0.4)
|
||||
}
|
||||
self.disappearingBadgeIcon?.tintColor = component.theme.list.itemCheckColors.foregroundColor
|
||||
|
||||
let badgeLabelSize = self.badgeLabel.update(
|
||||
transition: labelsTransition,
|
||||
@ -150,7 +182,12 @@ final class ProfileLevelRatingBarBadge: Component {
|
||||
containerSize: CGSize(width: 300.0, height: 100.0)
|
||||
)
|
||||
|
||||
var badgeWidth: CGFloat = badgeLabelSize.width + 3.0 + 60.0
|
||||
var badgeWidth: CGFloat = 0.0
|
||||
if !component.title.isEmpty {
|
||||
badgeWidth += badgeLabelSize.width + 3.0 + 60.0
|
||||
} else {
|
||||
badgeWidth += 78.0
|
||||
}
|
||||
if component.suffix != nil {
|
||||
badgeWidth += badgeSuffixSize.width + badgeSuffixSpacing
|
||||
}
|
||||
@ -158,13 +195,21 @@ final class ProfileLevelRatingBarBadge: Component {
|
||||
let badgeSize = CGSize(width: badgeWidth, height: 48.0)
|
||||
let badgeFullSize = CGSize(width: badgeWidth, height: 48.0 + 12.0)
|
||||
self.badgeMaskView.frame = CGRect(origin: .zero, size: badgeFullSize)
|
||||
self.badgeShapeLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -4.0), size: badgeFullSize)
|
||||
|
||||
self.badgeView.bounds = CGRect(origin: .zero, size: badgeFullSize)
|
||||
|
||||
self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: 600.0, height: badgeFullSize.height + 10.0))
|
||||
|
||||
self.badgeIcon.frame = CGRect(x: 13.0, y: 8.0, width: 30.0, height: 30.0)
|
||||
let badgeIconFrame: CGRect
|
||||
if !component.title.isEmpty {
|
||||
badgeIconFrame = CGRect(x: 13.0, y: 8.0, width: 30.0, height: 30.0)
|
||||
} else {
|
||||
badgeIconFrame = CGRect(x: floor((badgeWidth - 30.0) * 0.5), y: 8.0, width: 30.0, height: 30.0)
|
||||
}
|
||||
labelsTransition.setFrame(view: self.badgeIcon, frame: badgeIconFrame)
|
||||
if let disappearingBadgeIcon = self.disappearingBadgeIcon {
|
||||
labelsTransition.setFrame(view: disappearingBadgeIcon, frame: badgeIconFrame)
|
||||
}
|
||||
|
||||
self.badgeView.alpha = 1.0
|
||||
|
||||
@ -194,77 +239,63 @@ final class ProfileLevelRatingBarBadge: Component {
|
||||
|
||||
if self.previousAvailableSize != availableSize {
|
||||
self.previousAvailableSize = availableSize
|
||||
|
||||
let activeColors: [UIColor] = [
|
||||
component.theme.list.itemCheckColors.fillColor,
|
||||
component.theme.list.itemCheckColors.fillColor
|
||||
]
|
||||
|
||||
var locations: [CGFloat] = []
|
||||
let delta = 1.0 / CGFloat(activeColors.count - 1)
|
||||
for i in 0 ..< activeColors.count {
|
||||
locations.append(delta * CGFloat(i))
|
||||
}
|
||||
|
||||
let gradient = generateGradientImage(size: CGSize(width: 200.0, height: 60.0), colors: activeColors, locations: locations, direction: .horizontal)
|
||||
self.badgeForeground.contentsGravity = .resizeAspectFill
|
||||
self.badgeForeground.contents = gradient?.cgImage
|
||||
|
||||
self.setupGradientAnimations()
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
func adjustTail(size: CGSize, overflowWidth: CGFloat, transition: ComponentTransition) {
|
||||
guard let component else {
|
||||
return
|
||||
func updateColors(background: UIColor) {
|
||||
self.badgeForeground.backgroundColor = background.cgColor
|
||||
}
|
||||
|
||||
func adjustTail(size: CGSize, tailOffset: CGFloat, transition: ComponentTransition) {
|
||||
if self.badgeShapeView.image == nil {
|
||||
self.badgeShapeView.image = generateStretchableFilledCircleImage(diameter: 48.0, color: UIColor.white)
|
||||
}
|
||||
let _ = component
|
||||
self.badgeShapeView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: 48.0))
|
||||
|
||||
var tailPosition = size.width * 0.5
|
||||
tailPosition += overflowWidth
|
||||
tailPosition = max(36.0, min(size.width - 36.0, tailPosition))
|
||||
|
||||
let tailPositionFraction = tailPosition / size.width
|
||||
transition.setShapeLayerPath(layer: self.badgeShapeLayer, path: generateRoundedRectWithTailPath(rectSize: size, tailPosition: tailPositionFraction, transformTail: false).cgPath)
|
||||
|
||||
let badgeShapeSize = CGSize(width: 128, height: 128)
|
||||
let badgeShapeSize = CGSize(width: 78, height: 60)
|
||||
let _ = self.badgeShapeAnimation.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(LottieComponent(
|
||||
content: LottieComponent.AppBundleContent(name: "badge_with_tail"),
|
||||
color: .red,//component.theme.list.itemCheckColors.fillColor,
|
||||
color: .white,
|
||||
placeholderColor: nil,
|
||||
startingPosition: .begin,
|
||||
size: badgeShapeSize,
|
||||
renderingScale: nil,
|
||||
renderingScale: UIScreenScale,
|
||||
loop: false,
|
||||
playOnce: nil
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: badgeShapeSize
|
||||
)
|
||||
if let badgeShapeAnimationView = self.badgeShapeAnimation.view as? LottieComponent.View, !"".isEmpty {
|
||||
if let badgeShapeAnimationView = self.badgeShapeAnimation.view as? LottieComponent.View {
|
||||
if badgeShapeAnimationView.superview == nil {
|
||||
badgeShapeAnimationView.layer.anchorPoint = CGPoint()
|
||||
self.addSubview(badgeShapeAnimationView)
|
||||
self.badgeMaskView.addSubview(badgeShapeAnimationView)
|
||||
}
|
||||
|
||||
let transition: ComponentTransition = .immediate
|
||||
var shapeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: badgeShapeSize)
|
||||
|
||||
let badgeShapeWidth = badgeShapeSize.width
|
||||
|
||||
let midFrame = 359 / 2
|
||||
if tailOffset < badgeShapeWidth * 0.5 {
|
||||
let frameIndex = Int(floor(CGFloat(midFrame) * tailOffset / (badgeShapeWidth * 0.5)))
|
||||
badgeShapeAnimationView.setFrameIndex(index: frameIndex)
|
||||
} else if tailOffset >= size.width - badgeShapeWidth * 0.5 {
|
||||
let endOffset = tailOffset - (size.width - badgeShapeWidth * 0.5)
|
||||
let frameIndex = midFrame + Int(floor(CGFloat(359 - midFrame) * endOffset / (badgeShapeWidth * 0.5)))
|
||||
badgeShapeAnimationView.setFrameIndex(index: frameIndex)
|
||||
shapeFrame.origin.x = size.width - badgeShapeWidth
|
||||
} else {
|
||||
badgeShapeAnimationView.setFrameIndex(index: midFrame)
|
||||
shapeFrame.origin.x = tailOffset - badgeShapeWidth * 0.5
|
||||
}
|
||||
|
||||
let shapeFrame = CGRect(origin: CGPoint(x: 0.0, y: -10.0), size: badgeShapeSize)
|
||||
badgeShapeAnimationView.center = shapeFrame.origin
|
||||
badgeShapeAnimationView.bounds = CGRect(origin: CGPoint(), size: shapeFrame.size)
|
||||
|
||||
let scaleFactor: CGFloat = 144.0 / (946.0 / 4.0)
|
||||
transition.setScale(view: badgeShapeAnimationView, scale: scaleFactor)
|
||||
|
||||
let badgeShapeWidth = floor(shapeFrame.width * scaleFactor)
|
||||
let badgeShapeOffset = -overflowWidth / badgeShapeWidth
|
||||
let _ = badgeShapeOffset
|
||||
|
||||
//badgeShapeAnimationView.setFrameIndex(index: 0)
|
||||
}
|
||||
|
||||
//let transition: ContainedViewLayoutTransition = .immediate
|
||||
@ -275,38 +306,6 @@ final class ProfileLevelRatingBarBadge: Component {
|
||||
let transition: ContainedViewLayoutTransition = .immediate
|
||||
transition.updateTransformRotation(view: self.badgeView, angle: angle)
|
||||
}
|
||||
|
||||
private func setupGradientAnimations() {
|
||||
guard let _ = self.component else {
|
||||
return
|
||||
}
|
||||
if let _ = self.badgeForeground.animation(forKey: "movement") {
|
||||
} else {
|
||||
CATransaction.begin()
|
||||
|
||||
let badgePreviousValue = self.badgeForeground.position.x
|
||||
let badgeNewValue: CGFloat
|
||||
if self.badgeForeground.position.x == -300.0 {
|
||||
badgeNewValue = 0.0
|
||||
} else {
|
||||
badgeNewValue = -300.0
|
||||
}
|
||||
self.badgeForeground.position = CGPoint(x: badgeNewValue, y: 0.0)
|
||||
|
||||
let badgeAnimation = CABasicAnimation(keyPath: "position.x")
|
||||
badgeAnimation.duration = 4.5
|
||||
badgeAnimation.fromValue = badgePreviousValue
|
||||
badgeAnimation.toValue = badgeNewValue
|
||||
badgeAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
||||
|
||||
CATransaction.setCompletionBlock { [weak self] in
|
||||
self?.setupGradientAnimations()
|
||||
}
|
||||
self.badgeForeground.add(badgeAnimation, forKey: "movement")
|
||||
|
||||
CATransaction.commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
|
||||
@ -6,7 +6,6 @@ import ComponentFlow
|
||||
import MultilineTextComponent
|
||||
import BundleIconComponent
|
||||
import HierarchyTrackingLayer
|
||||
import AnimatedTextComponent
|
||||
|
||||
final class ProfileLevelRatingBarComponent: Component {
|
||||
final class TransitionHint {
|
||||
@ -69,20 +68,33 @@ final class ProfileLevelRatingBarComponent: Component {
|
||||
}
|
||||
|
||||
private final class AnimationState {
|
||||
enum Wraparound {
|
||||
case left
|
||||
case right
|
||||
}
|
||||
|
||||
let fromLevel: Int
|
||||
let toLevel: Int
|
||||
let fromLeftLabelText: String
|
||||
let fromRightLabelText: String
|
||||
let fromValue: CGFloat
|
||||
let toValue: CGFloat
|
||||
let fromBadgeSize: CGSize
|
||||
let startTime: Double
|
||||
let duration: Double
|
||||
let isWraparound: Bool
|
||||
let wraparound: Wraparound?
|
||||
|
||||
init(fromValue: CGFloat, toValue: CGFloat, fromBadgeSize: CGSize, startTime: Double, duration: Double, isWraparound: Bool) {
|
||||
init(fromLevel: Int, toLevel: Int, fromLeftLabelText: String, fromRightLabelText: String, fromValue: CGFloat, toValue: CGFloat, fromBadgeSize: CGSize, startTime: Double, duration: Double, wraparound: Wraparound?) {
|
||||
self.fromLevel = fromLevel
|
||||
self.toLevel = toLevel
|
||||
self.fromLeftLabelText = fromLeftLabelText
|
||||
self.fromRightLabelText = fromRightLabelText
|
||||
self.fromValue = fromValue
|
||||
self.toValue = toValue
|
||||
self.fromBadgeSize = fromBadgeSize
|
||||
self.startTime = startTime
|
||||
self.duration = duration
|
||||
self.isWraparound = isWraparound
|
||||
self.wraparound = wraparound
|
||||
}
|
||||
|
||||
func timeFraction(at timestamp: Double) -> CGFloat {
|
||||
@ -91,8 +103,30 @@ final class ProfileLevelRatingBarComponent: Component {
|
||||
return fraction
|
||||
}
|
||||
|
||||
func stepFraction(at timestamp: Double) -> (step: Int, fraction: CGFloat) {
|
||||
if self.wraparound != nil {
|
||||
var t = self.timeFraction(at: timestamp)
|
||||
t = bezierPoint(0.6, 0.0, 0.4, 1.0, t)
|
||||
if t < 0.5 {
|
||||
let vt = t / 0.5
|
||||
return (0, vt)
|
||||
} else {
|
||||
let vt = (t - 0.5) / 0.5
|
||||
return (1, vt)
|
||||
}
|
||||
} else {
|
||||
let t = self.timeFraction(at: timestamp)
|
||||
return (0, listViewAnimationCurveSystem(t))
|
||||
}
|
||||
}
|
||||
|
||||
func fraction(at timestamp: Double) -> CGFloat {
|
||||
return listViewAnimationCurveSystem(self.timeFraction(at: timestamp))
|
||||
let t = self.timeFraction(at: timestamp)
|
||||
if self.wraparound != nil {
|
||||
return listViewAnimationCurveEaseInOut(t)
|
||||
} else {
|
||||
return listViewAnimationCurveSystem(t)
|
||||
}
|
||||
}
|
||||
|
||||
func value(at timestamp: Double) -> CGFloat {
|
||||
@ -100,14 +134,12 @@ final class ProfileLevelRatingBarComponent: Component {
|
||||
return (1.0 - fraction) * self.fromValue + fraction * self.toValue
|
||||
}
|
||||
|
||||
func wrapAroundValue(at timestamp: Double, topValue: CGFloat) -> CGFloat {
|
||||
let fraction = self.fraction(at: timestamp)
|
||||
if fraction <= 0.5 {
|
||||
let halfFraction = fraction / 0.5
|
||||
return (1.0 - halfFraction) * self.fromValue + halfFraction * topValue
|
||||
func wrapAroundValue(at timestamp: Double, bottomValue: CGFloat, topValue: CGFloat) -> CGFloat {
|
||||
let (step, fraction) = self.stepFraction(at: timestamp)
|
||||
if step == 0 {
|
||||
return (1.0 - fraction) * self.fromValue + fraction * topValue
|
||||
} else {
|
||||
let halfFraction = (fraction - 0.5) / 0.5
|
||||
return halfFraction * self.toValue
|
||||
return (1.0 - fraction) * bottomValue + fraction * self.toValue
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,6 +155,7 @@ final class ProfileLevelRatingBarComponent: Component {
|
||||
final class View: UIView {
|
||||
private let barBackground: UIImageView
|
||||
private let backgroundClippingContainer: UIView
|
||||
private let foregroundBarClippingContainer: UIView
|
||||
private let foregroundClippingContainer: UIView
|
||||
private let barForeground: UIImageView
|
||||
|
||||
@ -139,13 +172,29 @@ final class ProfileLevelRatingBarComponent: Component {
|
||||
|
||||
private var hierarchyTracker: HierarchyTrackingLayer?
|
||||
private var animationLink: SharedDisplayLinkDriver.Link?
|
||||
private var badgePhysicsLink: SharedDisplayLinkDriver.Link?
|
||||
|
||||
private var animationState: AnimationState?
|
||||
|
||||
private var previousAnimationTimestamp: Double?
|
||||
private var previousAnimationTimeFraction: CGFloat?
|
||||
private var animationDeltaTime: Double?
|
||||
private var animationIsMovingOverStep: Bool = false
|
||||
|
||||
private var badgeAngularSpeed: CGFloat = 0.0
|
||||
private var badgeScale: CGFloat = 1.0
|
||||
private var badgeAngle: CGFloat = 0.0
|
||||
private var previousPhysicsTimestamp: Double?
|
||||
|
||||
private var testFraction: CGFloat?
|
||||
private var startTestFraction: CGFloat?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.barBackground = UIImageView()
|
||||
self.backgroundClippingContainer = UIView()
|
||||
self.backgroundClippingContainer.clipsToBounds = true
|
||||
self.foregroundBarClippingContainer = UIView()
|
||||
self.foregroundBarClippingContainer.clipsToBounds = true
|
||||
self.foregroundClippingContainer = UIView()
|
||||
self.foregroundClippingContainer.clipsToBounds = true
|
||||
self.barForeground = UIImageView()
|
||||
@ -161,7 +210,28 @@ final class ProfileLevelRatingBarComponent: Component {
|
||||
return
|
||||
}
|
||||
self.updateAnimations()
|
||||
|
||||
if value {
|
||||
if self.badgePhysicsLink == nil {
|
||||
let badgePhysicsLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.updateBadgePhysics()
|
||||
})
|
||||
self.badgePhysicsLink = badgePhysicsLink
|
||||
}
|
||||
} else {
|
||||
if let badgePhysicsLink = self.badgePhysicsLink {
|
||||
self.badgePhysicsLink = nil
|
||||
badgePhysicsLink.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.onPanGesture(_:))))
|
||||
#endif
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -171,7 +241,40 @@ final class ProfileLevelRatingBarComponent: Component {
|
||||
deinit {
|
||||
}
|
||||
|
||||
@objc private func onPanGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .began, .changed:
|
||||
if self.testFraction == nil {
|
||||
self.testFraction = self.component?.value
|
||||
}
|
||||
if self.startTestFraction == nil {
|
||||
if let testFraction = self.testFraction {
|
||||
self.startTestFraction = testFraction
|
||||
}
|
||||
}
|
||||
if let startTestFraction = self.startTestFraction {
|
||||
let x = recognizer.translation(in: self).x
|
||||
var value: CGFloat = startTestFraction + x / self.bounds.width
|
||||
value = max(0.0, min(1.0, value))
|
||||
self.testFraction = value
|
||||
self.state?.updated(transition: .immediate, isLocal: true)
|
||||
}
|
||||
case .ended, .cancelled:
|
||||
self.startTestFraction = nil
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func updateAnimations() {
|
||||
let timestamp = CACurrentMediaTime()
|
||||
let deltaTime: CGFloat
|
||||
if let previousAnimationTimestamp = self.previousAnimationTimestamp {
|
||||
deltaTime = min(0.2, timestamp - previousAnimationTimestamp)
|
||||
} else {
|
||||
deltaTime = 1.0 / 60.0
|
||||
}
|
||||
|
||||
if let hierarchyTracker = self.hierarchyTracker, hierarchyTracker.isInHierarchy {
|
||||
if self.animationState != nil {
|
||||
if self.animationLink == nil {
|
||||
@ -194,15 +297,104 @@ final class ProfileLevelRatingBarComponent: Component {
|
||||
}
|
||||
|
||||
if let animationState = self.animationState {
|
||||
if animationState.timeFraction(at: CACurrentMediaTime()) >= 1.0 {
|
||||
let timeFraction = animationState.timeFraction(at: timestamp)
|
||||
if timeFraction >= 1.0 {
|
||||
self.animationState = nil
|
||||
self.updateAnimations()
|
||||
return
|
||||
} else {
|
||||
if let previousAnimationTimeFraction = self.previousAnimationTimeFraction {
|
||||
if previousAnimationTimeFraction < 0.5 && timeFraction >= 0.5 {
|
||||
self.animationIsMovingOverStep = true
|
||||
}
|
||||
}
|
||||
self.previousAnimationTimeFraction = timeFraction
|
||||
}
|
||||
} else {
|
||||
self.previousAnimationTimeFraction = nil
|
||||
}
|
||||
|
||||
self.animationDeltaTime = Double(deltaTime)
|
||||
|
||||
if self.animationState != nil && !self.isUpdating {
|
||||
self.state?.updated(transition: .immediate, isLocal: true)
|
||||
}
|
||||
|
||||
self.animationDeltaTime = nil
|
||||
self.animationIsMovingOverStep = false
|
||||
}
|
||||
|
||||
private func addBadgeDeltaX(value: CGFloat, deltaTime: CGFloat) {
|
||||
var deltaTime = deltaTime
|
||||
deltaTime /= UIView.animationDurationFactor()
|
||||
let horizontalVelocity = value / deltaTime
|
||||
var badgeAngle = self.badgeAngle
|
||||
badgeAngle -= horizontalVelocity * 0.00005
|
||||
let maxAngle: CGFloat = 0.1
|
||||
if abs(badgeAngle) > maxAngle {
|
||||
badgeAngle = badgeAngle < 0.0 ? -maxAngle : maxAngle
|
||||
}
|
||||
self.badgeAngle = badgeAngle
|
||||
}
|
||||
|
||||
private func updateBadgePhysics() {
|
||||
let timestamp = CACurrentMediaTime()
|
||||
|
||||
var deltaTime: CGFloat
|
||||
if let previousPhysicsTimestamp = self.previousPhysicsTimestamp {
|
||||
deltaTime = CGFloat(min(1.0 / 60.0, timestamp - previousPhysicsTimestamp))
|
||||
} else {
|
||||
deltaTime = CGFloat(1.0 / 60.0)
|
||||
}
|
||||
self.previousPhysicsTimestamp = timestamp
|
||||
deltaTime /= UIView.animationDurationFactor()
|
||||
|
||||
let testSpringFriction: CGFloat = 18.5
|
||||
let testSpringConstant: CGFloat = 243.0
|
||||
|
||||
let frictionConstant: CGFloat = testSpringFriction
|
||||
let springConstant: CGFloat = testSpringConstant
|
||||
let time: CGFloat = deltaTime
|
||||
|
||||
var badgeAngle = self.badgeAngle
|
||||
|
||||
// friction force = velocity * friction constant
|
||||
let frictionForce = self.badgeAngularSpeed * frictionConstant
|
||||
// spring force = (target point - current position) * spring constant
|
||||
let springForce = -badgeAngle * springConstant
|
||||
// force = spring force - friction force
|
||||
let force = springForce - frictionForce
|
||||
|
||||
// velocity = current velocity + force * time / mass
|
||||
self.badgeAngularSpeed = self.badgeAngularSpeed + force * time
|
||||
// position = current position + velocity * time
|
||||
badgeAngle = badgeAngle + self.badgeAngularSpeed * time
|
||||
badgeAngle = badgeAngle.isNaN ? 0.0 : badgeAngle
|
||||
|
||||
let epsilon: CGFloat = 0.01
|
||||
if abs(badgeAngle) < epsilon && abs(self.badgeAngularSpeed) < epsilon {
|
||||
badgeAngle = 0.0
|
||||
self.badgeAngularSpeed = 0.0
|
||||
}
|
||||
|
||||
if abs(badgeAngle) > 0.22 {
|
||||
badgeAngle = badgeAngle < 0.0 ? -0.22 : 0.22
|
||||
}
|
||||
|
||||
if self.badgeAngle != badgeAngle {
|
||||
self.badgeAngle = badgeAngle
|
||||
self.updateBadgeTransform()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateBadgeTransform() {
|
||||
guard let badgeView = self.badge.view else {
|
||||
return
|
||||
}
|
||||
var transform = CATransform3DIdentity
|
||||
transform = CATransform3DScale(transform, self.badgeScale, self.badgeScale, 1.0)
|
||||
transform = CATransform3DRotate(transform, self.badgeAngle, 0.0, 0.0, 1.0)
|
||||
badgeView.layer.transform = transform
|
||||
}
|
||||
|
||||
func update(component: ProfileLevelRatingBarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
@ -215,11 +407,31 @@ final class ProfileLevelRatingBarComponent: Component {
|
||||
|
||||
var labelsTransition = transition
|
||||
if let previousComponent = self.component, let hint = transition.userData(TransitionHint.self), hint.animate {
|
||||
labelsTransition = .spring(duration: 0.4)
|
||||
labelsTransition = .spring(duration: 0.5)
|
||||
|
||||
var fromLevel = previousComponent.level
|
||||
var fromLeftLabelText = previousComponent.leftLabel
|
||||
var fromRightLabelText = previousComponent.rightLabel
|
||||
let toLevel: Int = component.level
|
||||
let fromValue: CGFloat
|
||||
if let animationState = self.animationState {
|
||||
fromValue = animationState.value(at: CACurrentMediaTime())
|
||||
if let wraparound = animationState.wraparound {
|
||||
let wraparoundEnd: CGFloat
|
||||
switch wraparound {
|
||||
case .left:
|
||||
wraparoundEnd = 0.0
|
||||
case .right:
|
||||
wraparoundEnd = 1.0
|
||||
}
|
||||
if animationState.stepFraction(at: CACurrentMediaTime()).step == 0 {
|
||||
fromLevel = animationState.fromLevel
|
||||
fromLeftLabelText = animationState.fromLeftLabelText
|
||||
fromRightLabelText = animationState.fromRightLabelText
|
||||
}
|
||||
fromValue = animationState.wrapAroundValue(at: CACurrentMediaTime(), bottomValue: 1.0 - wraparoundEnd, topValue: wraparoundEnd)
|
||||
} else {
|
||||
fromValue = animationState.value(at: CACurrentMediaTime())
|
||||
}
|
||||
} else {
|
||||
fromValue = previousComponent.value
|
||||
}
|
||||
@ -229,13 +441,23 @@ final class ProfileLevelRatingBarComponent: Component {
|
||||
} else {
|
||||
fromBadgeSize = CGSize()
|
||||
}
|
||||
var wraparound: AnimationState.Wraparound?
|
||||
var duration = 0.4
|
||||
if previousComponent.level != component.level {
|
||||
wraparound = component.level > previousComponent.level ? .right : .left
|
||||
duration = 0.8
|
||||
}
|
||||
self.animationState = AnimationState(
|
||||
fromLevel: fromLevel,
|
||||
toLevel: toLevel,
|
||||
fromLeftLabelText: fromLeftLabelText,
|
||||
fromRightLabelText: fromRightLabelText,
|
||||
fromValue: fromValue,
|
||||
toValue: component.value,
|
||||
fromBadgeSize: fromBadgeSize,
|
||||
startTime: CACurrentMediaTime(),
|
||||
duration: 0.4 * UIView.animationDurationFactor(),
|
||||
isWraparound: false//previousComponent.level < component.level
|
||||
duration: duration * UIView.animationDurationFactor(),
|
||||
wraparound: wraparound
|
||||
)
|
||||
self.updateAnimations()
|
||||
}
|
||||
@ -248,20 +470,19 @@ final class ProfileLevelRatingBarComponent: Component {
|
||||
self.barForeground.image = self.barBackground.image
|
||||
}
|
||||
|
||||
self.barBackground.tintColor = component.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.5)
|
||||
self.barForeground.tintColor = component.theme.list.itemCheckColors.fillColor
|
||||
|
||||
if self.barBackground.superview == nil {
|
||||
self.addSubview(self.barBackground)
|
||||
self.addSubview(self.backgroundClippingContainer)
|
||||
|
||||
self.addSubview(self.foregroundBarClippingContainer)
|
||||
self.foregroundBarClippingContainer.addSubview(self.barForeground)
|
||||
|
||||
self.addSubview(self.foregroundClippingContainer)
|
||||
self.foregroundClippingContainer.addSubview(self.barForeground)
|
||||
}
|
||||
|
||||
let progressValue: CGFloat
|
||||
if let animationState = self.animationState {
|
||||
progressValue = animationState.value(at: CACurrentMediaTime())
|
||||
if let testFraction = self.testFraction {
|
||||
progressValue = testFraction
|
||||
} else {
|
||||
progressValue = component.value
|
||||
}
|
||||
@ -269,18 +490,84 @@ final class ProfileLevelRatingBarComponent: Component {
|
||||
let barBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - barHeight), size: CGSize(width: availableSize.width, height: barHeight))
|
||||
transition.setFrame(view: self.barBackground, frame: barBackgroundFrame)
|
||||
|
||||
let barForegroundFrame = CGRect(origin: barBackgroundFrame.origin, size: CGSize(width: floorToScreenPixels(progressValue * barBackgroundFrame.width), height: barBackgroundFrame.height))
|
||||
var barForegroundFrame = CGRect(origin: barBackgroundFrame.origin, size: CGSize(width: floorToScreenPixels(progressValue * barBackgroundFrame.width), height: barBackgroundFrame.height))
|
||||
|
||||
var barApparentForegroundFrame = barForegroundFrame
|
||||
if let animationState = self.animationState, animationState.isWraparound {
|
||||
let progressValue = animationState.wrapAroundValue(at: CACurrentMediaTime(), topValue: 1.0)
|
||||
barApparentForegroundFrame = CGRect(origin: barBackgroundFrame.origin, size: CGSize(width: floorToScreenPixels(progressValue * barBackgroundFrame.width), height: barBackgroundFrame.height))
|
||||
var foregroundAlpha: CGFloat = 1.0
|
||||
var foregroundContentsAlpha: CGFloat = 1.0
|
||||
var badgeScale: CGFloat = 1.0
|
||||
var currentIsNegativeRating: Bool = component.level < 0
|
||||
var leftLabelText = component.leftLabel
|
||||
var rightLabelText = component.rightLabel
|
||||
|
||||
if let animationState = self.animationState {
|
||||
if let wraparound = animationState.wraparound {
|
||||
let (step, progress) = animationState.stepFraction(at: CACurrentMediaTime())
|
||||
if step == 0 {
|
||||
currentIsNegativeRating = animationState.fromLevel < 0
|
||||
leftLabelText = animationState.fromLeftLabelText
|
||||
rightLabelText = animationState.fromRightLabelText
|
||||
} else {
|
||||
currentIsNegativeRating = animationState.toLevel < 0
|
||||
}
|
||||
let wraparoundEnd: CGFloat
|
||||
switch wraparound {
|
||||
case .left:
|
||||
wraparoundEnd = 0.0
|
||||
if step == 0 {
|
||||
foregroundContentsAlpha = 1.0 * (1.0 - progress)
|
||||
badgeScale = 1.0 * (1.0 - progress) + 0.3 * progress
|
||||
} else {
|
||||
foregroundAlpha = 1.0 * progress
|
||||
foregroundContentsAlpha = foregroundAlpha
|
||||
badgeScale = 1.0 * progress + 0.3 * (1.0 - progress)
|
||||
}
|
||||
case .right:
|
||||
wraparoundEnd = 1.0
|
||||
if step == 0 {
|
||||
foregroundAlpha = 1.0 * (1.0 - progress)
|
||||
foregroundContentsAlpha = foregroundAlpha
|
||||
badgeScale = 1.0 * (1.0 - progress) + 0.3 * progress
|
||||
} else {
|
||||
foregroundContentsAlpha = 1.0 * progress
|
||||
badgeScale = 1.0 * progress + 0.3 * (1.0 - progress)
|
||||
}
|
||||
}
|
||||
|
||||
let progressValue = animationState.wrapAroundValue(at: CACurrentMediaTime(), bottomValue: 1.0 - wraparoundEnd, topValue: wraparoundEnd)
|
||||
barForegroundFrame = CGRect(origin: barBackgroundFrame.origin, size: CGSize(width: floorToScreenPixels(progressValue * barBackgroundFrame.width), height: barBackgroundFrame.height))
|
||||
} else {
|
||||
let progressValue = animationState.value(at: CACurrentMediaTime())
|
||||
barForegroundFrame = CGRect(origin: barBackgroundFrame.origin, size: CGSize(width: floorToScreenPixels(progressValue * barBackgroundFrame.width), height: barBackgroundFrame.height))
|
||||
}
|
||||
}
|
||||
transition.setFrame(view: self.foregroundClippingContainer, frame: barApparentForegroundFrame)
|
||||
|
||||
let backgroundClippingFrame = CGRect(origin: CGPoint(x: barBackgroundFrame.minX + barApparentForegroundFrame.width, y: barBackgroundFrame.minY), size: CGSize(width: barBackgroundFrame.width - barApparentForegroundFrame.width, height: barBackgroundFrame.height))
|
||||
let badgeColor: UIColor
|
||||
if currentIsNegativeRating {
|
||||
badgeColor = UIColor(rgb: 0xFF3B30)
|
||||
} else {
|
||||
badgeColor = component.theme.list.itemCheckColors.fillColor
|
||||
}
|
||||
|
||||
self.barBackground.tintColor = component.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.5)
|
||||
self.barForeground.tintColor = badgeColor
|
||||
|
||||
var effectiveBarForegroundFrame = barForegroundFrame
|
||||
if currentIsNegativeRating {
|
||||
effectiveBarForegroundFrame.size.width = barBackgroundFrame.maxX - barForegroundFrame.maxX
|
||||
effectiveBarForegroundFrame.origin.x = barBackgroundFrame.maxX - effectiveBarForegroundFrame.width
|
||||
}
|
||||
transition.setPosition(view: self.foregroundBarClippingContainer, position: effectiveBarForegroundFrame.center)
|
||||
transition.setBounds(view: self.foregroundBarClippingContainer, bounds: CGRect(origin: CGPoint(x: effectiveBarForegroundFrame.minX - barForegroundFrame.minX, y: 0.0), size: effectiveBarForegroundFrame.size))
|
||||
transition.setPosition(view: self.foregroundClippingContainer, position: effectiveBarForegroundFrame.center)
|
||||
transition.setBounds(view: self.foregroundClippingContainer, bounds: CGRect(origin: CGPoint(x: effectiveBarForegroundFrame.minX - barForegroundFrame.minX, y: 0.0), size: effectiveBarForegroundFrame.size))
|
||||
|
||||
transition.setAlpha(view: self.foregroundBarClippingContainer, alpha: foregroundAlpha)
|
||||
transition.setAlpha(view: self.foregroundClippingContainer, alpha: foregroundContentsAlpha)
|
||||
|
||||
let backgroundClippingFrame = CGRect(origin: CGPoint(x: barBackgroundFrame.minX + barForegroundFrame.width, y: barBackgroundFrame.minY), size: CGSize(width: barBackgroundFrame.width - barForegroundFrame.width, height: barBackgroundFrame.height))
|
||||
transition.setPosition(view: self.backgroundClippingContainer, position: backgroundClippingFrame.center)
|
||||
transition.setBounds(view: self.backgroundClippingContainer, bounds: CGRect(origin: CGPoint(x: backgroundClippingFrame.minX - barBackgroundFrame.minX, y: 0.0), size: backgroundClippingFrame.size))
|
||||
transition.setAlpha(view: self.backgroundClippingContainer, alpha: foregroundContentsAlpha)
|
||||
|
||||
transition.setFrame(view: self.barForeground, frame: CGRect(origin: CGPoint(), size: barBackgroundFrame.size))
|
||||
|
||||
@ -288,52 +575,32 @@ final class ProfileLevelRatingBarComponent: Component {
|
||||
|
||||
let leftLabelSize = self.backgroundLeftLabel.update(
|
||||
transition: labelsTransition,
|
||||
component: AnyComponent(AnimatedTextComponent(
|
||||
font: labelFont,
|
||||
color: component.theme.list.itemPrimaryTextColor,
|
||||
items: [AnimatedTextComponent.Item(
|
||||
id: AnyHashable(0),
|
||||
content: .text(component.leftLabel)
|
||||
)]
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: leftLabelText, font: labelFont, textColor: component.theme.list.itemPrimaryTextColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: barBackgroundFrame.width, height: 100.0)
|
||||
)
|
||||
let _ = self.foregroundLeftLabel.update(
|
||||
transition: labelsTransition,
|
||||
component: AnyComponent(AnimatedTextComponent(
|
||||
font: labelFont,
|
||||
color: component.theme.list.itemCheckColors.foregroundColor,
|
||||
items: [AnimatedTextComponent.Item(
|
||||
id: AnyHashable(0),
|
||||
content: .text(component.leftLabel)
|
||||
)]
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: leftLabelText, font: labelFont, textColor: component.theme.list.itemCheckColors.foregroundColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: barBackgroundFrame.width, height: 100.0)
|
||||
)
|
||||
let rightLabelSize = self.backgroundRightLabel.update(
|
||||
transition: labelsTransition,
|
||||
component: AnyComponent(AnimatedTextComponent(
|
||||
font: labelFont,
|
||||
color: component.theme.list.itemPrimaryTextColor,
|
||||
items: [AnimatedTextComponent.Item(
|
||||
id: AnyHashable(0),
|
||||
content: .text(component.rightLabel)
|
||||
)]
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: rightLabelText, font: labelFont, textColor: component.theme.list.itemPrimaryTextColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: barBackgroundFrame.width, height: 100.0)
|
||||
)
|
||||
let _ = self.foregroundRightLabel.update(
|
||||
transition: labelsTransition,
|
||||
component: AnyComponent(AnimatedTextComponent(
|
||||
font: labelFont,
|
||||
color: component.theme.list.itemCheckColors.foregroundColor,
|
||||
items: [AnimatedTextComponent.Item(
|
||||
id: AnyHashable(0),
|
||||
content: .text(component.rightLabel)
|
||||
)]
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: rightLabelText, font: labelFont, textColor: component.theme.list.itemCheckColors.foregroundColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: barBackgroundFrame.width, height: 100.0)
|
||||
@ -379,8 +646,8 @@ final class ProfileLevelRatingBarComponent: Component {
|
||||
transition: transition.withUserData(ProfileLevelRatingBarBadge.TransitionHint(animateText: !labelsTransition.animation.isImmediate)),
|
||||
component: AnyComponent(ProfileLevelRatingBarBadge(
|
||||
theme: component.theme,
|
||||
title: "\(component.badgeValue)",
|
||||
suffix: component.badgeTotal
|
||||
title: component.level < 0 ? "" : "\(component.badgeValue)",
|
||||
suffix: component.level < 0 ? nil : component.badgeTotal
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 200.0, height: 200.0)
|
||||
@ -412,9 +679,24 @@ final class ProfileLevelRatingBarComponent: Component {
|
||||
}
|
||||
|
||||
badgeFrame.origin.x += badgeOverflowWidth
|
||||
badgeView.frame = badgeFrame
|
||||
let badgeTailOffset = (barBackgroundFrame.minX + barForegroundFrame.width) - badgeFrame.minX
|
||||
let badgePosition = CGPoint(x: badgeFrame.minX + badgeTailOffset, y: badgeFrame.maxY)
|
||||
|
||||
badgeView.adjustTail(size: apparentBadgeSize, overflowWidth: -badgeOverflowWidth, transition: transition)
|
||||
if let animationDeltaTime = self.animationDeltaTime, self.animationState != nil, !self.animationIsMovingOverStep {
|
||||
let previousX = badgeView.center.x
|
||||
self.addBadgeDeltaX(value: badgePosition.x - previousX, deltaTime: animationDeltaTime)
|
||||
}
|
||||
|
||||
badgeView.center = badgePosition
|
||||
badgeView.bounds = CGRect(origin: CGPoint(), size: badgeFrame.size)
|
||||
transition.setAnchorPoint(layer: badgeView.layer, anchorPoint: CGPoint(x: max(0.0, min(1.0, badgeTailOffset / badgeFrame.width)), y: 1.0))
|
||||
|
||||
badgeView.updateColors(background: badgeColor)
|
||||
|
||||
badgeView.adjustTail(size: apparentBadgeSize, tailOffset: badgeTailOffset, transition: transition)
|
||||
transition.setAlpha(view: badgeView, alpha: foregroundContentsAlpha)
|
||||
self.badgeScale = badgeScale
|
||||
self.updateBadgeTransform()
|
||||
}
|
||||
|
||||
return availableSize
|
||||
|
||||
12
submodules/TelegramUI/Images.xcassets/Peer Info/InlineRatingWarning.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Peer Info/InlineRatingWarning.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "badratingprofile.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Peer Info/InlineRatingWarning.imageset/badratingprofile.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Peer Info/InlineRatingWarning.imageset/badratingprofile.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Peer Info/ProfileLevelWarningIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Peer Info/ProfileLevelWarningIcon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "badratingslider.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Peer Info/ProfileLevelWarningIcon.imageset/badratingslider.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Peer Info/ProfileLevelWarningIcon.imageset/badratingslider.pdf
vendored
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user