Various improvements

This commit is contained in:
Isaac 2025-08-01 21:29:26 +02:00
parent 61b3025654
commit 179c1341be
14 changed files with 192 additions and 230 deletions

View File

@ -14845,7 +14845,16 @@ Sorry for the inconvenience.";
"ProfileLevelInfo.MyDescriptionDays_any" = "%@ days";
"ProfileLevelInfo.MyDescriptionPoints_1" = "%@ point is";
"ProfileLevelInfo.MyDescriptionPoints_any" = "%@ points are";
"ProfileLevelInfo.MyDescriptionPreview" = "The rating updates in %@ after purchases.\n\%@ pending. [Preview >]()";
"ProfileLevelInfo.MyDescriptionPreview" = "The rating will update in %@.\n\%@ pending. [Preview >]()";
"ProfileLevelInfo.MyDescriptionInPreviewDays_1" = "%@ day";
"ProfileLevelInfo.MyDescriptionInPreviewDays_any" = "%@ days";
"ProfileLevelInfo.MyDescriptionInPreviewPoints_1" = "%@ point is";
"ProfileLevelInfo.MyDescriptionInPreviewPoints_any" = "%@ points are";
"ProfileLevelInfo.MyDescriptionInPreview" = "The rating will update in %1$@,\n after %2$@ added. [Back >]()";
"ProfileLevelInfo.MyDescriptionInPreviewToday_1" = "This will be your rating today,\n after %@ is added. [Back >]()";
"ProfileLevelInfo.MyDescriptionInPreviewToday_any" = "This will be your rating today,\n after %@ added. [Back >]()";
"ProfileLevelInfo.OtherDescription" = "The rating reflects **%@'s** activity on Telegram. What affects it:";
"ProfileLevelInfo.LevelIndex_1" = "Level %@";
"ProfileLevelInfo.LevelIndex_any" = "Level %@";
@ -14868,3 +14877,8 @@ Sorry for the inconvenience.";
"Stories.Post.AlbumAll" = "All Stories";
"Stories.Post.AlbumCount_1" = "%@ Album";
"Stories.Post.AlbumCount_any" = "%@ Albums";
"Stories.ToastAddedToFolder_1" = "Story added to folder.";
"Stories.ToastAddedToFolder_any" = "%@ stories added to folder.";
"Stories.ToastAlbumNotAvailable" = "The album is no longer available.";

View File

@ -715,7 +715,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) }, 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: true, selectedFilter: self.selectedFilter?.id, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: transition)
}
}
@ -786,7 +786,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) }, 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: true, 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)

View File

@ -71,7 +71,7 @@ private final class ItemNode: ASDisplayNode {
self.pressed()
}
func update(type: ChatListSearchFilter, presentationData: PresentationData, selectionFraction: CGFloat, transition: ContainedViewLayoutTransition) {
func update(type: ChatListSearchFilter, displayNewBadge: Bool, presentationData: PresentationData, selectionFraction: CGFloat, transition: ContainedViewLayoutTransition) {
self.selectionFraction = selectionFraction
let title: String
@ -94,7 +94,9 @@ private final class ItemNode: ASDisplayNode {
icon = nil
case .globalPosts:
title = presentationData.strings.ChatList_Search_FilterGlobalPosts
titleBadge = presentationData.strings.ChatList_ContextMenuBadgeNew
if displayNewBadge {
titleBadge = presentationData.strings.ChatList_ContextMenuBadgeNew
}
icon = nil
case .media:
title = presentationData.strings.ChatList_Search_FilterMedia
@ -271,7 +273,7 @@ final class ChatListSearchFiltersContainerNode: ASDisplayNode {
self.scrollNode.layer.removeAllAnimations()
}
func update(size: CGSize, sideInset: CGFloat, filters: [ChatListSearchFilterEntry], selectedFilter: ChatListSearchFilterEntryId?, transitionFraction: CGFloat, presentationData: PresentationData, transition proposedTransition: ContainedViewLayoutTransition) {
func update(size: CGSize, sideInset: CGFloat, filters: [ChatListSearchFilterEntry], displayGlobalPostsNewBadge: Bool, selectedFilter: ChatListSearchFilterEntryId?, transitionFraction: CGFloat, presentationData: PresentationData, transition proposedTransition: ContainedViewLayoutTransition) {
let isFirstTime = self.currentParams == nil
let transition: ContainedViewLayoutTransition = isFirstTime ? .immediate : proposedTransition
@ -321,7 +323,12 @@ final class ChatListSearchFiltersContainerNode: ASDisplayNode {
selectionFraction = 0.0
}
itemNode.update(type: type, presentationData: presentationData, selectionFraction: selectionFraction, transition: itemNodeTransition)
var displayNewBadge = false
if case .globalPosts = type {
displayNewBadge = displayGlobalPostsNewBadge
}
itemNode.update(type: type, displayNewBadge: displayNewBadge, presentationData: presentationData, selectionFraction: selectionFraction, transition: itemNodeTransition)
}
}

View File

@ -6189,6 +6189,7 @@ private final class EmptyResultsButtonSearchContent: Component {
}
func update(component: EmptyResultsButtonSearchContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let sideInset: CGFloat = 8.0
let iconSpacing: CGFloat = 2.0
let arrowSpacing: CGFloat = 4.0
@ -6237,7 +6238,7 @@ private final class EmptyResultsButtonSearchContent: Component {
text: .plain(string)
)),
environment: {},
containerSize: CGSize(width: availableSize.width - iconSize.width - iconSpacing - arrowSize.width - arrowSpacing, height: 100.0)
containerSize: CGSize(width: availableSize.width - iconSize.width - iconSpacing - arrowSize.width - arrowSpacing - sideInset * 2.0, height: 100.0)
)
let textFrame = CGRect(origin: CGPoint(x: iconSize.width + iconSpacing, y: 0.0), size: textSize)
if let textView = self.text.view {

View File

@ -576,7 +576,7 @@ public struct PreferencesKeys {
}
public static func globalPostSearchState() -> ValueBoxKey {
let key = ValueBoxKey(length: 4 + 8)
let key = ValueBoxKey(length: 4)
key.setInt32(0, value: PreferencesKeyValues.globalPostSearchState.rawValue)
return key
}

View File

@ -207,6 +207,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 {
case voiceMessagesPauseSuggestion = 80
case videoMessagesPauseSuggestion = 81
case voiceMessagesResumeTrimWarning = 82
case globalPostsSearch = 83
var key: ValueBoxKey {
let v = ValueBoxKey(length: 4)
@ -584,6 +585,10 @@ private struct ApplicationSpecificNoticeKeys {
static func voiceMessagesResumeTrimWarning() -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.voiceMessagesResumeTrimWarning.key)
}
static func globalPostsSearch() -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.globalPostsSearch.key)
}
}
public struct ApplicationSpecificNotice {
@ -2554,4 +2559,31 @@ public struct ApplicationSpecificNotice {
return Int(previousValue)
}
}
public static func getGlobalPostsSearch(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<Int32, NoError> {
return accountManager.transaction { transaction -> Int32 in
if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.globalPostsSearch())?.get(ApplicationSpecificCounterNotice.self) {
return value.value
} else {
return 0
}
}
}
public static func incrementGlobalPostsSearch(accountManager: AccountManager<TelegramAccountManagerTypes>, count: Int = 1) -> Signal<Int, NoError> {
return accountManager.transaction { transaction -> Int in
var currentValue: Int32 = 0
if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.globalPostsSearch())?.get(ApplicationSpecificCounterNotice.self) {
currentValue = value.value
}
let previousValue = currentValue
currentValue += Int32(count)
if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) {
transaction.setNotice(ApplicationSpecificNoticeKeys.globalPostsSearch(), entry)
}
return Int(previousValue)
}
}
}

View File

@ -287,6 +287,20 @@ public final class LottieComponent: Component {
}
}
public func setFrameIndex(index: Int) {
guard let _ = self.animationInstance, let animationFrameRange = self.animationFrameRange else {
self.scheduledPlayOnce = true
return
}
let currentFrame = max(animationFrameRange.lowerBound, min(animationFrameRange.upperBound, index))
if self.currentFrame != currentFrame {
self.currentFrame = currentFrame
self.updateImage()
}
}
private func loadPlaceholder(data: Data) {
guard let component = self.component, let placeholderColor = component.placeholderColor else {
return

View File

@ -7,10 +7,10 @@ import Svg
private func generateNumberOffsets() -> [CGPoint] {
return [
CGPoint(x: 0.33, y: -0.33),
CGPoint(x: 0.25, y: -0.33),
CGPoint(x: 0.24749999999999983, y: -0.495),
CGPoint(x: 0.0, y: -1.4025),
CGPoint(x: 0.5775, y: -0.495),
CGPoint(x: 0.33, y: -0.495),
CGPoint(x: 0.33, y: 0.0),
CGPoint(x: 0.5775, y: 0.0),
CGPoint(x: 0.49500000000000005, y: 0.0),
@ -324,7 +324,7 @@ public final class PeerInfoRatingComponent: Component {
let backgroundOffsetsY: [Int: CGFloat] = [
3: -0.8250000000000001,
7: 0.33,
40: 1.4025,
40: 1.0,
60: 0.2475,
70: 0.33,
80: 0.2475,

View File

@ -1990,7 +1990,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
#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, nextLevelStars: starRating.nextLevelStars! * 2), 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 {
@ -2092,7 +2092,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let subtitleBadgeFrame: CGRect
subtitleBadgeFrame = CGRect(origin: CGPoint(x: (-subtitleSize.width) * 0.5 - subtitleRatingSize.width + 1.0, y: subtitleOffset + floor((-subtitleRatingSize.height) * 0.5)), size: subtitleRatingSize)
transition.updateFrameAdditive(view: subtitleRatingView, frame: subtitleBadgeFrame)
transition.updateAlpha(layer: subtitleRatingView.layer, alpha: (1.0 - transitionFraction))
transition.updateAlpha(layer: subtitleRatingView.layer, alpha: subtitleAlpha)
}
} else {
let titleScale: CGFloat

View File

@ -2601,9 +2601,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
let text: String
text = "Story added to folder."
text = presentationData.strings.Stories_ToastAddedToFolder(1)
self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: nil, text: text, cancel: nil, destructive: false), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
})))
}
@ -2999,8 +2998,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
self.initialStoryFolderId = nil
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: nil, text: "The album is no longer available.", cancel: nil, destructive: false), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: nil, text: presentationData.strings.Stories_ToastAlbumNotAvailable, cancel: nil, destructive: false), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}
self.currentListState = state
@ -5111,13 +5109,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
let _ = listSource.addToFolder(id: folderId, items: items)
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
let text: String
if items.count == 1 {
text = "Story added to folder."
} else {
text = "\(items.count) stories added to folder."
}
let text: String = presentationData.strings.Stories_ToastAddedToFolder(Int32(items.count))
self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: nil, text: text, cancel: nil, destructive: false), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}
})

View File

@ -355,11 +355,15 @@ private final class ProfileLevelInfoScreenComponent: Component {
if pendingStarRating.rating.stars > component.starRating.stars {
let pendingPoints = pendingStarRating.rating.stars - component.starRating.stars
let dayCount = (pendingStarRating.timestamp - timestamp) / (24 * 60 * 60)
if self.isPreviewingPendingRating {
//TODO:localize
secondaryDescriptionTextString = "This will be your rating in 21 days,\n after \(pendingPoints) points are added. [Back >]()"
if dayCount == 0 {
secondaryDescriptionTextString = environment.strings.ProfileLevelInfo_MyDescriptionInPreviewToday(Int32(pendingPoints))
} else {
secondaryDescriptionTextString = environment.strings.ProfileLevelInfo_MyDescriptionInPreview(environment.strings.ProfileLevelInfo_MyDescriptionInPreviewDays(Int32(dayCount)), environment.strings.ProfileLevelInfo_MyDescriptionInPreviewPoints(Int32(pendingPoints))).string
}
} else {
let dayCount = (pendingStarRating.timestamp - timestamp) / (24 * 60 * 60)
if dayCount == 0 {
secondaryDescriptionTextString = environment.strings.ProfileLevelInfo_MyDescriptionToday(Int32(pendingPoints))
} else {
@ -615,23 +619,23 @@ private final class ProfileLevelInfoScreenComponent: Component {
}
let items: [Item] = [
Item(
title: "Gifts from Telegram",
text: "100% of the Stars spent on gifts purchased from Telegram.",
badgeText: "ADDED",
title: environment.strings.ProfileLevelInfo_Item0_Title,
text: environment.strings.ProfileLevelInfo_Item0_Text,
badgeText: environment.strings.ProfileLevelInfo_Item0_Badge,
isBadgeAccent: true,
icon: "Chat/Input/Accessory Panels/Gift"
),
Item(
title: "Gifts and Posts from Users",
text: "20% of the Stars spent on gifts or posts from users and channels.",
badgeText: "ADDED",
title: environment.strings.ProfileLevelInfo_Item1_Title,
text: environment.strings.ProfileLevelInfo_Item1_Text,
badgeText: environment.strings.ProfileLevelInfo_Item1_Badge,
isBadgeAccent: true,
icon: "Peer Info/ProfileLevelInfo2"
),
Item(
title: "Refunds and Conversions",
text: "10x of refunded Stars and 85% of bought gifts converted to Stars.",
badgeText: "DEDUCTED",
title: environment.strings.ProfileLevelInfo_Item2_Title,
text: environment.strings.ProfileLevelInfo_Item2_Text,
badgeText: environment.strings.ProfileLevelInfo_Item2_Badge,
isBadgeAccent: false,
icon: "Peer Info/ProfileLevelInfo3"
)

View File

@ -6,6 +6,7 @@ import ComponentFlow
import RoundedRectWithTailPath
import AnimatedTextComponent
import MultilineTextComponent
import LottieComponent
final class ProfileLevelRatingBarBadge: Component {
final class TransitionHint {
@ -47,6 +48,7 @@ final class ProfileLevelRatingBarBadge: Component {
private let badgeView: UIView
private let badgeMaskView: UIView
private let badgeShapeLayer = SimpleShapeLayer()
private let badgeShapeAnimation = ComponentView<Empty>()
private let badgeForeground: SimpleLayer
let badgeIcon: UIImageView
@ -64,6 +66,7 @@ final class ProfileLevelRatingBarBadge: Component {
override init(frame: CGRect) {
self.badgeView = UIView()
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)
@ -147,7 +150,7 @@ final class ProfileLevelRatingBarBadge: Component {
containerSize: CGSize(width: 300.0, height: 100.0)
)
var badgeWidth: CGFloat = badgeLabelSize.width + 3.0 + 54.0
var badgeWidth: CGFloat = badgeLabelSize.width + 3.0 + 60.0
if component.suffix != nil {
badgeWidth += badgeSuffixSize.width + badgeSuffixSpacing
}
@ -161,7 +164,7 @@ final class ProfileLevelRatingBarBadge: Component {
self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: 600.0, height: badgeFullSize.height + 10.0))
self.badgeIcon.frame = CGRect(x: 10.0, y: 8.0, width: 30.0, height: 30.0)
self.badgeIcon.frame = CGRect(x: 13.0, y: 8.0, width: 30.0, height: 30.0)
self.badgeView.alpha = 1.0
@ -172,7 +175,7 @@ final class ProfileLevelRatingBarBadge: Component {
badgeContentWidth += badgeSuffixSpacing + badgeSuffixSize.width
}
let badgeLabelFrame = CGRect(origin: CGPoint(x: 14.0 + floorToScreenPixels((badgeFullSize.width - badgeContentWidth) / 2.0), y: 9.0), size: badgeLabelSize)
let badgeLabelFrame = CGRect(origin: CGPoint(x: 15.0 + floorToScreenPixels((badgeFullSize.width - badgeContentWidth) / 2.0), y: 9.0), size: badgeLabelSize)
if let badgeLabelView = self.badgeLabel.view {
if badgeLabelView.superview == nil {
self.badgeView.addSubview(badgeLabelView)
@ -214,6 +217,11 @@ final class ProfileLevelRatingBarBadge: Component {
}
func adjustTail(size: CGSize, overflowWidth: CGFloat, transition: ComponentTransition) {
guard let component else {
return
}
let _ = component
var tailPosition = size.width * 0.5
tailPosition += overflowWidth
tailPosition = max(36.0, min(size.width - 36.0, tailPosition))
@ -221,9 +229,46 @@ final class ProfileLevelRatingBarBadge: Component {
let tailPositionFraction = tailPosition / size.width
transition.setShapeLayerPath(layer: self.badgeShapeLayer, path: generateRoundedRectWithTailPath(rectSize: size, tailPosition: tailPositionFraction, transformTail: false).cgPath)
let transition: ContainedViewLayoutTransition = .immediate
transition.updateAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: tailPositionFraction, y: 1.0))
transition.updatePosition(layer: self.badgeView.layer, position: CGPoint(x: (tailPositionFraction - 0.5) * size.width, y: 0.0))
let badgeShapeSize = CGSize(width: 128, height: 128)
let _ = self.badgeShapeAnimation.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "badge_with_tail"),
color: .red,//component.theme.list.itemCheckColors.fillColor,
placeholderColor: nil,
startingPosition: .begin,
size: badgeShapeSize,
renderingScale: nil,
loop: false,
playOnce: nil
)),
environment: {},
containerSize: badgeShapeSize
)
if let badgeShapeAnimationView = self.badgeShapeAnimation.view as? LottieComponent.View, !"".isEmpty {
if badgeShapeAnimationView.superview == nil {
badgeShapeAnimationView.layer.anchorPoint = CGPoint()
self.addSubview(badgeShapeAnimationView)
}
let transition: ComponentTransition = .immediate
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
//transition.updateAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: tailPositionFraction, y: 1.0))
}
func updateBadgeAngle(angle: CGFloat) {
@ -272,168 +317,3 @@ final class ProfileLevelRatingBarBadge: Component {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private let labelWidth: CGFloat = 16.0
private let labelHeight: CGFloat = 36.0
private let labelSize = CGSize(width: labelWidth, height: labelHeight)
private let font = Font.with(size: 24.0, design: .round, weight: .semibold, traits: [])
private final class BadgeLabelView: UIView {
private class StackView: UIView {
var labels: [UILabel] = []
var currentValue: Int32 = 0
var color: UIColor = .white {
didSet {
for view in self.labels {
view.textColor = self.color
}
}
}
init() {
super.init(frame: CGRect(origin: .zero, size: labelSize))
var height: CGFloat = -labelHeight
for i in -1 ..< 10 {
let label = UILabel()
if i == -1 {
label.text = "9"
} else {
label.text = "\(i)"
}
label.textColor = self.color
label.font = font
label.textAlignment = .center
label.frame = CGRect(x: 0, y: height, width: labelWidth, height: labelHeight)
self.addSubview(label)
self.labels.append(label)
height += labelHeight
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(value: Int32, isFirst: Bool, isLast: Bool, transition: ComponentTransition) {
let previousValue = self.currentValue
self.currentValue = value
self.labels[1].alpha = isFirst && !isLast ? 0.0 : 1.0
if previousValue == 9 && value < 9 {
self.bounds = CGRect(
origin: CGPoint(
x: 0.0,
y: -1.0 * labelSize.height
),
size: labelSize
)
}
let bounds = CGRect(
origin: CGPoint(
x: 0.0,
y: CGFloat(value) * labelSize.height
),
size: labelSize
)
transition.setBounds(view: self, bounds: bounds)
}
}
private var itemViews: [Int: StackView] = [:]
private var staticLabel = UILabel()
init() {
super.init(frame: .zero)
self.clipsToBounds = true
self.isUserInteractionEnabled = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var color: UIColor = .white {
didSet {
self.staticLabel.textColor = self.color
for (_, view) in self.itemViews {
view.color = self.color
}
}
}
func update(value: String, transition: ComponentTransition) -> CGSize {
if value.contains(" ") {
for (_, view) in self.itemViews {
view.isHidden = true
}
if self.staticLabel.superview == nil {
self.staticLabel.textColor = self.color
self.staticLabel.font = font
self.addSubview(self.staticLabel)
}
self.staticLabel.text = value
let size = self.staticLabel.sizeThatFits(CGSize(width: 100.0, height: 100.0))
self.staticLabel.frame = CGRect(origin: .zero, size: CGSize(width: size.width, height: labelHeight))
return CGSize(width: ceil(self.staticLabel.bounds.width), height: ceil(self.staticLabel.bounds.height))
}
let string = value
let stringArray = Array(string.map { String($0) }.reversed())
let labelSpacing: CGFloat = 0.0
let totalWidth = CGFloat(stringArray.count) * labelWidth + CGFloat(stringArray.count - 1) * labelSpacing
var validIds: [Int] = []
for i in 0 ..< stringArray.count {
validIds.append(i)
let itemView: StackView
var itemTransition = transition
if let current = self.itemViews[i] {
itemView = current
} else {
itemTransition = transition.withAnimation(.none)
itemView = StackView()
itemView.color = self.color
self.itemViews[i] = itemView
self.addSubview(itemView)
}
let digit = Int32(stringArray[i]) ?? 0
itemView.update(value: digit, isFirst: i == stringArray.count - 1, isLast: i == 0, transition: transition)
itemTransition.setFrame(
view: itemView,
frame: CGRect(x: totalWidth - labelWidth * CGFloat(i + 1) + labelSpacing * CGFloat(i), y: 0.0, width: labelWidth, height: labelHeight)
)
}
var removeIds: [Int] = []
for (id, itemView) in self.itemViews {
if !validIds.contains(id) {
removeIds.append(id)
transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in
itemView.removeFromSuperview()
})
}
}
for id in removeIds {
self.itemViews.removeValue(forKey: id)
}
return CGSize(width: totalWidth, height: labelHeight)
}
}

View File

@ -6,6 +6,7 @@ import ComponentFlow
import MultilineTextComponent
import BundleIconComponent
import HierarchyTrackingLayer
import AnimatedTextComponent
final class ProfileLevelRatingBarComponent: Component {
final class TransitionHint {
@ -286,33 +287,53 @@ final class ProfileLevelRatingBarComponent: Component {
let labelFont = Font.semibold(14.0)
let leftLabelSize = self.backgroundLeftLabel.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.leftLabel, font: labelFont, textColor: component.theme.list.itemPrimaryTextColor))
transition: labelsTransition,
component: AnyComponent(AnimatedTextComponent(
font: labelFont,
color: component.theme.list.itemPrimaryTextColor,
items: [AnimatedTextComponent.Item(
id: AnyHashable(0),
content: .text(component.leftLabel)
)]
)),
environment: {},
containerSize: CGSize(width: barBackgroundFrame.width, height: 100.0)
)
let _ = self.foregroundLeftLabel.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.leftLabel, font: labelFont, textColor: component.theme.list.itemCheckColors.foregroundColor))
transition: labelsTransition,
component: AnyComponent(AnimatedTextComponent(
font: labelFont,
color: component.theme.list.itemCheckColors.foregroundColor,
items: [AnimatedTextComponent.Item(
id: AnyHashable(0),
content: .text(component.leftLabel)
)]
)),
environment: {},
containerSize: CGSize(width: barBackgroundFrame.width, height: 100.0)
)
let rightLabelSize = self.backgroundRightLabel.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.rightLabel, font: labelFont, textColor: component.theme.list.itemPrimaryTextColor))
transition: labelsTransition,
component: AnyComponent(AnimatedTextComponent(
font: labelFont,
color: component.theme.list.itemPrimaryTextColor,
items: [AnimatedTextComponent.Item(
id: AnyHashable(0),
content: .text(component.rightLabel)
)]
)),
environment: {},
containerSize: CGSize(width: barBackgroundFrame.width, height: 100.0)
)
let _ = self.foregroundRightLabel.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.rightLabel, font: labelFont, textColor: component.theme.list.itemCheckColors.foregroundColor))
transition: labelsTransition,
component: AnyComponent(AnimatedTextComponent(
font: labelFont,
color: component.theme.list.itemCheckColors.foregroundColor,
items: [AnimatedTextComponent.Item(
id: AnyHashable(0),
content: .text(component.rightLabel)
)]
)),
environment: {},
containerSize: CGSize(width: barBackgroundFrame.width, height: 100.0)
@ -365,36 +386,32 @@ final class ProfileLevelRatingBarComponent: Component {
containerSize: CGSize(width: 200.0, height: 200.0)
)
var badgeFrame = CGRect(origin: CGPoint(x: barBackgroundFrame.minX + barForegroundFrame.width, y: barBackgroundFrame.minY - 7.0), size: badgeSize)
if let badgeView = self.badge.view as? ProfileLevelRatingBarBadge.View {
if badgeView.superview == nil {
self.addSubview(badgeView)
}
let apparentBadgeSize: CGSize
var apparentBadgeOffset: CGFloat = 0.0
if let animationState = self.animationState {
apparentBadgeSize = animationState.badgeSize(at: CACurrentMediaTime(), endValue: badgeSize)
apparentBadgeOffset = (animationState.fromBadgeSize.width - badgeSize.width) * (1.0 - animationState.fraction(at: CACurrentMediaTime()))
apparentBadgeOffset = -apparentBadgeOffset * 0.25
} else {
apparentBadgeSize = badgeSize
}
badgeFrame.size = apparentBadgeSize
var badgeFrame = CGRect(origin: CGPoint(x: barBackgroundFrame.minX + barForegroundFrame.width - apparentBadgeSize.width * 0.5, y: barBackgroundFrame.minY - 18.0 - badgeSize.height), size: apparentBadgeSize)
let badgeSideInset: CGFloat = 0.0
let badgeOverflowWidth: CGFloat
if badgeFrame.minX - apparentBadgeSize.width * 0.5 < badgeSideInset {
badgeOverflowWidth = badgeSideInset - (badgeFrame.minX - apparentBadgeSize.width * 0.5)
} else if badgeFrame.minX + apparentBadgeSize.width * 0.5 > availableSize.width - badgeSideInset {
badgeOverflowWidth = availableSize.width - badgeSideInset - (badgeFrame.minX + apparentBadgeSize.width * 0.5)
if badgeFrame.minX < badgeSideInset {
badgeOverflowWidth = badgeSideInset - badgeFrame.minX
} else if badgeFrame.minX + badgeFrame.width > availableSize.width - badgeSideInset {
badgeOverflowWidth = availableSize.width - badgeSideInset - badgeFrame.width - badgeFrame.minX
} else {
badgeOverflowWidth = 0.0
}
badgeFrame.origin.x += badgeOverflowWidth + apparentBadgeOffset
badgeFrame.origin.x += badgeOverflowWidth
badgeView.frame = badgeFrame
badgeView.adjustTail(size: apparentBadgeSize, overflowWidth: -badgeOverflowWidth, transition: transition)

File diff suppressed because one or more lines are too long