From 5e65806ac33fa61083659c8ed2651505f296a064 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 10 May 2022 19:32:23 +0400 Subject: [PATCH] Various improvements --- .../ChatListUI/Sources/ChatContextMenus.swift | 9 +- .../ChatListFilterPresetListController.swift | 64 ++- submodules/PremiumUI/Resources/star.scn | Bin 784607 -> 784634 bytes .../Sources/PremiumIntroScreen.swift | 129 ++++- .../Sources/PremiumLimitScreen.swift | 509 +++++++++++++++--- .../PremiumUI/Sources/RollingCountLabel.swift | 197 +++++++ .../Sources/SolidRoundedButtonNode.swift | 9 +- .../Data/TelegramEngineData.swift | 49 ++ .../Premium/Tmp.imageset/Contents.json | 21 - .../Premium/Tmp.imageset/Tmp.png | Bin 25736 -> 0 bytes .../Premium/Tmp2.imageset/Contents.json | 21 - .../Premium/Tmp2.imageset/Tmp2.png | Bin 4896 -> 0 bytes .../Sources/TelegramRootController.swift | 10 +- 13 files changed, 877 insertions(+), 141 deletions(-) create mode 100644 submodules/PremiumUI/Sources/RollingCountLabel.swift delete mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Tmp.imageset/Contents.json delete mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Tmp.imageset/Tmp.png delete mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Tmp2.imageset/Contents.json delete mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Tmp2.imageset/Tmp2.png diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index 74850a488d..4ffce82f61 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -321,13 +321,18 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch case .limitExceeded: f(.default) - let limitScreen = PremiumLimitScreen(context: context, subject: .pins, action: { + var dismissImpl: (() -> Void)? + let controller = PremiumLimitScreen(context: context, subject: .pins, action: { + dismissImpl?() let premiumScreen = PremiumIntroScreen(context: context, action: { }) chatListController?.push(premiumScreen) }) - chatListController?.push(limitScreen) + chatListController?.push(controller) + dismissImpl = { [weak controller] in + controller?.dismiss() + } } }) }))) diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index c10053e0d4..909e7ae853 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -10,6 +10,7 @@ import ItemListUI import AccountContext import ItemListPeerActionItem import ChatListFilterSettingsHeaderItem +import PremiumUI private final class ChatListFilterPresetListControllerArguments { let context: AccountContext @@ -246,6 +247,17 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch var pushControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController) -> Void)? + let filtersWithCountsSignal = context.engine.peers.updatedChatListFilters() + |> distinctUntilChanged + |> mapToSignal { filters -> Signal<[(ChatListFilter, Int)], NoError> in + return .single(filters.map { filter -> (ChatListFilter, Int) in + return (filter, 0) + }) + } + + let filtersWithCounts = Promise<[(ChatListFilter, Int)]>() + filtersWithCounts.set(filtersWithCountsSignal) + let arguments = ChatListFilterPresetListControllerArguments(context: context, addSuggestedPresed: { title, data in let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in @@ -259,7 +271,42 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch }, openPreset: { preset in pushControllerImpl?(chatListFilterPresetController(context: context, currentPreset: preset, updated: { _ in })) }, addNew: { - pushControllerImpl?(chatListFilterPresetController(context: context, currentPreset: nil, updated: { _ in })) + let _ = combineLatest( + queue: Queue.mainQueue(), + context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId), + TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false), + TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true) + ), + filtersWithCounts.get() |> take(1) + ).start(next: { result, filters in + let (accountPeer, limits, premiumLimits) = result + let limit = limits.maxFoldersCount + let premiumLimit = premiumLimits.maxFoldersCount + if let accountPeer = accountPeer, accountPeer.isPremium { + if filters.count >= premiumLimit { + //printPremiumError + return + } + } else { + if filters.count >= limit { + var dismissImpl: (() -> Void)? + let controller = PremiumLimitScreen(context: context, subject: .folders, action: { + dismissImpl?() + let controller = PremiumIntroScreen(context: context, action: { + + }) + pushControllerImpl?(controller) + }) + pushControllerImpl?(controller) + dismissImpl = { [weak controller] in + controller?.dismiss() + } + return + } + } + pushControllerImpl?(chatListFilterPresetController(context: context, currentPreset: nil, updated: { _ in })) + }) }, setItemWithRevealedOptions: { preset, fromPreset in updateState { state in var state = state @@ -296,15 +343,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch ]) presentControllerImpl?(actionSheet) }) - - let filtersWithCountsSignal = context.engine.peers.updatedChatListFilters() - |> distinctUntilChanged - |> mapToSignal { filters -> Signal<[(ChatListFilter, Int)], NoError> in - return .single(filters.map { filter -> (ChatListFilter, Int) in - return (filter, 0) - }) - } - + let featuredFilters = context.account.postbox.preferencesView(keys: [PreferencesKeys.chatListFiltersFeaturedState]) |> map { preferences -> [ChatListFeaturedFilter] in guard let state = preferences.values[PreferencesKeys.chatListFiltersFeaturedState]?.get(ChatListFiltersFeaturedState.self) else { @@ -313,10 +352,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch return state.filters } |> distinctUntilChanged - - let filtersWithCounts = Promise<[(ChatListFilter, Int)]>() - filtersWithCounts.set(filtersWithCountsSignal) - + let updatedFilterOrder = Promise<[Int32]?>(nil) let preferences = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.chatListFilterSettings]) diff --git a/submodules/PremiumUI/Resources/star.scn b/submodules/PremiumUI/Resources/star.scn index ad5f05949a8ebb09642fd0f127033e8dd1e3b3d9..f0f760eb388ccfe4d974a1b12780c265a88c4257 100644 GIT binary patch delta 9220 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 delta 9349 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 L^8M;jAM*YmEGISG diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index da17b54bd5..bf60593631 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -14,6 +14,14 @@ import SolidRoundedButtonComponent import Markdown import SceneKit +private func deg2rad(_ number: Float) -> Float { + return number * .pi / 180 +} + +private func rad2deg(_ number: Float) -> Float { + return number * 180.0 / .pi +} + private class StarComponent: Component { static func ==(lhs: StarComponent, rhs: StarComponent) -> Bool { return true @@ -41,18 +49,97 @@ private class StarComponent: Component { self.sceneView = SCNView(frame: frame) self.sceneView.backgroundColor = .clear self.sceneView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5) + self.sceneView.isUserInteractionEnabled = false super.init(frame: frame) self.addSubview(self.sceneView) self.setup() + + let panGestureRecoginzer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) + self.addGestureRecognizer(panGestureRecoginzer) + + let tapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) + self.addGestureRecognizer(tapGestureRecoginzer) + + self.disablesInteractiveModalDismiss = true + self.disablesInteractiveTransitionGestureRecognizer = true } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + @objc private func handleTap(_ gesture: UITapGestureRecognizer) { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + + var left = true + if let view = gesture.view { + let point = gesture.location(in: view) + if point.x > view.frame.width / 2.0 { + left = false + } + } + + let initial = node.rotation + let target = SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: left ? -0.6 : 0.6) + + let animation = CABasicAnimation(keyPath: "rotation") + animation.fromValue = NSValue(scnVector4: initial) + animation.toValue = NSValue(scnVector4: target) + animation.duration = 0.25 + animation.timingFunction = CAMediaTimingFunction(name: .easeOut) + animation.fillMode = .forwards + node.addAnimation(animation, forKey: "rotate") + + node.rotation = target + + Queue.mainQueue().after(0.25) { + node.rotation = initial + let springAnimation = CASpringAnimation(keyPath: "rotation") + springAnimation.fromValue = NSValue(scnVector4: target) + springAnimation.toValue = NSValue(scnVector4: SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: 0.0)) + springAnimation.mass = 1.0 + springAnimation.stiffness = 21.0 + springAnimation.damping = 5.8 + springAnimation.duration = springAnimation.settlingDuration * 0.8 + node.addAnimation(springAnimation, forKey: "rotate") + } + } + + private var previousAngle: Float = 0.0 + @objc private func handlePan(_ gesture: UIPanGestureRecognizer) { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + + switch gesture.state { + case .began: + self.previousAngle = 0.0 + case .changed: + let translation = gesture.translation(in: gesture.view) + let anglePan = deg2rad(Float(translation.x)) + + self.previousAngle = anglePan + node.rotation = SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: self.previousAngle) + case .ended: + let velocity = gesture.velocity(in: gesture.view) + + var small = false + if (self.previousAngle < .pi / 2 && self.previousAngle > -.pi / 2) && abs(velocity.x) < 200 { + small = true + } + + self.playAppearanceAnimation(velocity: velocity.x, small: small) + node.rotation = SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: 0.0) + default: + break + } + } + private func setup() { guard let scene = SCNScene(named: "star.scn") else { return @@ -77,11 +164,11 @@ private class StarComponent: Component { self.setupGradientAnimation() self.setupShineAnimation() - self.playAppearanceAnimation() + self.playAppearanceAnimation(boom: true) } private func setupGradientAnimation() { - guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: true) else { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { return } guard let initial = node.geometry?.materials.first?.diffuse.contentsTransform else { @@ -124,21 +211,37 @@ private class StarComponent: Component { node.geometry?.materials.first?.emission.addAnimation(group, forKey: "shimmer") } - private func playAppearanceAnimation() { - guard let scene = self.sceneView.scene, let starNode = scene.rootNode.childNode(withName: "star", recursively: false) else { + private func playAppearanceAnimation(velocity: CGFloat? = nil, small: Bool = false, boom: Bool = false) { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { return } + + if boom, let node = scene.rootNode.childNode(withName: "swirl", recursively: false), let particles = scene.rootNode.childNode(withName: "particles", recursively: false) { + node.physicsField?.isActive = true + Queue.mainQueue().after(1.0) { + node.physicsField?.isActive = false + particles.particleSystems?.first?.birthRate = 0.8 + } + } + let from = node.rotation + var toValue: Float = small ? 0.0 : .pi * 2.0 + if let velocity = velocity, !small && abs(velocity) > 200 && velocity < 0.0 { + toValue *= -1 + } + let to = SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: toValue) + let distance = rad2deg(to.w - from.w) + let springAnimation = CASpringAnimation(keyPath: "rotation") - springAnimation.fromValue = NSValue(scnVector4: SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: 0.0)) - springAnimation.toValue = NSValue(scnVector4: SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: .pi * 2.0)) + springAnimation.fromValue = NSValue(scnVector4: from) + springAnimation.toValue = NSValue(scnVector4: to) springAnimation.mass = 1.0 springAnimation.stiffness = 21.0 springAnimation.damping = 5.8 - springAnimation.duration = 1.5 - springAnimation.initialVelocity = 1.7 + springAnimation.duration = springAnimation.settlingDuration * 0.75 + springAnimation.initialVelocity = velocity.flatMap { abs($0 / CGFloat(distance)) } ?? 1.7 - starNode.addAnimation(springAnimation, forKey: "rotate") + node.addAnimation(springAnimation, forKey: "rotate") } func update(component: StarComponent, availableSize: CGSize, transition: Transition) -> CGSize { @@ -631,7 +734,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { .position(CGPoint(x: fade.size.width / 2.0, y: fade.size.height / 2.0)) ) - size.height += 183.0 + size.height += 183.0 + 10.0 let textColor = theme.list.itemPrimaryTextColor let titleColor = theme.list.itemPrimaryTextColor @@ -1053,7 +1156,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { let topPanelAlpha: CGFloat let titleOffset: CGFloat let titleScale: CGFloat - let titleOffsetDelta = 150.0 - environment.navigationHeight / 2.0 + let titleOffsetDelta = 160.0 - environment.navigationHeight / 2.0 if let topContentOffset = state.topContentOffset { topPanelAlpha = min(30.0, max(0.0, topContentOffset - 64.0)) / 30.0 @@ -1066,7 +1169,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } context.add(star - .position(CGPoint(x: context.availableSize.width / 2.0, y: star.size.height / 2.0 - 18.0 - titleOffset * titleScale)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: star.size.height / 2.0 - 10.0 - titleOffset * titleScale)) .scale(titleScale) ) @@ -1080,7 +1183,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { ) context.add(title - .position(CGPoint(x: context.availableSize.width / 2.0, y: max(150.0 - titleOffset, environment.navigationHeight / 2.0))) + .position(CGPoint(x: context.availableSize.width / 2.0, y: max(160.0 - titleOffset, environment.navigationHeight / 2.0))) .scale(titleScale) ) diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index 318907fbc2..1a8083233d 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -16,6 +16,410 @@ import BundleIconComponent import SolidRoundedButtonComponent import Markdown +private class PremiumLimitAnimationComponent: Component { + private let iconName: String + private let inactiveColor: UIColor + private let activeColors: [UIColor] + private let textColor: UIColor + + init( + iconName: String, + inactiveColor: UIColor, + activeColors: [UIColor], + textColor: UIColor + ) { + self.iconName = iconName + self.inactiveColor = inactiveColor + self.activeColors = activeColors + self.textColor = textColor + } + + static func ==(lhs: PremiumLimitAnimationComponent, rhs: PremiumLimitAnimationComponent) -> Bool { + if lhs.iconName != rhs.iconName { + return false + } + if lhs.inactiveColor != rhs.inactiveColor { + return false + } + if lhs.activeColors != rhs.activeColors { + return false + } + if lhs.textColor != rhs.textColor { + return false + } + return true + } + + final class View: UIView { + private let container: SimpleLayer + private let inactiveBackground: SimpleLayer + + private let activeContainer: SimpleLayer + private let activeBackground: SimpleLayer + + private let badgeView: UIView + private let badgeMaskView: UIView + private let badgeMaskBackgroundView: UIView + private let badgeMaskArrowView: UIImageView + private let badgeForeground: SimpleLayer + private let badgeIcon: UIImageView + private let badgeCountLabel: RollingLabel + + override init(frame: CGRect) { + self.container = SimpleLayer() + self.container.masksToBounds = true + self.container.cornerRadius = 6.0 + + self.inactiveBackground = SimpleLayer() + + self.activeContainer = SimpleLayer() + self.activeContainer.masksToBounds = true + + self.activeBackground = SimpleLayer() + + self.badgeView = UIView() + self.badgeView.layer.anchorPoint = CGPoint(x: 0.5, y: 1.0) + + self.badgeMaskBackgroundView = UIView() + self.badgeMaskBackgroundView.backgroundColor = .white + self.badgeMaskBackgroundView.layer.cornerRadius = 24.0 + + self.badgeMaskArrowView = UIImageView() + self.badgeMaskArrowView.image = generateImage(CGSize(width: 44.0, height: 12.0), rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + context.setFillColor(UIColor.white.cgColor) + context.scaleBy(x: 3.76, y: 3.76) + context.translateBy(x: -9.3, y: -12.7) + try? drawSvgPath(context, path: "M6.4,0.0 C2.9,0.0 0.0,2.84 0.0,6.35 C0.0,9.86 2.9,12.7 6.4,12.7 H9.302 H11.3 C11.7,12.7 12.1,12.87 12.4,13.17 L14.4,15.13 C14.8,15.54 15.5,15.54 15.9,15.13 L17.8,13.17 C18.1,12.87 18.5,12.7 18.9,12.7 H20.9 H23.6 C27.1,12.7 29.9,9.86 29.9,6.35 C29.9,2.84 27.1,0.0 23.6,0.0 Z ") + }) + + self.badgeMaskView = UIView() + self.badgeMaskView.addSubview(self.badgeMaskBackgroundView) + self.badgeMaskView.addSubview(self.badgeMaskArrowView) + self.badgeView.mask = self.badgeMaskView + + self.badgeForeground = SimpleLayer() + + self.badgeIcon = UIImageView() + self.badgeIcon.contentMode = .center + + self.badgeCountLabel = RollingLabel() + self.badgeCountLabel.font = Font.with(size: 24.0, design: .round, weight: .semibold, traits: []) + self.badgeCountLabel.textColor = .white + self.badgeCountLabel.text(num: 0) + + super.init(frame: frame) + + self.layer.addSublayer(self.container) + self.container.addSublayer(self.inactiveBackground) + self.container.addSublayer(self.activeContainer) + self.activeContainer.addSublayer(self.activeBackground) + + self.addSubview(self.badgeView) + self.badgeView.layer.addSublayer(self.badgeForeground) + self.badgeView.addSubview(self.badgeIcon) + self.badgeView.addSubview(self.badgeCountLabel) + + self.isUserInteractionEnabled = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var didPlayAppearanceAnimation = false + func playAppearanceAnimation(availableSize: CGSize) { + self.badgeView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.4, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + + let now = self.badgeView.layer.convertTime(CACurrentMediaTime(), from: nil) + + let positionAnimation = CABasicAnimation(keyPath: "position.x") + positionAnimation.fromValue = NSValue(cgPoint: CGPoint(x: -availableSize.width / 2.0, y: 0.0)) + positionAnimation.toValue = NSValue(cgPoint: CGPoint()) + positionAnimation.isAdditive = true + positionAnimation.duration = 0.5 + positionAnimation.fillMode = .forwards + positionAnimation.beginTime = now + + let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z") + rotateAnimation.fromValue = 0.0 as NSNumber + rotateAnimation.toValue = 0.2 as NSNumber + rotateAnimation.isAdditive = true + rotateAnimation.duration = 0.2 + rotateAnimation.beginTime = now + 0.5 + rotateAnimation.fillMode = .forwards + rotateAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut) + + let returnAnimation = CABasicAnimation(keyPath: "transform.rotation.z") + returnAnimation.fromValue = 0.2 as NSNumber + returnAnimation.toValue = 0.0 as NSNumber + returnAnimation.isAdditive = true + returnAnimation.duration = 0.18 + returnAnimation.beginTime = now + 0.5 + 0.2 + returnAnimation.fillMode = .forwards + returnAnimation.timingFunction = CAMediaTimingFunction(name: .easeIn) + + self.badgeView.layer.add(positionAnimation, forKey: "appearance1") + self.badgeView.layer.add(rotateAnimation, forKey: "appearance2") + self.badgeView.layer.add(returnAnimation, forKey: "appearance3") + + self.badgeCountLabel.text(num: 4) + } + + var previousAvailableSize: CGSize? + func update(component: PremiumLimitAnimationComponent, availableSize: CGSize, transition: Transition) -> CGSize { + self.inactiveBackground.backgroundColor = component.inactiveColor.cgColor + self.activeBackground.backgroundColor = component.activeColors.last?.cgColor + + self.badgeIcon.image = UIImage(bundleImageName: component.iconName)?.withRenderingMode(.alwaysTemplate) + self.badgeIcon.tintColor = component.textColor + + let lineHeight: CGFloat = 30.0 + let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - lineHeight), size: CGSize(width: availableSize.width, height: lineHeight)) + self.container.frame = containerFrame + + self.inactiveBackground.frame = CGRect(origin: .zero, size: CGSize(width: containerFrame.width / 2.0 - 1.0, height: lineHeight)) + self.activeContainer.frame = CGRect(origin: CGPoint(x: containerFrame.width / 2.0 + 1.0, y: 0.0), size: CGSize(width: containerFrame.width / 2.0 - 1.0, height: lineHeight)) + + self.activeBackground.bounds = CGRect(origin: .zero, size: CGSize(width: containerFrame.width * 3.0 / 2.0, height: lineHeight)) + if self.activeBackground.animation(forKey: "movement") == nil { + self.activeBackground.position = CGPoint(x: containerFrame.width * 3.0 / 4.0, y: lineHeight / 2.0) + } + + let badgeSize = CGSize(width: 82.0, height: 48.0 + 12.0) + self.badgeMaskView.frame = CGRect(origin: .zero, size: badgeSize) + self.badgeMaskBackgroundView.frame = CGRect(origin: .zero, size: CGSize(width: badgeSize.width, height: 48.0)) + self.badgeMaskArrowView.frame = CGRect(origin: CGPoint(x: (badgeSize.width - 44.0) / 2.0, y: badgeSize.height - 12.0), size: CGSize(width: 44.0, height: 12.0)) + + self.badgeView.bounds = CGRect(origin: .zero, size: badgeSize) + self.badgeView.center = CGPoint(x: availableSize.width / 2.0, y: 82.0) + if self.badgeForeground.animation(forKey: "movement") == nil { + self.badgeForeground.position = CGPoint(x: badgeSize.width * 3.0 / 2.0, y: badgeSize.height / 2.0) + } + self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: badgeSize.width * 3.0, height: badgeSize.height)) + + self.badgeIcon.frame = CGRect(x: 15.0, y: 9.0, width: 30.0, height: 30.0) + + self.badgeCountLabel.frame = CGRect(x: badgeSize.width - 36.0, y: 10.0, width: 30.0, height: 48.0) + + if !self.didPlayAppearanceAnimation { + self.didPlayAppearanceAnimation = true + self.playAppearanceAnimation(availableSize: availableSize) + } + + if self.previousAvailableSize != availableSize { + self.previousAvailableSize = availableSize + + var locations: [CGFloat] = [] + let delta = 1.0 / CGFloat(component.activeColors.count - 1) + for i in 0 ..< component.activeColors.count { + locations.append(delta * CGFloat(i)) + } + + let gradient = generateGradientImage(size: CGSize(width: 200.0, height: 60.0), colors: component.activeColors, locations: locations, direction: .horizontal) + self.badgeForeground.contentsGravity = .resizeAspectFill + self.badgeForeground.contents = gradient?.cgImage + + self.activeBackground.contentsGravity = .resizeAspectFill + self.activeBackground.contents = gradient?.cgImage + + self.setupGradientAnimations() + } + + return availableSize + } + + private func setupGradientAnimations() { + if let _ = self.badgeForeground.animation(forKey: "movement") { + } else { + CATransaction.begin() + + let badgeOffset = (self.badgeForeground.frame.width - self.badgeView.bounds.width) / 2.0 + let badgePreviousValue = self.badgeForeground.position.x + var badgeNewValue: CGFloat = badgeOffset + if badgeOffset - badgePreviousValue < self.badgeForeground.frame.width * 0.25 { + badgeNewValue -= self.badgeForeground.frame.width * 0.35 + } + self.badgeForeground.position = CGPoint(x: badgeNewValue, y: self.badgeForeground.bounds.size.height / 2.0) + + let lineOffset = (self.activeBackground.frame.width - self.activeContainer.bounds.width) / 2.0 + let linePreviousValue = self.activeBackground.position.x + var lineNewValue: CGFloat = lineOffset + if lineOffset - linePreviousValue < self.activeBackground.frame.width * 0.25 { + lineNewValue -= self.activeBackground.frame.width * 0.35 + } + self.activeBackground.position = CGPoint(x: lineNewValue, y: self.activeBackground.bounds.size.height / 2.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") + + let lineAnimation = CABasicAnimation(keyPath: "position.x") + lineAnimation.duration = 4.5 + lineAnimation.fromValue = linePreviousValue + lineAnimation.toValue = lineNewValue + lineAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + self.activeBackground.add(lineAnimation, forKey: "movement") + + CATransaction.commit() + } + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} + +public final class PremiumLimitDisplayComponent: CombinedComponent { + public let inactiveColor: UIColor + public let activeColors: [UIColor] + public let inactiveTitle: String + public let inactiveTitleColor: UIColor + public let activeTitle: String + public let activeValue: String + public let activeTitleColor: UIColor + public let badgeIconName: String + public let badgeValue: String + + public init( + inactiveColor: UIColor, + activeColors: [UIColor], + inactiveTitle: String, + inactiveTitleColor: UIColor, + activeTitle: String, + activeValue: String, + activeTitleColor: UIColor, + badgeIconName: String, + badgeValue: String + ) { + self.inactiveColor = inactiveColor + self.activeColors = activeColors + self.inactiveTitle = inactiveTitle + self.inactiveTitleColor = inactiveTitleColor + self.activeTitle = activeTitle + self.activeValue = activeValue + self.activeTitleColor = activeTitleColor + self.badgeIconName = badgeIconName + self.badgeValue = badgeValue + } + + public static func ==(lhs: PremiumLimitDisplayComponent, rhs: PremiumLimitDisplayComponent) -> Bool { + if lhs.inactiveColor != rhs.inactiveColor { + return false + } + if lhs.activeColors != rhs.activeColors { + return false + } + if lhs.inactiveTitle != rhs.inactiveTitle { + return false + } + if lhs.inactiveTitleColor != rhs.inactiveTitleColor { + return false + } + if lhs.activeTitle != rhs.activeTitle { + return false + } + if lhs.activeValue != rhs.activeValue { + return false + } + if lhs.activeTitleColor != rhs.activeTitleColor { + return false + } + if lhs.badgeIconName != rhs.badgeIconName { + return false + } + if lhs.badgeValue != rhs.badgeValue { + return false + } + return true + } + + public static var body: Body { + let inactiveTitle = Child(Text.self) + let activeTitle = Child(Text.self) + let activeValue = Child(Text.self) + let animation = Child(PremiumLimitAnimationComponent.self) + + return { context in + let component = context.component + + let height: CGFloat = 120.0 + let lineHeight: CGFloat = 30.0 + + let inactiveTitle = inactiveTitle.update( + component: Text( + text: component.inactiveTitle, + font: Font.semibold(15.0), + color: component.inactiveTitleColor + ), + availableSize: context.availableSize, + transition: context.transition + ) + + let activeTitle = activeTitle.update( + component: Text( + text: component.activeTitle, + font: Font.semibold(15.0), + color: component.activeTitleColor + ), + availableSize: context.availableSize, + transition: context.transition + ) + + let activeValue = activeValue.update( + component: Text( + text: component.activeValue, + font: Font.semibold(15.0), + color: component.activeTitleColor + ), + availableSize: context.availableSize, + transition: context.transition + ) + + let animation = animation.update( + component: PremiumLimitAnimationComponent( + iconName: component.badgeIconName, + inactiveColor: component.inactiveColor, + activeColors: component.activeColors, + textColor: component.activeTitleColor + ), + availableSize: CGSize(width: context.availableSize.width, height: height), + transition: context.transition + ) + + context.add(animation + .position(CGPoint(x: context.availableSize.width / 2.0, y: height / 2.0)) + ) + + context.add(inactiveTitle + .position(CGPoint(x: inactiveTitle.size.width / 2.0 + 12.0, y: height - lineHeight / 2.0)) + ) + + context.add(activeTitle + .position(CGPoint(x: context.availableSize.width / 2.0 + 1.0 + activeTitle.size.width / 2.0 + 12.0, y: height - lineHeight / 2.0)) + ) + + context.add(activeValue + .position(CGPoint(x: context.availableSize.width - activeValue.size.width / 2.0 - 12.0, y: height - lineHeight / 2.0)) + ) + + return CGSize(width: context.availableSize.width, height: height) + } + } +} + private final class LimitSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -78,13 +482,9 @@ private final class LimitSheetContent: CombinedComponent { } static var body: Body { - let badgeBackground = Child(RoundedRectangle.self) - let badgeIcon = Child(BundleIconComponent.self) - let badgeText = Child(MultilineTextComponent.self) - let title = Child(MultilineTextComponent.self) let text = Child(MultilineTextComponent.self) - + let limit = Child(PremiumLimitDisplayComponent.self) let button = Child(SolidRoundedButtonComponent.self) return { context in @@ -100,68 +500,40 @@ private final class LimitSheetContent: CombinedComponent { let textSideInset: CGFloat = 24.0 + environment.safeInsets.left let iconName: String - let badgeString: String + let badgeValue: String let string: String + let premiumValue: String switch subject { case .folders: let limit = state.limits.maxFoldersCount let premiumLimit = state.premiumLimits.maxFoldersCount iconName = "Premium/Folder" - badgeString = "\(limit)" + badgeValue = "\(limit)" string = strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string + premiumValue = "\(premiumLimit)" case .chatsInFolder: let limit = state.limits.maxFolderChatsCount let premiumLimit = state.premiumLimits.maxFolderChatsCount iconName = "Premium/Chat" - badgeString = "\(limit)" + badgeValue = "\(limit)" string = strings.Premium_MaxChatsInFolderCountText("\(limit)", "\(premiumLimit)").string + premiumValue = "\(premiumLimit)" case .pins: - let limit = state.limits.maxPinnedChatCount - let premiumLimit = state.premiumLimits.maxPinnedChatCount + let limit = 4//state.limits.maxPinnedChatCount + let premiumLimit = 6//state.premiumLimits.maxPinnedChatCount iconName = "Premium/Pin" - badgeString = "\(limit)" + badgeValue = "\(limit)" string = strings.Premium_MaxPinsText("\(limit)", "\(premiumLimit)").string + premiumValue = "\(premiumLimit)" case .files: let limit = 2048 * 1024 * 1024 //state.limits.maxPinnedChatCount let premiumLimit = 4096 * 1024 * 1024 //state.premiumLimits.maxPinnedChatCount iconName = "Premium/File" - badgeString = dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) + badgeValue = dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) string = strings.Premium_MaxFileSizeText(dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))).string + premiumValue = dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) } - let badgeIcon = badgeIcon.update( - component: BundleIconComponent( - name: iconName, - tintColor: .white - ), - availableSize: context.availableSize, - transition: .immediate - ) - - let badgeText = badgeText.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString( - string: badgeString, - font: Font.with(size: 24.0, design: .round, weight: .semibold, traits: []), - textColor: .white, - paragraphAlignment: .center - )), - horizontalAlignment: .center, - maximumNumberOfLines: 1 - ), - availableSize: context.availableSize, - transition: .immediate - ) - - let badgeBackground = badgeBackground.update( - component: RoundedRectangle( - colors: [UIColor(rgb: 0xa34fcf), UIColor(rgb: 0xc8498a), UIColor(rgb: 0xff7a23)], - cornerRadius: 23.5 - ), - availableSize: CGSize(width: badgeText.size.width + 67.0, height: 47.0), - transition: .immediate - ) - let title = title.update( component: MultilineTextComponent( text: .plain(NSAttributedString( @@ -195,19 +567,45 @@ private final class LimitSheetContent: CombinedComponent { transition: .immediate ) + let limit = limit.update( + component: PremiumLimitDisplayComponent( + inactiveColor: UIColor(rgb: 0xE9E9EA), + activeColors: [ + UIColor(rgb: 0x0077ff), + UIColor(rgb: 0x6b93ff), + UIColor(rgb: 0x8878ff), + UIColor(rgb: 0xe46ace) + ], + inactiveTitle: "Free", + inactiveTitleColor: .black, + activeTitle: "Premium", + activeValue: premiumValue, + activeTitleColor: .white, + badgeIconName: iconName, + badgeValue: badgeValue + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + let button = button.update( component: SolidRoundedButtonComponent( title: strings.Premium_IncreaseLimit, theme: SolidRoundedButtonComponent.Theme( backgroundColor: .black, - backgroundColors: [UIColor(rgb: 0x407af0), UIColor(rgb: 0x9551e8), UIColor(rgb: 0xbf499a), UIColor(rgb: 0xf17b30)], + backgroundColors: [ + UIColor(rgb: 0x0077ff), + UIColor(rgb: 0x6b93ff), + UIColor(rgb: 0x8878ff), + UIColor(rgb: 0xe46ace) + ], foregroundColor: .white ), font: .bold, fontSize: 17.0, height: 50.0, cornerRadius: 10.0, - gloss: false, + gloss: true, iconName: "Premium/X2", iconPosition: .right, action: { [weak component] in @@ -224,23 +622,12 @@ private final class LimitSheetContent: CombinedComponent { let width = context.availableSize.width - let badgeFrame = CGRect(origin: CGPoint(x: floor((context.availableSize.width - badgeBackground.size.width) / 2.0), y: 33.0), size: badgeBackground.size) - context.add(badgeBackground - .position(CGPoint(x: badgeFrame.midX, y: badgeFrame.midY)) + context.add(limit + .position(CGPoint(x: width / 2.0, y: limit.size.height / 2.0 + 44.0)) ) - let badgeIconFrame = CGRect(origin: CGPoint(x: badgeFrame.minX + 18.0, y: badgeFrame.minY + floor((badgeFrame.height - badgeIcon.size.height) / 2.0)), size: badgeIcon.size) - context.add(badgeIcon - .position(CGPoint(x: badgeIconFrame.midX, y: badgeIconFrame.midY)) - ) - - let badgeTextFrame = CGRect(origin: CGPoint(x: badgeFrame.maxX - badgeText.size.width - 15.0, y: badgeFrame.minY + floor((badgeFrame.height - badgeText.size.height) / 2.0)), size: badgeText.size) - context.add(badgeText - .position(CGPoint(x: badgeTextFrame.midX, y: badgeTextFrame.midY)) - ) - context.add(title - .position(CGPoint(x: width / 2.0, y: 28.0)) + .position(CGPoint(x: width / 2.0, y: 28.0)) ) context.add(text .position(CGPoint(x: width / 2.0, y: 228.0)) diff --git a/submodules/PremiumUI/Sources/RollingCountLabel.swift b/submodules/PremiumUI/Sources/RollingCountLabel.swift new file mode 100644 index 0000000000..521e3ca275 --- /dev/null +++ b/submodules/PremiumUI/Sources/RollingCountLabel.swift @@ -0,0 +1,197 @@ +import UIKit + +private extension UILabel { + func textWidth() -> CGFloat { + return UILabel.textWidth(label: self) + } + + class func textWidth(label: UILabel) -> CGFloat { + return textWidth(label: label, text: label.text!) + } + + class func textWidth(label: UILabel, text: String) -> CGFloat { + return textWidth(font: label.font, text: text) + } + + class func textWidth(font: UIFont, text: String) -> CGFloat { + let myText = text as NSString + + let rect = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) + let labelSize = myText.boundingRect(with: rect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil) + return ceil(labelSize.width) + } +} + +open class RollingLabel: UILabel { + private var fullText = "" + + private var suffix: String = "" + open var showSymbol = false + private var scrollLayers: [CAScrollLayer] = [] + private var scrollLabels: [UILabel] = [] + private let duration = 1.12 + private let durationOffset = 0.2 + private let textsNotAnimated = [","] + + public func text(num: Int) { + self.configure(with: num) + self.text = " " + self.animate() + } + + public func setPrefix(prefix: String) { + self.suffix = prefix + } + + private func configure(with number: Int) { + fullText = String(number) + + clean() + setupSubviews() + } + + private func animate(ascending: Bool = true) { + createAnimations(ascending: ascending) + } + + private func clean() { + self.text = nil + self.subviews.forEach { $0.removeFromSuperview() } + self.layer.sublayers?.forEach { $0.removeFromSuperlayer() } + scrollLayers.removeAll() + scrollLabels.removeAll() + } + + private func setupSubviews() { + let stringArray = fullText.map { String($0) } + var x: CGFloat = 0 + let y: CGFloat = 0 + if self.textAlignment == .center { + if showSymbol { + self.text = "\(fullText) \(suffix)" + } else { + self.text = fullText + } + let w = UILabel.textWidth(font: self.font, text: self.text ?? "") + self.text = "" // 초기화 + x = -(w / 2) + } else if self.textAlignment == .right { + if showSymbol { + self.text = "\(fullText) \(suffix) " + } else { + self.text = fullText + } + let w = UILabel.textWidth(font: self.font, text: self.text ?? "") + self.text = "" // 초기화 + x = -w + } + + if showSymbol { + let wLabel = UILabel() + wLabel.frame.origin = CGPoint(x: x, y: y) + wLabel.textColor = textColor + wLabel.font = font + wLabel.text = "\(suffix) " + wLabel.textAlignment = .center + wLabel.sizeToFit() + self.addSubview(wLabel) + x += wLabel.bounds.width + } + + stringArray.enumerated().forEach { index, text in + if textsNotAnimated.contains(text) { + let label = UILabel() + label.frame.origin = CGPoint(x: x, y: y) + label.textColor = textColor + label.font = font + label.text = text + label.textAlignment = .center + label.sizeToFit() + self.addSubview(label) + + x += label.bounds.width + } else { + let label = UILabel() + label.frame.origin = CGPoint(x: x, y: y) + label.textColor = textColor + label.font = font + label.text = "0" + label.textAlignment = .center + label.sizeToFit() + createScrollLayer(to: label, text: text) + + x += label.bounds.width + } + } + } + + private func createScrollLayer(to label: UILabel, text: String) { + let scrollLayer = CAScrollLayer() + scrollLayer.frame = label.frame + scrollLayers.append(scrollLayer) + self.layer.addSublayer(scrollLayer) + + createContentForLayer(scrollLayer: scrollLayer, text: text) + } + + private func createContentForLayer(scrollLayer: CAScrollLayer, text: String) { + var textsForScroll: [String] = [] + + let max: Int + var found = false + if let val = Int(text) { + max = val + found = true + } else { + max = 9 + } + + for i in 0...max { + let str = String(i) + textsForScroll.append(str) + } + if !found { + textsForScroll.append(text) + } + + var height: CGFloat = 0 + for text in textsForScroll { + let label = UILabel() + label.text = text + label.textColor = textColor + label.font = font + label.textAlignment = .center + label.frame = CGRect(x: 0, y: height, width: scrollLayer.frame.width, height: scrollLayer.frame.height) + scrollLayer.addSublayer(label.layer) + scrollLabels.append(label) + + height = label.frame.maxY + } + } + + private func createAnimations(ascending: Bool) { + var offset: CFTimeInterval = 0.0 + + for scrollLayer in scrollLayers { + let maxY = scrollLayer.sublayers?.last?.frame.origin.y ?? 0.0 + + let animation = CABasicAnimation(keyPath: "sublayerTransform.translation.y") + animation.duration = duration + offset + animation.timingFunction = CAMediaTimingFunction(name: .easeOut) + + if ascending { + animation.fromValue = maxY + animation.toValue = 0 + } else { + animation.fromValue = 0 + animation.toValue = maxY + } + + scrollLayer.scrollMode = .vertically + scrollLayer.add(animation, forKey: nil) + scrollLayer.scroll(to: CGPoint(x: 0, y: maxY)) + + offset += self.durationOffset + } + } +} diff --git a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift index fa164d897a..895699be86 100644 --- a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift +++ b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift @@ -645,9 +645,7 @@ public final class SolidRoundedButtonView: UIView { let previousValue = buttonBackgroundAnimationView.center.x var newValue: CGFloat = offset if offset - previousValue < buttonBackgroundAnimationView.frame.width * 0.25 { - newValue -= CGFloat.random(in: buttonBackgroundAnimationView.frame.width * 0.3 ..< buttonBackgroundAnimationView.frame.width * 0.4) - } else { -// newValue -= CGFloat.random(in: 0.0 ..< buttonBackgroundAnimationView.frame.width * 0.1) + newValue -= buttonBackgroundAnimationView.frame.width * 0.35 } buttonBackgroundAnimationView.center = CGPoint(x: newValue, y: buttonBackgroundAnimationView.bounds.size.height / 2.0) @@ -794,7 +792,10 @@ public final class SolidRoundedButtonView: UIView { } if let buttonBackgroundAnimationView = self.buttonBackgroundAnimationView { - transition.updateFrame(view: buttonBackgroundAnimationView, frame: CGRect(origin: CGPoint(), size: CGSize(width: buttonSize.width * 2.4, height: buttonSize.height))) + if buttonBackgroundAnimationView.layer.animation(forKey: "movement") == nil { + buttonBackgroundAnimationView.center = CGPoint(x: buttonSize.width * 2.4 / 2.0, y: buttonSize.height / 2.0) + } + buttonBackgroundAnimationView.bounds = CGRect(origin: CGPoint(), size: CGSize(width: buttonSize.width * 2.4, height: buttonSize.height)) self.setupGradientAnimations() } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/TelegramEngineData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/TelegramEngineData.swift index 7ded3bf636..3a2b8e5a24 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/TelegramEngineData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/TelegramEngineData.swift @@ -128,6 +128,37 @@ public extension TelegramEngine { ) } } + + public func subscribe< + T0: TelegramEngineDataItem, + T1: TelegramEngineDataItem, + T2: TelegramEngineDataItem + >( + _ t0: T0, + _ t1: T1, + _ t2: T2 + ) -> Signal< + ( + T0.Result, + T1.Result, + T2.Result + ), + NoError> { + return self._subscribe(items: [ + t0 as! AnyPostboxViewDataItem, + t1 as! AnyPostboxViewDataItem, + t2 as! AnyPostboxViewDataItem + ]) + |> map { results -> (T0.Result, T1.Result, T2.Result) in + return ( + results[0] as! T0.Result, + results[1] as! T1.Result, + results[2] as! T2.Result + ) + } + } + + public func get< T0: TelegramEngineDataItem, T1: TelegramEngineDataItem @@ -142,5 +173,23 @@ public extension TelegramEngine { NoError> { return self.subscribe(t0, t1) |> take(1) } + + public func get< + T0: TelegramEngineDataItem, + T1: TelegramEngineDataItem, + T2: TelegramEngineDataItem + >( + _ t0: T0, + _ t1: T1, + _ t2: T2 + ) -> Signal< + ( + T0.Result, + T1.Result, + T2.Result + ), + NoError> { + return self.subscribe(t0, t1, t2) |> take(1) + } } } diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Tmp.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Tmp.imageset/Contents.json deleted file mode 100644 index 96acb63cc3..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Premium/Tmp.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "Tmp.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Tmp.imageset/Tmp.png b/submodules/TelegramUI/Images.xcassets/Premium/Tmp.imageset/Tmp.png deleted file mode 100644 index 460cf4ce24ad7adcb9a681c67a2a6e81fccaad5f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25736 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 LNkvXXu0mjfyyWZE#+KcVA4dLOX=HbRXQdryy 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