From 179c1341be6c7e41122b599ce6a124e7c2da77cb Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 1 Aug 2025 21:29:26 +0200 Subject: [PATCH] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 16 +- .../Sources/ChatListSearchContainerNode.swift | 4 +- .../ChatListSearchFiltersContainerNode.swift | 15 +- .../Sources/ChatListSearchListPaneNode.swift | 3 +- .../SyncCore/SyncCore_Namespaces.swift | 2 +- .../TelegramNotices/Sources/Notices.swift | 32 +++ .../Sources/LottieComponent.swift | 14 ++ .../Sources/PeerInfoRatingComponent.swift | 6 +- .../Sources/PeerInfoHeaderNode.swift | 4 +- .../Sources/PeerInfoStoryPaneNode.swift | 14 +- .../Sources/ProfileLevelInfoScreen.swift | 28 ++- .../Sources/ProfileLevelRatingBarBadge.swift | 222 ++++-------------- .../ProfileLevelRatingBarComponent.swift | 61 +++-- .../Resources/Animations/badge_with_tail.json | 1 + 14 files changed, 192 insertions(+), 230 deletions(-) create mode 100644 submodules/TelegramUI/Resources/Animations/badge_with_tail.json diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 1cab68ba72..9c491f558b 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -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."; + diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 807b90bb55..a1fc5e5fdc 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -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) diff --git a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift index d641b16ff7..0f61b3b64c 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift @@ -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) } } diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index ebb233f45b..4611c2577b 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -6189,6 +6189,7 @@ private final class EmptyResultsButtonSearchContent: Component { } func update(component: EmptyResultsButtonSearchContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, 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 { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 2f93423e26..cebf05cbd0 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -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 } diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index 904f2a2e26..93a69a8bed 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -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) -> Signal { + 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, count: Int = 1) -> Signal { + 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) + } + } } diff --git a/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift b/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift index 5fdf32e458..a4f0bbb857 100644 --- a/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift +++ b/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift @@ -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 diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift index 2d964015fb..07bbb69c5b 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift @@ -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, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 539d317ea4..6d8ad714ce 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -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 diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 2a4449a56d..9d1c13b307 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -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) } }) diff --git a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift index b121ca24ae..b5feaba4b9 100644 --- a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift @@ -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" ) diff --git a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelRatingBarBadge.swift b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelRatingBarBadge.swift index bc4d34798e..80fb328174 100644 --- a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelRatingBarBadge.swift +++ b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelRatingBarBadge.swift @@ -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() 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) - } -} - diff --git a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelRatingBarComponent.swift b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelRatingBarComponent.swift index de5fdf751c..42c7b1931b 100644 --- a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelRatingBarComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelRatingBarComponent.swift @@ -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) diff --git a/submodules/TelegramUI/Resources/Animations/badge_with_tail.json b/submodules/TelegramUI/Resources/Animations/badge_with_tail.json new file mode 100644 index 0000000000..56717e56b5 --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/badge_with_tail.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":60,"ip":0,"op":360,"w":780,"h":600,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"1 Outlines 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[390,300,0],"ix":2,"l":2},"a":{"a":0,"k":[390,300,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":181,"s":[{"i":[[0,132.5],[0,0],[132.5,0],[11.264,-11.424],[0,0],[15.637,15.858],[0,0],[22.824,0]],"o":[[0,0],[0,132.548],[-22.081,0],[0,0],[-15.637,15.858],[0,0],[-11.265,-11.424],[-132.5,0]],"v":[[-390,-60],[390,-60],[150,180],[102.53,197.841],[28.313,273.107],[-28.313,273.107],[-102.53,197.841],[-150,180]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":270,"s":[{"i":[[0,132.5],[0,0],[132.5,0],[11.264,-11.424],[0,0],[15.637,15.858],[0,0],[29.086,29.579]],"o":[[0,0],[0,132.548],[-22.081,0],[0,0],[-15.637,15.858],[0,0],[-11.264,-11.424],[-29.086,-29.579]],"v":[[-390.002,-60],[390,-60],[-40,180],[-87.47,197.841],[-161.687,273.107],[-218.313,273.107],[-292.53,197.841],[-350,140]],"c":true}]},{"t":359,"s":[{"i":[[0,132.5],[0,0],[132.5,0],[11.264,-11.424],[0,0],[0,20.894],[0,0],[0,32.083]],"o":[[0,0],[0,132.548],[-22.081,0],[0,0],[-15.637,15.858],[0,0],[0,-17.841],[0,-32.083]],"v":[[-390,-60],[390,-60],[-240,180],[-287.47,197.841],[-361.687,273.107],[-390,263.672],[-390,197.841],[-390,140]],"c":true}]}],"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":180,"op":360,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Blob 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[390,300,0],"ix":2,"l":2},"a":{"a":0,"k":[390,300,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-132.548],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[132.548,0]],"v":[[390,-60],[0,-60],[0,-300],[150,-300]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"RT","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,132.493]],"o":[[0,0],[0,0],[0,0],[132.493,0],[0,0]],"v":[[390,-60],[0,-60],[0,180],[150.1,180],[390,-59.9]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"RB","np":3,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-132.548],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[-132.548,0]],"v":[[-390,-60],[0,-60],[0,-300],[-150,-300]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"LT","np":3,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":181,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,132.493]],"o":[[0,0],[0,0],[0,0],[-132.493,0],[0,0]],"v":[[-390,-60],[0,-60],[0,180],[-150.1,180],[-390,-59.9]],"c":true}]},{"t":359,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,66.274]],"o":[[0,0],[0,0],[0,0],[-66.274,0],[0,0]],"v":[[-390,-60],[0,-60],[0,180],[-270,180],[-390,60]],"c":true}]}],"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"LB","np":3,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false}],"ip":180,"op":360,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"1 Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[390,300,0],"ix":2,"l":2},"a":{"a":0,"k":[390,300,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":1,"s":[{"i":[[0,132.5],[0,0],[132.5,0],[11.264,-11.424],[0,0],[0,20.894],[0,0],[0,32.083]],"o":[[0,0],[0,132.548],[-22.081,0],[0,0],[-15.637,15.858],[0,0],[0,-17.841],[0,-32.083]],"v":[[-390,-60],[390,-60],[-240,180],[-287.47,197.841],[-361.687,273.107],[-390,263.672],[-390,197.841],[-390,140]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":90,"s":[{"i":[[0,132.5],[0,0],[132.5,0],[11.264,-11.424],[0,0],[15.637,15.858],[0,0],[29.086,29.579]],"o":[[0,0],[0,132.548],[-22.081,0],[0,0],[-15.637,15.858],[0,0],[-11.264,-11.424],[-29.086,-29.579]],"v":[[-390.002,-60],[390,-60],[-40,180],[-87.47,197.841],[-161.687,273.107],[-218.313,273.107],[-292.53,197.841],[-350,140]],"c":true}]},{"t":179,"s":[{"i":[[0,132.5],[0,0],[132.5,0],[11.264,-11.424],[0,0],[15.637,15.858],[0,0],[22.824,0]],"o":[[0,0],[0,132.548],[-22.081,0],[0,0],[-15.637,15.858],[0,0],[-11.265,-11.424],[-132.5,0]],"v":[[-390,-60],[390,-60],[150,180],[102.53,197.841],[28.313,273.107],[-28.313,273.107],[-102.53,197.841],[-150,180]],"c":true}]}],"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Blob","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[390,300,0],"ix":2,"l":2},"a":{"a":0,"k":[390,300,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-132.548],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[132.548,0]],"v":[[390,-60],[0,-60],[0,-300],[150,-300]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"RT","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,132.493]],"o":[[0,0],[0,0],[0,0],[132.493,0],[0,0]],"v":[[390,-60],[0,-60],[0,180],[150.1,180],[390,-59.9]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"RB","np":3,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-132.548],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[-132.548,0]],"v":[[-390,-60],[0,-60],[0,-300],[-150,-300]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"LT","np":3,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":1,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,66.274]],"o":[[0,0],[0,0],[0,0],[-66.274,0],[0,0]],"v":[[-390,-60],[0,-60],[0,180],[-270,180],[-390,60]],"c":true}]},{"t":179,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,132.493]],"o":[[0,0],[0,0],[0,0],[-132.493,0],[0,0]],"v":[[-390,-60],[0,-60],[0,180],[-150.1,180],[-390,-59.9]],"c":true}]}],"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"LB","np":3,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"ct":1,"bm":0}],"markers":[],"props":{}} \ No newline at end of file