diff --git a/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift b/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift index 26cc5ae129..4d1c156ffb 100644 --- a/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift @@ -281,7 +281,7 @@ final class BrowserNavigationBarComponent: Component { if let leftItemsBackground { let leftItemsFrame = CGRect(origin: CGPoint(x: sideInset - (leftItemsWidth / 2.0 * 0.35 * component.collapseFraction), y: component.topInset + contentHeight / 2.0 + verticalOffset - panelHeight / 2.0), size: CGSize(width: leftItemsWidth, height: panelHeight)) leftItemsBackgroundTransition.setFrame(view: leftItemsBackground, frame: leftItemsFrame) - leftItemsBackground.update(size: leftItemsFrame.size, shape: .roundedRect(cornerRadius: leftItemsFrame.height * 0.5), isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: component.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: leftItemsBackgroundTransition) + leftItemsBackground.update(size: leftItemsFrame.size, cornerRadius: leftItemsFrame.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: component.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: leftItemsBackgroundTransition) leftItemsBackgroundTransition.setScale(view: leftItemsBackground, scale: 1.0 - 0.999 * component.collapseFraction) leftItemsBackgroundTransition.setAlpha(view: leftItemsBackground.contentView, alpha: 1.0 - component.collapseFraction) @@ -293,7 +293,7 @@ final class BrowserNavigationBarComponent: Component { if let rightItemsBackground { let rightItemsFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - rightItemsWidth * (1.0 - component.collapseFraction) + (rightItemsWidth / 2.0 * 0.35 * component.collapseFraction), y: component.topInset + contentHeight / 2.0 + verticalOffset - panelHeight / 2.0), size: CGSize(width: rightItemsWidth, height: panelHeight)) rightItemsBackgroundTransition.setFrame(view: rightItemsBackground, frame: rightItemsFrame) - rightItemsBackground.update(size: rightItemsFrame.size, shape: .roundedRect(cornerRadius: rightItemsFrame.height * 0.5), isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: component.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: rightItemsBackgroundTransition) + rightItemsBackground.update(size: rightItemsFrame.size, cornerRadius: rightItemsFrame.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: component.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: rightItemsBackgroundTransition) rightItemsBackgroundTransition.setScale(view: rightItemsBackground, scale: 1.0 - 0.999 * component.collapseFraction) rightItemsBackgroundTransition.setAlpha(view: rightItemsBackground.contentView, alpha: 1.0 - component.collapseFraction) diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index 733f7d2356..92999c1ea9 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -19,30 +19,46 @@ public extension UIView { public extension CALayer { func animate(from: Any, to: Any, keyPath: String, duration: Double, delay: Double, curve: ComponentTransition.Animation.Curve, removeOnCompletion: Bool, additive: Bool, completion: ((Bool) -> Void)? = nil, key: String? = nil) { - let timingFunction: String - let mediaTimingFunction: CAMediaTimingFunction? - switch curve { - case .spring: - timingFunction = kCAMediaTimingFunctionSpring - mediaTimingFunction = nil - default: - timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue - mediaTimingFunction = curve.asTimingFunction() + if case let .bounce(stiffness, damping) = curve { + self.animateSpring( + from: from, + to: to, + keyPath: keyPath, + duration: duration, + delay: delay, + stiffness: stiffness, + damping: damping, + removeOnCompletion: removeOnCompletion, + additive: additive, + completion: completion, + key: key + ) + } else { + let timingFunction: String + let mediaTimingFunction: CAMediaTimingFunction? + switch curve { + case .spring: + timingFunction = kCAMediaTimingFunctionSpring + mediaTimingFunction = nil + default: + timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue + mediaTimingFunction = curve.asTimingFunction() + } + + self.animate( + from: from, + to: to, + keyPath: keyPath, + timingFunction: timingFunction, + duration: duration, + delay: delay, + mediaTimingFunction: mediaTimingFunction, + removeOnCompletion: removeOnCompletion, + additive: additive, + completion: completion, + key: key + ) } - - self.animate( - from: from, - to: to, - keyPath: keyPath, - timingFunction: timingFunction, - duration: duration, - delay: delay, - mediaTimingFunction: mediaTimingFunction, - removeOnCompletion: removeOnCompletion, - additive: additive, - completion: completion, - key: key - ) } } @@ -55,21 +71,23 @@ private extension ComponentTransition.Animation.Curve { return CAMediaTimingFunction(name: .linear) case let .custom(a, b, c, d): return CAMediaTimingFunction(controlPoints: a, b, c, d) - case .spring: + case .spring, .bounce: preconditionFailure() } } var viewAnimationOptions: UIView.AnimationOptions { switch self { - case .linear: - return [.curveLinear] - case .easeInOut: - return [.curveEaseInOut] - case .spring: - return UIView.AnimationOptions(rawValue: 7 << 16) - case .custom: - return [] + case .linear: + return [.curveLinear] + case .easeInOut: + return [.curveEaseInOut] + case .spring: + return UIView.AnimationOptions(rawValue: 7 << 16) + case .custom: + return [] + case .bounce: + return [] } } } @@ -95,13 +113,20 @@ public extension ComponentTransition { if allowUserInteraction { options.insert(.allowUserInteraction) } - if case .spring = curve { - CALayer.push(CALayerSpringParametersOverride()) - UIView.animate(withDuration: duration, delay: delay, usingSpringWithDamping: 500.0, initialSpringVelocity: 0.0, options: options, animations: { + switch curve { + case .spring, .bounce: + var parameters: CALayerSpringParametersOverrideParameters? + var dampingValue: CGFloat = 500.0 + if case let .bounce(stiffness, damping) = curve { + dampingValue = damping + parameters = CALayerSpringParametersOverrideParameters(stiffness: stiffness, damping: damping, duration: duration) + } + CALayer.push(CALayerSpringParametersOverride(parameters: parameters)) + UIView.animate(withDuration: duration, delay: delay, usingSpringWithDamping: dampingValue, initialSpringVelocity: 0.0, options: options, animations: { f() }, completion: completion) CALayer.popSpringParametersOverride() - } else { + default: UIView.animate(withDuration: duration, delay: delay, options: options, animations: { f() }, completion: completion) @@ -117,6 +142,7 @@ public struct ComponentTransition { case spring case linear case custom(Float, Float, Float, Float) + case bounce(stiffness: CGFloat, damping: CGFloat) public func solve(at offset: CGFloat) -> CGFloat { switch self { @@ -128,6 +154,9 @@ public struct ComponentTransition { return offset case let .custom(c1x, c1y, c2x, c2y): return bezierPoint(CGFloat(c1x), CGFloat(c1y), CGFloat(c2x), CGFloat(c2y), offset) + case .bounce: + assertionFailure() + return listViewAnimationCurveSystem(offset) } } @@ -197,8 +226,6 @@ public struct ComponentTransition { switch self.animation { case .none: view.frame = frame - //view.bounds = CGRect(origin: view.bounds.origin, size: frame.size) - //view.layer.position = CGPoint(x: frame.midX, y: frame.midY) view.layer.removeAnimation(forKey: "position") view.layer.removeAnimation(forKey: "bounds") view.layer.removeAnimation(forKey: "bounds.size") @@ -215,8 +242,6 @@ public struct ComponentTransition { } view.frame = frame - //view.bounds = CGRect(origin: previousBounds.origin, size: frame.size) - //view.center = CGPoint(x: frame.midX, y: frame.midY) let anchorPoint = view.layer.anchorPoint let updatedPosition = CGPoint(x: frame.minX + frame.width * anchorPoint.x, y: frame.minY + frame.height * anchorPoint.y) @@ -1440,4 +1465,138 @@ public struct ComponentTransition { ) } } + + public func animatePositionParabollic(layer: CALayer, from fromPosition: CGPoint, to toPosition: CGPoint, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + switch self.animation { + case .none: + completion?(true) + case let .curve(duration, curve): + let timingFunction: String + let mediaTimingFunction: CAMediaTimingFunction? + switch curve { + case .spring: + timingFunction = kCAMediaTimingFunctionSpring + mediaTimingFunction = nil + case .linear: + timingFunction = CAMediaTimingFunctionName.linear.rawValue + mediaTimingFunction = curve.asTimingFunction() + default: + timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue + mediaTimingFunction = curve.asTimingFunction() + } + + let keyframes = generateParabolicMotionKeyframes(from: fromPosition, to: toPosition) + layer.animateKeyframes(values: keyframes.map(NSValue.init(cgPoint:)), duration: duration, keyPath: "position", timingFunction: timingFunction, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: true, additive: additive, completion: { value in + completion?(value) + }) + } + } +} + +private func generateParabolicMotionKeyframes( + from start: CGPoint, + to end: CGPoint, + steps: Int = 10 +) -> [CGPoint] { + let dampingRatio: CGFloat = 0.65 // < 1 => overshoot + let angularFrequency: CGFloat = 16.0 // higher => snappier + let liftFactor: CGFloat = 0.25 + let minLift: CGFloat = 24 + let maxLift: CGFloat = 180 + + let dx = end.x - start.x + let dy = end.y - start.y + let chord = hypot(dx, dy) + if chord < 0.001 { return Array(repeating: start, count: steps) } + + // Control point (direction-aware arc: down arcs down, up arcs up) + let liftMag = min(max(chord * liftFactor, minLift), maxLift) + let signedLift: CGFloat = (dy > 0) ? liftMag : -liftMag + + let control = CGPoint( + x: (start.x + end.x) * 0.5, + y: (start.y + end.y) * 0.5 + signedLift + ) + + // Quadratic Bézier point + let bezier: (CGFloat) -> CGPoint = { t in + let tt: CGFloat = min(max(t, 0.0), 1.0) + let u: CGFloat = 1.0 - tt + let x: CGFloat = (u*u*start.x) + (2*u*tt*control.x) + (tt*tt*end.x) + let y: CGFloat = (u*u*start.y) + (2*u*tt*control.y) + (tt*tt*end.y) + return CGPoint( + x: x, + y: y + ) + } + + // Quadratic Bézier derivative: B'(t) = 2(1-t)(P1-P0) + 2t(P2-P1) + let bezierDerivative: (CGFloat) -> CGPoint = { t in + let tt = min(max(t, 0), 1) + let a = CGPoint(x: control.x - start.x, y: control.y - start.y) + let b = CGPoint(x: end.x - control.x, y: end.y - control.y) + let x = 2 * (1 - tt) * a.x + 2 * tt * b.x + let y = 2 * (1 - tt) * a.y + 2 * tt * b.y + return CGPoint(x: x, y: y) + } + let _ = bezierDerivative + + // Approximate curve length by sampling speeds (polyline integral) + let approximateBezierLength: (Int) -> CGFloat = { samples in + guard samples >= 2 else { return 0 } + var length: CGFloat = 0 + var prev = bezier(0) + for i in 1.. 0.0001 + ? CGPoint(x: tan.x / tanLen, y: tan.y / tanLen) + : CGPoint(x: dx / chord, y: dy / chord) + + // Spring progress p(time): 0 -> 1 with overshoot if dampingRatio < 1 + let zeta = min(max(dampingRatio, 0.01), 0.99) + let omega = max(angularFrequency, 0.01) + let omegaD = omega * sqrt(1 - zeta*zeta) + let zetaTerm = zeta / sqrt(1 - zeta*zeta) + + let springProgress: (CGFloat) -> CGFloat = { time in + let expTerm = exp(-zeta * omega * time) + return 1 - expTerm * (cos(omegaD * time) + zetaTerm * sin(omegaD * time)) + } + + // Ensure we include at least one overshoot peak (~pi/omegaD), plus settling. + let duration: CGFloat = max(0.45, (CGFloat.pi * 1.6) / omegaD) + + var frames: [CGPoint] = [] + frames.reserveCapacity(steps) + + for i in 0.. Void)? = nil) { + func animateSpring(from: Any, to: Any, keyPath: String, duration: Double, delay: Double = 0.0, initialVelocity: CGFloat = 0.0, stiffness: CGFloat = 900.0, damping: CGFloat = 88.0, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil, key: String? = nil) { let animation = makeSpringBounceAnimation(keyPath, initialVelocity, damping) animation.stiffness = stiffness animation.fromValue = from @@ -349,7 +349,7 @@ public extension CALayer { adjustFrameRate(animation: animation) - self.add(animation, forKey: additive ? nil : keyPath) + self.add(animation, forKey: additive ? key : (key ?? keyPath)) } func animateAdditive(from: NSValue, to: NSValue, keyPath: String, key: String, timingFunction: String, mediaTimingFunction: CAMediaTimingFunction? = nil, duration: Double, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { diff --git a/submodules/Display/Source/ContextContentSourceNode.swift b/submodules/Display/Source/ContextContentSourceNode.swift index 9fae6585b4..ec9d13b4d6 100644 --- a/submodules/Display/Source/ContextContentSourceNode.swift +++ b/submodules/Display/Source/ContextContentSourceNode.swift @@ -49,12 +49,30 @@ public enum ContextExtractableContainerState { case extracted(size: CGSize, cornerRadius: CGFloat, state: ExtractionState) } +public struct ContextExtractableContainerNormalState { + public let size: CGSize + public let cornerRadius: CGFloat + + public init(size: CGSize, cornerRadius: CGFloat) { + self.size = size + self.cornerRadius = cornerRadius + } +} + +public enum ContextExtractableContainerTransition { + case transition(ContainedViewLayoutTransition) + case spring(duration: Double, stiffness: CGFloat, damping: CGFloat) +} + public protocol ContextExtractableContainer: UIView { typealias State = ContextExtractableContainerState + typealias NormalState = ContextExtractableContainerNormalState + typealias Transition = ContextExtractableContainerTransition + var normalState: NormalState { get } var extractableContentView: UIView { get } - func updateState(state: State, transition: ContainedViewLayoutTransition) + func updateState(state: State, transition: Transition, completion: ((Bool) -> Void)?) } public final class ContextExtractedContentContainingView: UIView { diff --git a/submodules/GalleryUI/BUILD b/submodules/GalleryUI/BUILD index 46a1605653..b07b517b1b 100644 --- a/submodules/GalleryUI/BUILD +++ b/submodules/GalleryUI/BUILD @@ -73,6 +73,7 @@ swift_library( "//submodules/TelegramUI/Components/EdgeEffect", "//submodules/TelegramUI/Components/GlassBackgroundComponent", "//submodules/TelegramUI/Components/GlassControls", + "//submodules/TelegramUI/Components/VideoPlaybackControlsComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index c11af4ae3b..290c218763 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -155,8 +155,6 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll private let animationCache: AnimationCache private let animationRenderer: MultiAnimationRenderer - private let authorNameNode: ASTextNode - private let dateNode: ASTextNode private let backwardButton: PlaybackButtonNode private let forwardButton: PlaybackButtonNode private let playbackControlButton: HighlightableButtonNode @@ -166,8 +164,6 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll private let statusNode: RadialStatusNode private var currentMessageText: NSAttributedString? - private var currentAuthorNameText: String? - private var currentDateText: String? private var currentMessage: Message? private var currentWebPageAndMedia: (TelegramMediaWebpage, Media)? @@ -176,7 +172,14 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll private var videoFramePreviewNode: (ASImageNode, ImmediateTextNode)? private var validLayout: (CGSize, LayoutMetrics, CGFloat, CGFloat, CGFloat, CGFloat)? - private var buttonsState: (displayDeleteButton: Bool, displayFullscreenButton: Bool, displayActionButton: Bool, displayEditButton: Bool)? + private var buttonsState: ( + displayDeleteButton: Bool, + displayFullscreenButton: Bool, + displayActionButton: Bool, + displayEditButton: Bool, + displayPictureInPictureButton: Bool, + displaySettingsButton: Bool + )? private var codeHighlightState: (id: EngineMessage.Id, specs: [CachedMessageSyntaxHighlight.Spec], disposable: Disposable)? @@ -212,16 +215,12 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll if self.content != oldValue { switch self.content { case .info: - self.authorNameNode.isHidden = false - self.dateNode.isHidden = false self.hasSeekControls = false self.playbackControlButton.isHidden = true self.statusButtonNode.isHidden = true self.statusNode.isHidden = true case let .fetch(status, seekable): self.currentIsPaused = true - self.authorNameNode.isHidden = true - self.dateNode.isHidden = true self.hasSeekControls = seekable && !self.isAd if status == .Local && !self.isAd { @@ -262,8 +261,6 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll self.statusButtonNode.isUserInteractionEnabled = statusState != .none case let .playback(paused, seekable): self.currentIsPaused = paused - self.authorNameNode.isHidden = true - self.dateNode.isHidden = true if !self.isAd { self.playbackControlButton.isHidden = false @@ -381,16 +378,6 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll self.textNode.linkHighlightColor = UIColor(rgb: 0x5ac8fa, alpha: 0.2) self.textNode.displaySpoilerEffect = false - self.authorNameNode = ASTextNode() - self.authorNameNode.maximumNumberOfLines = 1 - self.authorNameNode.isUserInteractionEnabled = false - self.authorNameNode.displaysAsynchronously = false - - self.dateNode = ASTextNode() - self.dateNode.maximumNumberOfLines = 1 - self.dateNode.isUserInteractionEnabled = false - self.dateNode.displaysAsynchronously = false - self.backwardButton = PlaybackButtonNode() self.backwardButton.alpha = 0.0 self.backwardButton.isUserInteractionEnabled = false @@ -609,12 +596,9 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll self.scrollNode.addSubnode(self.textNode) self.scrollNode.addSubnode(textSelectionNode) - self.contentNode.addSubnode(self.authorNameNode) - self.contentNode.addSubnode(self.dateNode) - - self.contentNode.addSubnode(self.backwardButton) - self.contentNode.addSubnode(self.forwardButton) - self.contentNode.addSubnode(self.playbackControlButton) + //self.contentNode.addSubnode(self.backwardButton) + //self.contentNode.addSubnode(self.forwardButton) + //self.contentNode.addSubnode(self.playbackControlButton) self.playbackControlButton.addSubnode(self.playPauseIconNode) self.contentNode.addSubnode(self.statusNode) @@ -795,7 +779,6 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll func setup(origin: GalleryItemOriginData?, caption: NSAttributedString, isAd: Bool = false) { var titleText = origin?.title - var dateText = origin?.timestamp.flatMap { humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: $0).string } let caption = caption.mutableCopy() as! NSMutableAttributedString if isAd { @@ -803,7 +786,6 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll caption.insert(NSAttributedString(string: titleText + "\n", font: Font.semibold(17.0), textColor: .white), at: 0) } titleText = nil - dateText = nil } if origin == nil { @@ -811,14 +793,14 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll displayDeleteButton: false, displayFullscreenButton: false, displayActionButton: false, - displayEditButton: false + displayEditButton: false, + displayPictureInPictureButton: false, + displaySettingsButton: false ) } - if self.currentMessageText != caption || self.currentAuthorNameText != titleText || self.currentDateText != dateText { + if self.currentMessageText != caption { self.currentMessageText = caption - self.currentAuthorNameText = titleText - self.currentDateText = dateText if caption.length == 0 { self.textNode.isHidden = true @@ -829,25 +811,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } self.textSelectionNode?.isHidden = self.textNode.isHidden - if let titleText = titleText { - self.authorNameNode.attributedText = NSAttributedString(string: titleText, font: titleFont, textColor: .white) - } else { - self.authorNameNode.attributedText = nil - } - self.authorNameNode.accessibilityLabel = self.authorNameNode.attributedText?.string - - if let dateText = dateText { - self.dateNode.attributedText = NSAttributedString(string: dateText, font: dateFont, textColor: .white) - } else { - self.dateNode.attributedText = nil - } - self.dateNode.accessibilityLabel = self.dateNode.attributedText?.string - self.requestLayout?(.immediate) } } - func setMessage(_ message: Message, displayInfo: Bool = true, translateToLanguage: String? = nil, peerIsCopyProtected: Bool = false) { + func setMessage(_ message: Message, displayInfo: Bool = true, translateToLanguage: String? = nil, peerIsCopyProtected: Bool = false, displayPictureInPictureButton: Bool = false, displaySettingsButton: Bool = false) { self.currentMessage = message var displayInfo = displayInfo @@ -959,8 +927,6 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll authorNameText = EnginePeer(peer).displayTitle(strings: self.strings, displayOrder: self.nameOrder) } - var dateText = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: message.timestamp).string - var messageText = NSMutableAttributedString(string: "") var hasCaption = false var mediaDuration: Double? @@ -1036,8 +1002,6 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } if !displayInfo { - authorNameText = "" - dateText = "" canEdit = false } @@ -1059,21 +1023,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll self.textSelectionNode?.isHidden = self.textNode.isHidden - if let authorNameText = authorNameText { - self.authorNameNode.attributedText = NSAttributedString(string: authorNameText, font: titleFont, textColor: .white) - } else { - self.authorNameNode.attributedText = nil - } - self.authorNameNode.accessibilityLabel = self.authorNameNode.attributedText?.string - - self.dateNode.attributedText = NSAttributedString(string: dateText, font: dateFont, textColor: .white) - self.dateNode.accessibilityLabel = self.dateNode.attributedText?.string - if canFullscreen { displayFullscreenButton = true - } else { - displayDeleteButton = canDelete } + displayDeleteButton = canDelete displayActionButton = canShare displayEditButton = canEdit @@ -1106,7 +1059,9 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll displayDeleteButton: displayDeleteButton, displayFullscreenButton: displayFullscreenButton, displayActionButton: displayActionButton, - displayEditButton: displayEditButton + displayEditButton: displayEditButton, + displayPictureInPictureButton: displayPictureInPictureButton, + displaySettingsButton: displaySettingsButton ) self.requestLayout?(.immediate) @@ -1329,7 +1284,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll if self.textNode.isHidden || !displayCaption { panelHeight += 8.0 } else { - scrubberY = panelHeight - buttonPanelInsets.bottom - 44.0 - 44.0 - 8.0 + scrubberY = panelHeight - buttonPanelInsets.bottom - 44.0 - 44.0 - 10.0 if contentInset > 0.0 { scrubberY -= contentInset } @@ -1339,8 +1294,8 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll panelHeight -= 44.0 } - let scrubberFrame = CGRect(origin: CGPoint(x: leftInset + 4.0, y: scrubberY), size: CGSize(width: width - (leftInset + 4.0) * 2.0, height: 34.0)) - scrubberView.updateLayout(size: size, leftInset: leftInset + 4.0, rightInset: rightInset, transition: .immediate) + let scrubberFrame = CGRect(origin: CGPoint(x: buttonPanelInsets.left, y: scrubberY), size: CGSize(width: width - buttonPanelInsets.left - buttonPanelInsets.right, height: 44.0)) + scrubberView.updateLayout(size: scrubberFrame.size, leftInset: 0.0, rightInset: 0.0, transition: .immediate) transition.updateBounds(layer: scrubberView.layer, bounds: CGRect(origin: CGPoint(), size: scrubberFrame.size)) transition.updatePosition(layer: scrubberView.layer, position: CGPoint(x: scrubberFrame.midX, y: scrubberFrame.midY)) } @@ -1348,6 +1303,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll var leftControlItems: [GlassControlGroupComponent.Item] = [] var rightControlItems: [GlassControlGroupComponent.Item] = [] + var centerControlItems: [GlassControlGroupComponent.Item] = [] if let buttonsState = self.buttonsState { if buttonsState.displayActionButton { @@ -1362,8 +1318,35 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } )) } + if buttonsState.displayPictureInPictureButton { + centerControlItems.append(GlassControlGroupComponent.Item( + id: AnyHashable("pip"), + content: .icon("Media Gallery/PictureInPictureButton"), + action: { [weak self] in + guard let self else { + return + } + self.pipButtonPressed() + } + )) + } + if buttonsState.displaySettingsButton { + centerControlItems.append(GlassControlGroupComponent.Item( + id: AnyHashable("settings"), + content: .icon("Chat/Context Menu/Settings"), + action: { [weak self] in + guard let self, let buttonPanelView = self.buttonPanel.view as? GlassControlPanelComponent.View, let centerItemView = buttonPanelView.centerItemView else { + return + } + guard let itemView = centerItemView.itemView(id: AnyHashable("settings")) else { + return + } + self.settingsButtonPressed(sourceView: itemView) + } + )) + } if buttonsState.displayEditButton { - rightControlItems.append(GlassControlGroupComponent.Item( + centerControlItems.append(GlassControlGroupComponent.Item( id: AnyHashable("edit"), content: .icon("Media Gallery/Draw"), action: { [weak self] in @@ -1375,7 +1358,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll )) } if buttonsState.displayFullscreenButton && !metrics.isTablet { - rightControlItems.append(GlassControlGroupComponent.Item( + centerControlItems.append(GlassControlGroupComponent.Item( id: AnyHashable("fullscreen"), content: .icon(isLandscape ? "Chat/Context Menu/Collapse" : "Chat/Context Menu/Expand"), action: { [weak self] in @@ -1408,7 +1391,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll items: leftControlItems, background: .panel ), - centralItem: nil, + centralItem: GlassControlPanelComponent.Item( + items: centerControlItems, + background: .panel + ), rightItem: GlassControlPanelComponent.Item( items: rightControlItems, background: .panel @@ -1421,7 +1407,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll let buttonPanelFrame = CGRect(origin: CGPoint(x: buttonPanelInsets.left, y: panelHeight - buttonPanelInsets.bottom - buttonPanelSize.height), size: buttonPanelSize) if let buttonPanelView = self.buttonPanel.view { if buttonPanelView.superview == nil { - self.contentNode.view.insertSubview(buttonPanelView, belowSubview: self.authorNameNode.view) + self.contentNode.view.addSubview(buttonPanelView) } ComponentTransition(transition).setFrame(view: buttonPanelView, frame: buttonPanelFrame) } @@ -1441,18 +1427,6 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll self.statusButtonNode.frame = CGRect(origin: CGPoint(x: floor((width - 44.0) / 2.0), y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0)) - let buttonsSideInset: CGFloat = 44.0 * CGFloat(max(leftControlItems.count, rightControlItems.count)) - let authorNameSize = self.authorNameNode.measure(CGSize(width: width - buttonsSideInset * 2.0 - 8.0 * 2.0 - buttonPanelInsets.left - buttonPanelInsets.right, height: CGFloat.greatestFiniteMagnitude)) - let dateSize = self.dateNode.measure(CGSize(width: width - buttonsSideInset * 2.0 - 8.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)) - - if authorNameSize.height.isZero { - self.dateNode.frame = CGRect(origin: CGPoint(x: floor((width - dateSize.width) / 2.0), y: panelHeight - buttonPanelInsets.bottom - 44.0 + floor((44.0 - dateSize.height) / 2.0)), size: dateSize) - } else { - let labelsSpacing: CGFloat = 0.0 - self.authorNameNode.frame = CGRect(origin: CGPoint(x: floor((width - authorNameSize.width) / 2.0), y: panelHeight - buttonPanelInsets.bottom - 44.0 + floor((44.0 - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0)), size: authorNameSize) - self.dateNode.frame = CGRect(origin: CGPoint(x: floor((width - dateSize.width) / 2.0), y: panelHeight - buttonPanelInsets.bottom - 44.0 + floor((44.0 - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0) + authorNameSize.height + labelsSpacing), size: dateSize) - } - if let (videoFramePreviewNode, videoFrameTextNode) = self.videoFramePreviewNode { let intrinsicImageSize = videoFramePreviewNode.image?.size ?? CGSize(width: 320.0, height: 240.0) let fitSize: CGSize @@ -1501,8 +1475,6 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } transition.animatePositionAdditive(node: self.scrollWrapperNode, offset: CGPoint(x: 0.0, y: self.bounds.height - fromHeight)) self.scrollWrapperNode.alpha = 1.0 - self.dateNode.alpha = 1.0 - self.authorNameNode.alpha = 1.0 if let buttonPanelView = self.buttonPanel.view { buttonPanelView.alpha = 1.0 } @@ -1530,8 +1502,6 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } transition.updateFrame(node: self.scrollWrapperNode, frame: self.scrollWrapperNode.frame.offsetBy(dx: 0.0, dy: self.bounds.height - toHeight)) self.scrollWrapperNode.alpha = 0.0 - self.dateNode.alpha = 0.0 - self.authorNameNode.alpha = 0.0 if let buttonPanelView = self.buttonPanel.view { buttonPanelView.alpha = 0.0 @@ -2156,6 +2126,20 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll self.controllerInteraction?.editMedia(message.id) } + private func pipButtonPressed() { + guard let currentItemNode = self.controllerInteraction?.currentItemNode() as? UniversalVideoGalleryItemNode else { + return + } + currentItemNode.pictureInPictureButtonPressed() + } + + private func settingsButtonPressed(sourceView: UIView) { + guard let currentItemNode = self.controllerInteraction?.currentItemNode() as? UniversalVideoGalleryItemNode else { + return + } + currentItemNode.settingsButtonPressed(sourceView: sourceView) + } + @objc func playbackControlPressed() { self.playbackControl?() } diff --git a/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift b/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift index 2288152ed9..a60adaaac1 100644 --- a/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift +++ b/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift @@ -10,15 +10,21 @@ import TelegramPresentationData import RangeSet import ShimmerEffect import TelegramUniversalVideoContent +import ComponentFlow +import ComponentDisplayAdapters +import GlassBackgroundComponent -private let textFont = Font.with(size: 13.0, design: .regular, weight: .regular, traits: [.monospacedNumbers]) +private let textFont = Font.with(size: 13.0, design: .regular, weight: .medium, traits: [.monospacedNumbers]) private let scrubberBackgroundColor = UIColor(white: 1.0, alpha: 0.42) private let scrubberForegroundColor = UIColor.white private let scrubberBufferingColor = UIColor(rgb: 0xffffff, alpha: 0.5) final class ChatVideoGalleryItemScrubberView: UIView { - private var containerLayout: (CGSize, CGFloat, CGFloat)? + private var containerLayout: (size: CGSize, leftInset: CGFloat, rightInset: CGFloat)? + + private let backgroundContainer: GlassBackgroundContainerView + private let backgroundView: GlassBackgroundView private let leftTimestampNode: MediaPlayerTimeTextNode private let rightTimestampNode: MediaPlayerTimeTextNode @@ -44,22 +50,19 @@ final class ChatVideoGalleryItemScrubberView: UIView { private var isAnimatedOut: Bool = false + private var currentLeftString: String? + private var currentRightString: String? + var hideWhenDurationIsUnknown = false { didSet { if self.hideWhenDurationIsUnknown { if let playbackStatus = self.playbackStatus, !playbackStatus.duration.isZero { - self.scrubberNode.isHidden = false - self.leftTimestampNode.isHidden = false - self.rightTimestampNode.isHidden = false + self.backgroundContainer.isHidden = false } else { - self.scrubberNode.isHidden = true - self.leftTimestampNode.isHidden = true - self.rightTimestampNode.isHidden = true + self.backgroundContainer.isHidden = true } } else { - self.scrubberNode.isHidden = false - self.leftTimestampNode.isHidden = false - self.rightTimestampNode.isHidden = false + self.backgroundContainer.isHidden = false } } } @@ -70,8 +73,13 @@ final class ChatVideoGalleryItemScrubberView: UIView { var seek: (Double) -> Void = { _ in } init(chapters: [MediaPlayerScrubbingChapter]) { + self.backgroundContainer = GlassBackgroundContainerView() + self.backgroundView = GlassBackgroundView() + self.backgroundContainer.contentView.addSubview(self.backgroundView) + self.backgroundView.contentView.layer.allowsGroupOpacity = true + self.chapters = chapters - self.scrubberNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 5.0, lineCap: .round, scrubberHandle: .circle, backgroundColor: scrubberBackgroundColor, foregroundColor: scrubberForegroundColor, bufferingColor: scrubberBufferingColor, chapters: chapters)) + self.scrubberNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 8.0, lineCap: .round, scrubberHandle: .none, backgroundColor: scrubberBackgroundColor, foregroundColor: scrubberForegroundColor, bufferingColor: scrubberBufferingColor, chapters: chapters)) self.shimmerEffectNode = ShimmerEffectForegroundNode() self.leftTimestampNode = MediaPlayerTimeTextNode(textColor: .white) @@ -86,6 +94,8 @@ final class ChatVideoGalleryItemScrubberView: UIView { super.init(frame: CGRect()) + self.addSubview(self.backgroundContainer) + self.scrubberNode.seek = { [weak self] timestamp in self?.seek(timestamp) } @@ -117,10 +127,10 @@ final class ChatVideoGalleryItemScrubberView: UIView { } } - self.addSubnode(self.scrubberNode) - self.addSubnode(self.leftTimestampNode) - self.addSubnode(self.rightTimestampNode) - self.addSubnode(self.infoNode) + self.backgroundView.contentView.addSubview(self.scrubberNode.view) + self.backgroundView.contentView.addSubview(self.leftTimestampNode.view) + self.backgroundView.contentView.addSubview(self.rightTimestampNode.view) + //self.backgroundView.contentView.addSubview(self.infoNode.view) } required init?(coder aDecoder: NSCoder) { @@ -171,7 +181,8 @@ final class ChatVideoGalleryItemScrubberView: UIView { } self.scrubberNode.setCollapsed(collapsed == true, animated: animated) let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .linear) : .immediate - transition.updateAlpha(node: self.scrubberNode, alpha: alpha) + + ComponentTransition(transition).setAlpha(view: self.backgroundContainer, alpha: alpha) } func animateTo(_ timestamp: Double) { @@ -213,6 +224,18 @@ final class ChatVideoGalleryItemScrubberView: UIView { }) } } + + let leftString = MediaPlayerTimeTextNode.timestampString(for: status, mode: .normal) + let rightString = MediaPlayerTimeTextNode.timestampString(for: status, mode: .reversed) + + if strongSelf.currentLeftString?.count != leftString?.count || strongSelf.currentRightString?.count != rightString?.count { + strongSelf.currentLeftString = leftString + strongSelf.currentRightString = rightString + + if let (size, leftInset, rightInset) = strongSelf.containerLayout { + strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate) + } + } } })) @@ -274,16 +297,16 @@ final class ChatVideoGalleryItemScrubberView: UIView { guard let strongSelf = self else { return } - let leftTimestampNodePushed: Bool - let rightTimestampNodePushed: Bool + let leftTimestampNodePushed: Bool = false + let rightTimestampNodePushed: Bool = false let infoNodePushed: Bool if let value = value { - leftTimestampNodePushed = value < 0.16 - rightTimestampNodePushed = value > 0.84 + //leftTimestampNodePushed = value < 0.16 + //rightTimestampNodePushed = value > 0.84 infoNodePushed = value >= 0.16 && value <= 0.84 } else { - leftTimestampNodePushed = false - rightTimestampNodePushed = false + //leftTimestampNodePushed = false + //rightTimestampNodePushed = false infoNodePushed = false } if leftTimestampNodePushed != strongSelf.leftTimestampNodePushed || rightTimestampNodePushed != strongSelf.rightTimestampNodePushed || infoNodePushed != strongSelf.infoNodePushed { @@ -336,40 +359,56 @@ final class ChatVideoGalleryItemScrubberView: UIView { func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { self.containerLayout = (size, leftInset, rightInset) + let transition = ComponentTransition(transition) + let scrubberHeight: CGFloat = 14.0 - var scrubberInset: CGFloat let leftTimestampOffset: CGFloat let rightTimestampOffset: CGFloat let infoOffset: CGFloat - if size.width > size.height { - scrubberInset = 58.0 - leftTimestampOffset = 4.0 - rightTimestampOffset = 4.0 - infoOffset = 0.0 - } else { - scrubberInset = 13.0 - leftTimestampOffset = 22.0 + (self.leftTimestampNodePushed ? 8.0 : 0.0) - rightTimestampOffset = 22.0 + (self.rightTimestampNodePushed ? 8.0 : 0.0) - infoOffset = 22.0 + (self.infoNodePushed ? 8.0 : 0.0) + + var scrubberLeftInset: CGFloat = 58.0 + var scrubberRightInset: CGFloat = 58.0 + + if let leftString = self.currentLeftString, let rightString = self.currentRightString { + if leftString.count > 4 { + let string = NSAttributedString(string: leftString, font: Font.regular(13.0), textColor: .black) + let size = string.boundingRect(with: CGSize(width: 200.0, height: 100.0), options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil).size + scrubberLeftInset = min(100.0, 16.0 + size.width + 8.0) + } + if rightString.count > 4 { + let string = NSAttributedString(string: rightString, font: Font.regular(13.0), textColor: .black) + let size = string.boundingRect(with: CGSize(width: 200.0, height: 100.0), options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil).size + scrubberRightInset = min(100.0, 16.0 + size.width + 8.0) + } } - transition.updateFrame(node: self.leftTimestampNode, frame: CGRect(origin: CGPoint(x: 12.0, y: leftTimestampOffset), size: CGSize(width: 60.0, height: 20.0))) - transition.updateFrame(node: self.rightTimestampNode, frame: CGRect(origin: CGPoint(x: size.width - leftInset - rightInset - 60.0 - 12.0, y: rightTimestampOffset), size: CGSize(width: 60.0, height: 20.0))) + leftTimestampOffset = 14.0 + rightTimestampOffset = 14.0 + infoOffset = 0.0 + + transition.setFrame(view: self.leftTimestampNode.view, frame: CGRect(origin: CGPoint(x: 16.0, y: leftTimestampOffset), size: CGSize(width: 60.0, height: 20.0))) + transition.setFrame(view: self.rightTimestampNode.view, frame: CGRect(origin: CGPoint(x: size.width - leftInset - rightInset - 60.0 - 16.0, y: rightTimestampOffset), size: CGSize(width: 60.0, height: 20.0))) var infoConstrainedSize = size - infoConstrainedSize.width = size.width - scrubberInset * 2.0 - 100.0 + infoConstrainedSize.width = size.width - scrubberLeftInset - scrubberRightInset - 100.0 let infoSize = self.infoNode.measure(infoConstrainedSize) self.infoNode.bounds = CGRect(origin: CGPoint(), size: infoSize) - transition.updatePosition(node: self.infoNode, position: CGPoint(x: size.width / 2.0, y: infoOffset + infoSize.height / 2.0)) + transition.setPosition(view: self.infoNode.view, position: CGPoint(x: size.width / 2.0, y: infoOffset + infoSize.height / 2.0)) self.infoNode.alpha = size.width < size.height && self.isCollapsed == false ? 1.0 : 0.0 - let scrubberFrame = CGRect(origin: CGPoint(x: scrubberInset, y: 6.0), size: CGSize(width: size.width - leftInset - rightInset - scrubberInset * 2.0, height: scrubberHeight)) + let scrubberFrame = CGRect(origin: CGPoint(x: scrubberLeftInset, y: 15.0), size: CGSize(width: size.width - leftInset - rightInset - scrubberLeftInset - scrubberRightInset, height: scrubberHeight)) self.scrubberNode.frame = scrubberFrame self.shimmerEffectNode.updateAbsoluteRect(CGRect(origin: .zero, size: scrubberFrame.size), within: scrubberFrame.size) self.shimmerEffectNode.update(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.75), horizontal: true, effectSize: nil, globalTimeOffset: false, duration: nil) self.shimmerEffectNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 4.0), size: CGSize(width: scrubberFrame.size.width, height: 5.0)) self.shimmerEffectNode.cornerRadius = 2.5 + + transition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: CGPoint(), size: size)) + self.backgroundContainer.update(size: size, isDark: true, transition: transition) + + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) + self.backgroundView.update(size: size, cornerRadius: min(44.0 * 0.5, size.height * 0.5), isDark: true, tintColor: .init(kind: .panel, color: UIColor(white: 0.0, alpha: 0.6)), transition: transition) } override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { @@ -386,7 +425,7 @@ final class ChatVideoGalleryItemScrubberView: UIView { let fromRect = scrubberTransition.view.convert(scrubberTransition.view.bounds, to: self) let targetCloneView = scrubberTransition.makeView() - self.addSubview(targetCloneView) + self.backgroundView.contentView.addSubview(targetCloneView) targetCloneView.frame = fromRect scrubberTransition.updateView(targetCloneView, GalleryItemScrubberTransition.Scrubber.TransitionState(sourceSize: fromRect.size, destinationSize: CGSize(width: self.scrubberNode.bounds.width, height: fromRect.height), progress: 0.0, direction: .in), .immediate) targetCloneView.alpha = 1.0 @@ -411,10 +450,8 @@ final class ChatVideoGalleryItemScrubberView: UIView { transition.animatePosition(node: self.rightTimestampNode, from: CGPoint(x: rightTimestampOffset.x + scrubberSourceRect.maxX, y: rightTimestampOffset.y + scrubberSourceRect.maxY)) } - self.scrubberNode.layer.animateAlpha(from: 0.0, to: self.leftTimestampNode.alpha, duration: 0.25) - self.leftTimestampNode.layer.animateAlpha(from: 0.0, to: self.leftTimestampNode.alpha, duration: 0.25) - self.rightTimestampNode.layer.animateAlpha(from: 0.0, to: self.leftTimestampNode.alpha, duration: 0.25) - self.infoNode.layer.animateAlpha(from: 0.0, to: self.leftTimestampNode.alpha, duration: 0.25) + self.backgroundContainer.alpha = 0.0 + ComponentTransition.easeInOut(duration: 0.2).setAlpha(view: self.backgroundContainer, alpha: 1.0) } func animateOut(to scrubberTransition: GalleryItemScrubberTransition?, transition: ContainedViewLayoutTransition) { @@ -425,7 +462,7 @@ final class ChatVideoGalleryItemScrubberView: UIView { let scrubberDestinationRect = CGRect(origin: CGPoint(x: toRect.minX, y: toRect.maxY - 3.0), size: CGSize(width: toRect.width, height: 3.0)) let targetCloneView = scrubberTransition.makeView() - self.addSubview(targetCloneView) + self.backgroundView.contentView.addSubview(targetCloneView) targetCloneView.frame = CGRect(origin: CGPoint(x: self.scrubberNode.frame.minX, y: self.scrubberNode.frame.maxY - toRect.height), size: CGSize(width: self.scrubberNode.bounds.width, height: toRect.height)) scrubberTransition.updateView(targetCloneView, GalleryItemScrubberTransition.Scrubber.TransitionState(sourceSize: CGSize(width: self.scrubberNode.bounds.width, height: toRect.height), destinationSize: toRect.size, progress: 0.0, direction: .out), .immediate) targetCloneView.alpha = 0.0 @@ -450,9 +487,6 @@ final class ChatVideoGalleryItemScrubberView: UIView { transition.animatePositionAdditive(layer: self.rightTimestampNode.layer, offset: CGPoint(), to: CGPoint(x: -self.rightTimestampNode.position.x + (rightTimestampOffset.x + scrubberDestinationRect.maxX), y: -self.rightTimestampNode.position.y + (rightTimestampOffset.y + scrubberDestinationRect.maxY)), removeOnCompletion: false) } - transition.updateAlpha(layer: self.scrubberNode.layer, alpha: 0.0) - transition.updateAlpha(layer: self.leftTimestampNode.layer, alpha: 0.0) - transition.updateAlpha(layer: self.rightTimestampNode.layer, alpha: 0.0) - transition.updateAlpha(layer: self.infoNode.layer, alpha: 0.0) + ComponentTransition.easeInOut(duration: 0.2).setAlpha(view: self.backgroundContainer, alpha: 0.0) } } diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index df721eacae..289b1480ce 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -576,7 +576,7 @@ private func galleryEntriesForMessageHistoryEntries(_ entries: [MessageHistoryEn } public class GalleryController: ViewController, StandalonePresentableController, KeyShortcutResponder, GalleryControllerProtocol { - public static let darkNavigationTheme = NavigationBarTheme(overallDarkAppearance: true, buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: UIColor(white: 0.0, alpha: 0.6), enableBackgroundBlur: false, separatorColor: UIColor(white: 0.0, alpha: 0.8), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear, edgeEffectColor: .black, style: .glass) + public static let darkNavigationTheme = NavigationBarTheme(overallDarkAppearance: true, buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: UIColor(white: 0.0, alpha: 0.6), enableBackgroundBlur: false, separatorColor: UIColor(white: 0.0, alpha: 0.8), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear, edgeEffectColor: .clear, style: .glass) private var galleryNode: GalleryControllerNode { return self.displayNode as! GalleryControllerNode @@ -1370,6 +1370,8 @@ public class GalleryController: ViewController, StandalonePresentableController, } }, controller: { [weak self] in return self + }, currentItemNode: { [weak self] in + return self?.galleryNode.pager.centralItemNode() }) let disableTapNavigation = !(self.context.sharedContext.currentMediaDisplaySettings.with { $0 }.showNextMediaOnTap) diff --git a/submodules/GalleryUI/Sources/GalleryControllerNode.swift b/submodules/GalleryUI/Sources/GalleryControllerNode.swift index 1c8cf7ec67..10ec3ffd9e 100644 --- a/submodules/GalleryUI/Sources/GalleryControllerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryControllerNode.swift @@ -6,6 +6,9 @@ import Postbox import SwipeToDismissGesture import AccountContext import UndoUI +import EdgeEffect +import ComponentFlow +import ComponentDisplayAdapters open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDelegate { public enum CustomDismissType { @@ -22,6 +25,9 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture } } + + private let headerEdgeEffectView: EdgeEffectView + public let footerNode: GalleryFooterNode public var currentThumbnailContainerNode: GalleryThumbnailContainerNode? public var overlayNode: ASDisplayNode? @@ -67,6 +73,8 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { self.scrollView.contentInsetAdjustmentBehavior = .never } + + self.headerEdgeEffectView = EdgeEffectView() self.pager = GalleryPagerNode(pageGap: pageGap, disableTapNavigation: disableTapNavigation) self.footerNode = GalleryFooterNode(controllerInteraction: controllerInteraction) @@ -287,19 +295,42 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture transition.updateFrame(node: self.footerNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + var edgeEffectFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: navigationBarHeight)) + let edgeEffectHeight: CGFloat = 120.0 + let edgeEffectOffset: CGFloat = 70.0 + edgeEffectFrame.size.height += edgeEffectOffset + if self.areControlsHidden { + edgeEffectFrame.origin.y -= navigationBarHeight + } + transition.updateFrame(view: self.headerEdgeEffectView, frame: edgeEffectFrame) + self.headerEdgeEffectView.update(content: .black, alpha: 0.35, rect: edgeEffectFrame, edge: .top, edgeSize: min(edgeEffectHeight, edgeEffectFrame.height), transition: ComponentTransition(transition)) + transition.updateAlpha(layer: self.headerEdgeEffectView.layer, alpha: self.areControlsHidden ? 0.0 : 1.0) + if let navigationBar = self.navigationBar { transition.updateFrame(node: navigationBar, frame: CGRect(origin: CGPoint(x: 0.0, y: self.areControlsHidden ? -navigationBarHeight : 0.0), size: CGSize(width: layout.size.width, height: navigationBarHeight))) if self.footerNode.supernode == nil { self.addSubnode(self.footerNode) } + if self.headerEdgeEffectView.superview == nil { + self.view.insertSubview(self.headerEdgeEffectView, belowSubview: navigationBar.view) + } } var thumbnailPanelHeight: CGFloat = 0.0 if let currentThumbnailContainerNode = self.currentThumbnailContainerNode { - let panelHeight: CGFloat = 52.0 - thumbnailPanelHeight = panelHeight + let panelHeight: CGFloat = 30.0 + thumbnailPanelHeight = panelHeight + 22.0 - let thumbnailsFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - 40.0 - panelHeight + 4.0 - layout.intrinsicInsets.bottom + (self.areControlsHidden ? 106.0 : 0.0)), size: CGSize(width: layout.size.width, height: panelHeight - 4.0)) + var buttonPanelInsets = UIEdgeInsets() + buttonPanelInsets.left = 8.0 + buttonPanelInsets.right = 8.0 + buttonPanelInsets.bottom = layout.intrinsicInsets.bottom + 8.0 + if buttonPanelInsets.bottom <= 32.0 { + buttonPanelInsets.left += 18.0 + buttonPanelInsets.right += 18.0 + } + + let thumbnailsFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - 60.0 - panelHeight - buttonPanelInsets.bottom + (self.areControlsHidden ? 106.0 : 0.0)), size: CGSize(width: layout.size.width, height: panelHeight)) transition.updateFrame(node: currentThumbnailContainerNode, frame: thumbnailsFrame) currentThumbnailContainerNode.updateLayout(size: thumbnailsFrame.size, transition: transition) @@ -371,6 +402,7 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture self.statusBar?.alpha = 0.0 self.navigationBar?.alpha = 0.0 self.footerNode.alpha = 0.0 + self.headerEdgeEffectView.alpha = 0.0 self.currentThumbnailContainerNode?.alpha = 0.0 self.backgroundNode.layer.animate(from: backgroundColor.withAlphaComponent(0.0).cgColor, to: backgroundColor.cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.15) @@ -386,6 +418,8 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture if !self.areControlsHidden { self.footerNode.alpha = 1.0 self.footerNode.animateIn(transition: .animated(duration: 0.15, curve: .linear)) + + ComponentTransition.easeInOut(duration: 0.15).setAlpha(view: self.headerEdgeEffectView, alpha: 1.0) } if animateContent { @@ -426,6 +460,7 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture interfaceAnimationCompleted = true intermediateCompletion() }) + ComponentTransition.easeInOut(duration: 0.1).setAlpha(view: self.headerEdgeEffectView, alpha: 0.0) self.footerNode.animateOut(transition: .animated(duration: 0.1, curve: .easeInOut)) diff --git a/submodules/GalleryUI/Sources/GalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/GalleryFooterContentNode.swift index e284f45268..b4136e278c 100644 --- a/submodules/GalleryUI/Sources/GalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/GalleryFooterContentNode.swift @@ -14,14 +14,16 @@ public final class GalleryControllerInteraction { public let replaceRootController: (ViewController, Promise?) -> Void public let editMedia: (MessageId) -> Void public let controller: () -> ViewController? + public let currentItemNode: () -> GalleryItemNode? - public init(presentController: @escaping (ViewController, ViewControllerPresentationArguments?) -> Void, pushController: @escaping (ViewController) -> Void, dismissController: @escaping () -> Void, replaceRootController: @escaping (ViewController, Promise?) -> Void, editMedia: @escaping (MessageId) -> Void, controller: @escaping () -> ViewController?) { + public init(presentController: @escaping (ViewController, ViewControllerPresentationArguments?) -> Void, pushController: @escaping (ViewController) -> Void, dismissController: @escaping () -> Void, replaceRootController: @escaping (ViewController, Promise?) -> Void, editMedia: @escaping (MessageId) -> Void, controller: @escaping () -> ViewController?, currentItemNode: @escaping () -> GalleryItemNode?) { self.presentController = presentController self.pushController = pushController self.dismissController = dismissController self.replaceRootController = replaceRootController self.editMedia = editMedia self.controller = controller + self.currentItemNode = currentItemNode } } diff --git a/submodules/GalleryUI/Sources/GalleryFooterNode.swift b/submodules/GalleryUI/Sources/GalleryFooterNode.swift index 54df013512..beff9b0fac 100644 --- a/submodules/GalleryUI/Sources/GalleryFooterNode.swift +++ b/submodules/GalleryUI/Sources/GalleryFooterNode.swift @@ -133,11 +133,12 @@ public final class GalleryFooterNode: ASDisplayNode { } var edgeEffectFrame = backgroundFrame - let edgeEffectHeight: CGFloat = 46.0 - edgeEffectFrame.origin.y -= edgeEffectHeight - edgeEffectFrame.size.height += edgeEffectHeight + let edgeEffectHeight: CGFloat = 120.0 + let edgeEffectOffset: CGFloat = 70.0 + edgeEffectFrame.origin.y -= edgeEffectOffset + edgeEffectFrame.size.height += edgeEffectOffset edgeEffectTransition.setFrame(view: self.edgeEffectView, frame: edgeEffectFrame) - self.edgeEffectView.update(content: .black, rect: edgeEffectFrame, edge: .bottom, edgeSize: min(90.0, edgeEffectFrame.height), transition: edgeEffectTransition) + self.edgeEffectView.update(content: .black, alpha: 0.35, rect: edgeEffectFrame, edge: .bottom, edgeSize: min(edgeEffectHeight, edgeEffectFrame.height), transition: edgeEffectTransition) let contentTransition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) if let overlayContentNode = self.currentOverlayContentNode { diff --git a/submodules/GalleryUI/Sources/GalleryThumbnailContainerNode.swift b/submodules/GalleryUI/Sources/GalleryThumbnailContainerNode.swift index fa8806d70b..1f7e9247e8 100644 --- a/submodules/GalleryUI/Sources/GalleryThumbnailContainerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryThumbnailContainerNode.swift @@ -4,22 +4,24 @@ import AsyncDisplayKit import Display import SwiftSignalKit -private let itemBaseSize = CGSize(width: 23.0, height: 42.0) -private let spacing: CGFloat = 2.0 -private let maxWidth: CGFloat = 75.0 - public protocol GalleryThumbnailItem { func isEqual(to: GalleryThumbnailItem) -> Bool func image(synchronous: Bool) -> (Signal<(TransformImageArguments) -> DrawingContext?, NoError>, CGSize) } private final class GalleryThumbnailItemNode: ASDisplayNode { + private let itemBaseSize: CGSize + private let maxWidth: CGFloat + private let imageNode: TransformImageNode private let imageContainerNode: ASDisplayNode private let imageSize: CGSize - init(item: GalleryThumbnailItem, synchronous: Bool) { + init(item: GalleryThumbnailItem, itemBaseSize: CGSize, maxWidth: CGFloat, synchronous: Bool) { + self.itemBaseSize = itemBaseSize + self.maxWidth = maxWidth + self.imageNode = TransformImageNode() self.imageContainerNode = ASDisplayNode() self.imageContainerNode.clipsToBounds = true @@ -36,7 +38,7 @@ private final class GalleryThumbnailItemNode: ASDisplayNode { func updateLayout(height: CGFloat, progress: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { let boundingSize = self.imageSize.aspectFilled(CGSize(width: 1.0, height: height)) - let width = itemBaseSize.width * (1.0 - progress) + min(maxWidth, boundingSize.width) * progress + let width = self.itemBaseSize.width * (1.0 - progress) + min(self.maxWidth, boundingSize.width) * progress let arguments = TransformImageArguments(corners: ImageCorners(radius: 0), imageSize: boundingSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()) let makeLayout = self.imageNode.asyncLayout() let apply = makeLayout(arguments) @@ -52,6 +54,10 @@ public final class GalleryThumbnailContainerNode: ASDisplayNode, ASScrollViewDel public let groupId: Int64 private let scrollNode: ASScrollNode + private let itemBaseSize: CGSize + private let spacing: CGFloat + private let maxWidth: CGFloat + public private(set) var items: [GalleryThumbnailItem] = [] public private(set) var indexes: [Int] = [] private var itemNodes: [GalleryThumbnailItemNode] = [] @@ -67,6 +73,10 @@ public final class GalleryThumbnailContainerNode: ASDisplayNode, ASScrollViewDel self.groupId = groupId self.scrollNode = ASScrollNode() + self.itemBaseSize = CGSize(width: 15.0, height: 30.0) + self.spacing = 2.0 + self.maxWidth = 75.0 + super.init() self.scrollNode.view.delegate = self.wrappedScrollViewDelegate @@ -108,7 +118,7 @@ public final class GalleryThumbnailContainerNode: ASDisplayNode, ASScrollViewDel if let index = self.items.firstIndex(where: { $0.isEqual(to: item) }) { itemNodes.append(self.itemNodes[index]) } else { - itemNodes.append(GalleryThumbnailItemNode(item: item, synchronous: self.updateSynchronously)) + itemNodes.append(GalleryThumbnailItemNode(item: item, itemBaseSize: self.itemBaseSize, maxWidth: self.maxWidth, synchronous: self.updateSynchronously)) } } diff --git a/submodules/GalleryUI/Sources/GalleryTitleView.swift b/submodules/GalleryUI/Sources/GalleryTitleView.swift index bfae28100f..0cdbd18b19 100644 --- a/submodules/GalleryUI/Sources/GalleryTitleView.swift +++ b/submodules/GalleryUI/Sources/GalleryTitleView.swift @@ -6,14 +6,24 @@ import Postbox import TelegramCore import TelegramPresentationData import TelegramStringFormatting +import GlassBackgroundComponent +import ComponentFlow +import ComponentDisplayAdapters +import MultilineTextComponent -private let titleFont = Font.medium(15.0) -private let dateFont = Font.regular(14.0) +private let titleFont = Font.semibold(17.0) +private let dateFont = Font.regular(12.0) final class GalleryTitleView: UIView, NavigationBarTitleView { private let authorNameNode: ASTextNode private let dateNode: ASTextNode + private let titleBackgroundContainer: GlassBackgroundContainerView + private let titleBackground: GlassBackgroundView + private let title = ComponentView() + + private var titleString: String? + var requestUpdate: ((ContainedViewLayoutTransition) -> Void)? override init(frame: CGRect) { @@ -25,22 +35,31 @@ final class GalleryTitleView: UIView, NavigationBarTitleView { self.dateNode.displaysAsynchronously = false self.dateNode.maximumNumberOfLines = 1 + self.titleBackgroundContainer = GlassBackgroundContainerView() + self.titleBackground = GlassBackgroundView() + + self.titleBackgroundContainer.contentView.addSubview(self.titleBackground) + super.init(frame: frame) self.addSubnode(self.authorNameNode) self.addSubnode(self.dateNode) + + self.addSubview(self.titleBackgroundContainer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - func setMessage(_ message: Message, presentationData: PresentationData, accountPeerId: PeerId) { - let authorNameText = stringForFullAuthorName(message: EngineMessage(message), strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: accountPeerId).joined(separator: " → ") + func setMessage(_ message: Message, presentationData: PresentationData, accountPeerId: PeerId, title: String?) { + let authorNameText = stringForFullAuthorName(message: EngineMessage(message), strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: accountPeerId)[0]//.joined(separator: " → ") let dateText = humanReadableStringForTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, timestamp: message.timestamp).string self.authorNameNode.attributedText = NSAttributedString(string: authorNameText, font: titleFont, textColor: .white) - self.dateNode.attributedText = NSAttributedString(string: dateText, font: dateFont, textColor: .white) + self.dateNode.attributedText = NSAttributedString(string: dateText, font: dateFont, textColor: UIColor(white: 1.0, alpha: 0.5)) + + self.titleString = title } func updateLayout(availableSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { @@ -55,11 +74,41 @@ final class GalleryTitleView: UIView, NavigationBarTitleView { if authorNameSize.height.isZero { self.dateNode.frame = CGRect(origin: CGPoint(x: floor((size.width - dateSize.width) / 2.0), y: floor((size.height - dateSize.height) / 2.0)), size: dateSize) } else { - let labelsSpacing: CGFloat = 0.0 + let labelsSpacing: CGFloat = 2.0 self.authorNameNode.frame = CGRect(origin: CGPoint(x: floor((size.width - authorNameSize.width) / 2.0), y: floor((size.height - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0)), size: authorNameSize) self.dateNode.frame = CGRect(origin: CGPoint(x: floor((size.width - dateSize.width) / 2.0), y: floor((size.height - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0) + authorNameSize.height + labelsSpacing), size: dateSize) } + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: self.titleString ?? "", font: Font.semibold(12.0), textColor: .white)) + )), + environment: {}, + containerSize: CGSize(width: 200.0, height: 100.0) + ) + let titleInset: CGFloat = 12.0 + let titleBackgroundSize = CGSize(width: titleInset * 2.0 + titleSize.width, height: 24.0) + let titleBackgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleBackgroundSize.width) * 0.5), y: availableSize.height + 2.0), size: titleBackgroundSize) + let titleFrame = titleSize.centered(in: CGRect(origin: CGPoint(), size: titleBackgroundSize)) + if let titleView = self.title.view { + if titleView.superview == nil { + self.titleBackground.contentView.addSubview(titleView) + } + titleView.frame = titleFrame + } + + do { + let transition = ComponentTransition.immediate + transition.setFrame(view: self.titleBackgroundContainer, frame: titleBackgroundFrame) + self.titleBackgroundContainer.update(size: titleBackgroundSize, isDark: true, transition: transition) + + transition.setFrame(view: self.titleBackground, frame: CGRect(origin: CGPoint(), size: titleBackgroundSize)) + self.titleBackground.update(size: titleBackgroundSize, cornerRadius: titleBackgroundSize.height * 0.5, isDark: true, tintColor: .init(kind: .panel, color: UIColor(white: 0.0, alpha: 0.6)), transition: transition) + + self.titleBackgroundContainer.isHidden = !(self.titleString != nil && self.titleString != "") + } + return availableSize } diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index 8b21295ba8..5cd5d7661d 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -175,25 +175,24 @@ class ChatImageGalleryItem: GalleryItem { } } + var title: String? if let _ = message.adAttribute { - node._title.set(.single(self.presentationData.strings.Gallery_Ad)) + title = self.presentationData.strings.Gallery_Ad } else if let location = self.location { - node._title.set(.single(self.presentationData.strings.Items_NOfM("\(location.index + 1)", "\(location.count)").string)) + title = self.presentationData.strings.Items_NOfM("\(location.index + 1)", "\(location.count)").string } - if self.displayInfoOnTop { - node.titleContentView?.setMessage(self.message, presentationData: self.presentationData, accountPeerId: self.context.account.peerId) - } + node.titleContentView?.setMessage(self.message, presentationData: self.presentationData, accountPeerId: self.context.account.peerId, title: title) return node } func updateNode(node: GalleryItemNode, synchronous: Bool) { if let node = node as? ChatImageGalleryItemNode, let location = self.location { - node._title.set(.single(self.presentationData.strings.Items_NOfM("\(location.index + 1)", "\(location.count)").string)) + let title = self.presentationData.strings.Items_NOfM("\(location.index + 1)", "\(location.count)").string if self.displayInfoOnTop { - node.titleContentView?.setMessage(self.message, presentationData: self.presentationData, accountPeerId: self.context.account.peerId) + node.titleContentView?.setMessage(self.message, presentationData: self.presentationData, accountPeerId: self.context.account.peerId, title: title) } node.setMessage(self.message, displayInfo: !self.displayInfoOnTop, translateToLanguage: self.translateToLanguage, peerIsCopyProtected: self.peerIsCopyProtected, isSecret: self.isSecret) } diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 1eff5296a8..4cf2ba9756 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -36,6 +36,7 @@ import ComponentDisplayAdapters import ToastComponent import MultilineTextComponent import BundleIconComponent +import VideoPlaybackControlsComponent public enum UniversalVideoGalleryItemContentInfo { case message(Message, Int?) @@ -100,16 +101,17 @@ public class UniversalVideoGalleryItem: GalleryItem { public func node(synchronous: Bool) -> GalleryItemNode { let node = UniversalVideoGalleryItemNode(context: self.context, presentationData: self.presentationData, performAction: self.performAction, openActionOptions: self.openActionOptions, present: self.present) + var title: String? if let indexData = self.indexData { - node._title.set(.single(self.presentationData.strings.Items_NOfM("\(indexData.position + 1)", "\(indexData.totalCount)").string)) + title = self.presentationData.strings.Items_NOfM("\(indexData.position + 1)", "\(indexData.totalCount)").string } else if case let .message(message, _) = self.contentInfo, let _ = message.adAttribute { - node._title.set(.single(self.presentationData.strings.Gallery_Ad)) + title = self.presentationData.strings.Gallery_Ad } node.setupItem(self) - if self.displayInfoOnTop, case let .message(message, _) = self.contentInfo { - node.titleContentView?.setMessage(message, presentationData: self.presentationData, accountPeerId: self.context.account.peerId) + if case let .message(message, _) = self.contentInfo { + node.titleContentView?.setMessage(message, presentationData: self.presentationData, accountPeerId: self.context.account.peerId, title: title) } return node @@ -117,14 +119,15 @@ public class UniversalVideoGalleryItem: GalleryItem { public func updateNode(node: GalleryItemNode, synchronous: Bool) { if let node = node as? UniversalVideoGalleryItemNode { + var title: String? if let indexData = self.indexData { - node._title.set(.single(self.presentationData.strings.Items_NOfM("\(indexData.position + 1)", "\(indexData.totalCount)").string)) + title = self.presentationData.strings.Items_NOfM("\(indexData.position + 1)", "\(indexData.totalCount)").string } node.setupItem(self) if self.displayInfoOnTop, case let .message(message, _) = self.contentInfo { - node.titleContentView?.setMessage(message, presentationData: self.presentationData, accountPeerId: self.context.account.peerId) + node.titleContentView?.setMessage(message, presentationData: self.presentationData, accountPeerId: self.context.account.peerId, title: title) } } } @@ -237,7 +240,7 @@ private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentN var performAction: ((GalleryControllerInteractionTapAction) -> Void)? var presentPremiumDemo: (() -> Void)? - var openMoreMenu: ((ContextReferenceContentNode, Message) -> Void)? + var openMoreMenu: ((UIView, Message) -> Void)? private var validLayout: (size: CGSize, metrics: LayoutMetrics, insets: UIEdgeInsets)? @@ -373,7 +376,7 @@ private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentN }, moreAction: { [weak self] sourceNode in if let self { - self.openMoreMenu?(sourceNode, adMessage) + self.openMoreMenu?(sourceNode.view, adMessage) } } ) @@ -1121,6 +1124,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private let statusNode: RadialStatusNode private var statusNodeShouldBeHidden = true + private let playbackControls = ComponentView() + private var isCentral: Bool? private var _isVisible: Bool? private var initiallyActivated = false @@ -1134,6 +1139,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var dismissOnOrientationChange = false private var keepSoundOnDismiss = false private var hasPictureInPicture = false + private var hasSettingsButton = false private var pictureInPictureButton: UIBarButtonItem? @@ -1163,6 +1169,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var isPlaying = false private let isPlayingPromise = ValuePromise(false, ignoreRepeated: true) private let isInteractingPromise = ValuePromise(false, ignoreRepeated: true) + private var areControlsVisible: Bool = true private let controlsVisiblePromise = ValuePromise(true, ignoreRepeated: true) private let isShowingContextMenuPromise = ValuePromise(false, ignoreRepeated: true) private let isShowingSettingsMenuPromise = ValuePromise(false, ignoreRepeated: true) @@ -1240,7 +1247,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } self.moreBarButton.addTarget(self, action: #selector(self.moreButtonPressed), forControlEvents: .touchUpInside) - self.settingsBarButton.addTarget(self, action: #selector(self.settingsButtonPressed), forControlEvents: .touchUpInside) + //self.settingsBarButton.addTarget(self, action: #selector(self.settingsButtonPressed), forControlEvents: .touchUpInside) self.footerContentNode.interacting = { [weak self] value in self?.isInteractingPromise.set(value) @@ -1249,7 +1256,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.statusButtonNode.addSubnode(self.statusNode) self.statusButtonNode.addTarget(self, action: #selector(self.statusButtonPressed), forControlEvents: .touchUpInside) - self.addSubnode(self.statusButtonNode) + //self.addSubnode(self.statusButtonNode) self.footerContentNode.playbackControl = { [weak self] in if let strongSelf = self { @@ -1354,7 +1361,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if case let .message(message, _) = self.item?.contentInfo, let _ = message.adAttribute { adMessage = message } - self.openMoreMenu(sourceNode: self.moreBarButton.referenceNode, gesture: gesture, adMessage: adMessage, isSettings: false) + self.openMoreMenu(sourceView: self.moreBarButton.referenceNode.view, gesture: gesture, adMessage: adMessage, isSettings: false) } self.titleContentView = GalleryTitleView(frame: CGRect()) @@ -1432,6 +1439,60 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { transition.updateFrame(node: self.statusButtonNode, frame: statusFrame) transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(), size: statusFrame.size)) + let hideControls = self.item?.hideControls ?? true + + var playbackControlsIsPlaying: Bool = false + var playbackControlsIsSeekable: Bool = false + var playbackControlsIsVisible: Bool = false + if case let .playback(paused, seekable) = self.footerContentNode.content { + playbackControlsIsVisible = true + playbackControlsIsSeekable = seekable + playbackControlsIsPlaying = !paused + } + if !self.areControlsVisible || hideControls { + playbackControlsIsVisible = false + } + + let playbackControlsSize = self.playbackControls.update( + transition: ComponentTransition(transition), + component: AnyComponent(VideoPlaybackControlsComponent( + layoutParams: VideoPlaybackControlsComponent.LayoutParams( + sideButtonSize: 64.0, + centerButtonSize: 92.0, + spacing: 30.0 + ), + isVisible: playbackControlsIsVisible, + isPlaying: playbackControlsIsPlaying, + displaySeekControls: playbackControlsIsSeekable, + togglePlayback: { [weak self] in + guard let self else { + return + } + self.footerContentNode.playbackControlPressed() + }, + seek: { [weak self] isForward in + guard let self else { + return + } + if isForward { + self.footerContentNode.seekForward?(15.0) + } else { + self.footerContentNode.seekBackward?(15.0) + } + } + )), + environment: {}, + containerSize: CGSize(width: 1000.0, height: 1000.0) + ) + let playbackControlsFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - playbackControlsSize.width) / 2.0), y: floor((layout.size.height - playbackControlsSize.height) / 2.0)), size: playbackControlsSize) + if let playbackControlsView = self.playbackControls.view { + if playbackControlsView.superview == nil { + self.view.addSubview(playbackControlsView) + } + let transition = ComponentTransition(transition) + transition.setFrame(view: playbackControlsView, frame: playbackControlsFrame) + } + if let pictureInPictureNode = self.pictureInPictureNode { if let item = self.item { var placeholderSize = item.content.dimensions.fitted(layout.size) @@ -1934,25 +1995,38 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { strongSelf.statusButtonNode.isHidden = strongSelf.hideStatusNodeUntilCentrality || strongSelf.statusNodeShouldBeHidden } + let footerContent: ChatItemGalleryFooterContent if isAnimated || disablePlayerControls { - strongSelf.footerContentNode.content = .info + footerContent = .info } else if isPaused && !strongSelf.ignorePauseStatus && strongSelf.isCentral == true { if hasStarted || strongSelf.didPause { - strongSelf.footerContentNode.content = .playback(paused: true, seekable: seekable) + footerContent = .playback(paused: true, seekable: seekable) } else if let fetchStatus = fetchStatus, !strongSelf.requiresDownload { if item.content is HLSVideoContent { - strongSelf.footerContentNode.content = .playback(paused: true, seekable: seekable) + footerContent = .playback(paused: true, seekable: seekable) } else { - strongSelf.footerContentNode.content = .fetch(status: fetchStatus, seekable: seekable) + footerContent = .fetch(status: fetchStatus, seekable: seekable) } + } else { + footerContent = .info } } else { - strongSelf.footerContentNode.content = .playback(paused: false, seekable: seekable) + footerContent = .playback(paused: false, seekable: seekable) + } + + if strongSelf.footerContentNode.content != footerContent { + strongSelf.footerContentNode.content = footerContent + + if let validLayout = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(validLayout.layout, navigationBarHeight: validLayout.navigationBarHeight, transition: .animated(duration: 0.2, curve: .easeInOut)) + } } } })) self.zoomableContent = (videoSize, videoNode) + + var hasSettingsButton = false var barButtonItems: [UIBarButtonItem] = [] if hasLinkedStickers { @@ -1962,16 +2036,17 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } if forceEnablePiP || (!isAnimated && !disablePlayerControls && !disablePictureInPicture) { - let rightBarButtonItem = UIBarButtonItem(image: pictureInPictureButtonImage, style: .plain, target: self, action: #selector(self.pictureInPictureButtonPressed)) + /*let rightBarButtonItem = UIBarButtonItem(image: pictureInPictureButtonImage, style: .plain, target: self, action: #selector(self.pictureInPictureButtonPressed)) rightBarButtonItem.accessibilityLabel = self.presentationData.strings.Gallery_VoiceOver_PictureInPicture self.pictureInPictureButton = rightBarButtonItem - barButtonItems.append(rightBarButtonItem) + barButtonItems.append(rightBarButtonItem)*/ self.hasPictureInPicture = true } else { self.hasPictureInPicture = false } - + if let contentInfo = item.contentInfo, case let .message(message, mediaIndex) = contentInfo { + var hasMoreButton = false var file: TelegramMediaFile? for m in message.media { if let m = m as? TelegramMediaFile, m.isVideo { @@ -1990,7 +2065,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } - var hasMoreButton = false if isEnhancedWebPlayer { hasMoreButton = true } else if let file = file, !file.isAnimated { @@ -2006,9 +2080,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } if !isAnimated && !disablePlayerControls { - let settingsMenuItem = UIBarButtonItem(customDisplayNode: self.settingsBarButton)! + /*let settingsMenuItem = UIBarButtonItem(customDisplayNode: self.settingsBarButton)! settingsMenuItem.accessibilityLabel = self.presentationData.strings.Settings_Title - barButtonItems.append(settingsMenuItem) + barButtonItems.append(settingsMenuItem)*/ + hasSettingsButton = true } if hasMoreButton { @@ -2018,6 +2093,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } + self.hasSettingsButton = hasSettingsButton self._rightBarButtonItems.set(.single(barButtonItems)) videoNode.playbackCompleted = { [weak self, weak videoNode] in @@ -2072,7 +2148,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { switch contentInfo { case let .message(message, _): isAd = message.adAttribute != nil - self.footerContentNode.setMessage(message, displayInfo: !item.displayInfoOnTop, peerIsCopyProtected: item.peerIsCopyProtected) + self.footerContentNode.setMessage(message, displayInfo: !item.displayInfoOnTop, peerIsCopyProtected: item.peerIsCopyProtected, displayPictureInPictureButton: self.hasPictureInPicture, displaySettingsButton: self.hasSettingsButton) case let .webPage(webPage, media, _): self.footerContentNode.setWebPage(webPage, media: media) } @@ -2096,18 +2172,23 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.overlayContentNode.presentPremiumDemo = { [weak self] in self?.presentPremiumDemo() } - self.overlayContentNode.openMoreMenu = { [weak self] sourceNode, adMessage in - self?.openMoreMenu(sourceNode: sourceNode, gesture: nil, adMessage: adMessage, isSettings: false, actionsOnTop: true) + self.overlayContentNode.openMoreMenu = { [weak self] sourceView, adMessage in + self?.openMoreMenu(sourceView: sourceView, gesture: nil, adMessage: adMessage, isSettings: false, actionsOnTop: true) } self.overlayContentNode.setMessage(context: item.context, message: message) } } override func controlsVisibilityUpdated(isVisible: Bool) { + self.areControlsVisible = isVisible self.controlsVisiblePromise.set(isVisible) self.videoNode?.isUserInteractionEnabled = isVisible ? self.videoNodeUserInteractionEnabled : false self.videoNode?.notifyPlaybackControlsHidden(!isVisible) + + if let validLayout = self.validLayout { + self.containerLayoutUpdated(validLayout.layout, navigationBarHeight: validLayout.navigationBarHeight, transition: .animated(duration: 0.2, curve: .easeInOut)) + } } private func updateDisplayPlaceholder() { @@ -2527,6 +2608,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.statusButtonNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.statusButtonNode.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) self.statusButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) self.statusButtonNode.layer.animateScale(from: 0.5, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + + if let playbackControlsView = self.playbackControls.view { + playbackControlsView.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: playbackControlsView.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + playbackControlsView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring) + playbackControlsView.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + } } } @@ -2618,6 +2705,13 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.statusButtonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) self.statusButtonNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.25, removeOnCompletion: false) + if let playbackControlsView = self.playbackControls.view { + playbackControlsView.layer.animatePosition(from: playbackControlsView.layer.position, to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + }) + playbackControlsView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + playbackControlsView.layer.animateScale(from: 1.0, to: 0.2, duration: 0.25, removeOnCompletion: false) + } + let fromTransform: CATransform3D let toTransform: CATransform3D @@ -3017,7 +3111,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { switch contentInfo { case let .message(message, _): isAd = message.adAttribute != nil - self.footerContentNode.setMessage(message, displayInfo: !item.displayInfoOnTop, peerIsCopyProtected: item.peerIsCopyProtected) + self.footerContentNode.setMessage(message, displayInfo: !item.displayInfoOnTop, peerIsCopyProtected: item.peerIsCopyProtected, displayPictureInPictureButton: self.hasPictureInPicture, displaySettingsButton: self.hasSettingsButton) case let .webPage(webPage, media, _): self.footerContentNode.setWebPage(webPage, media: media) } @@ -3218,7 +3312,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } private var playOnDismiss = false - private func openMoreMenu(sourceNode: ContextReferenceContentNode, gesture: ContextGesture?, adMessage: Message?, isSettings: Bool, actionsOnTop: Bool = false) { + private func openMoreMenu(sourceView: UIView, gesture: ContextGesture?, adMessage: Message?, isSettings: Bool, actionsOnTop: Bool = false) { guard let controller = self.baseNavigationController()?.topViewController as? ViewController else { return } @@ -3235,7 +3329,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { }) } - var sourceView = sourceNode.view + var sourceView = sourceView if !isSettings { if let value = self.galleryController()?.navigationBar?.navigationButtonContextContainer(sourceView: sourceView) { sourceView = value @@ -3900,8 +3994,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { }) } - @objc private func settingsButtonPressed() { - self.openMoreMenu(sourceNode: self.settingsBarButton.referenceNode, gesture: nil, adMessage: nil, isSettings: true) + func settingsButtonPressed(sourceView: UIView) { + self.openMoreMenu(sourceView: sourceView, gesture: nil, adMessage: nil, isSettings: true, actionsOnTop: true) } override func adjustForPreviewing() { diff --git a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift index 3a33f9bde7..ca58bd1c74 100644 --- a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift +++ b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift @@ -253,6 +253,8 @@ public final class SecretMediaPreviewController: ViewController { }, editMedia: { _ in }, controller: { [weak self] in return self + }, currentItemNode: { [weak self] in + return self?.controllerNode.pager.centralItemNode() }) self.displayNode = SecretMediaPreviewControllerNode(context: self.context, controllerInteraction: controllerInteraction) self.displayNodeDidLoad() diff --git a/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift b/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift index 9a8f39f9bb..e5a235dd03 100644 --- a/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift +++ b/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift @@ -376,6 +376,8 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable }, editMedia: { _ in }, controller: { [weak self] in return self + }, currentItemNode: { [weak self] in + return self?.galleryNode.pager.centralItemNode() }) self.displayNode = GalleryControllerNode(context: self.context,controllerInteraction: controllerInteraction) self.displayNodeDidLoad() diff --git a/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift b/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift index b964bd591f..ab98ca0550 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift @@ -450,7 +450,13 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { switch scrubberHandle { case .none: - break + let handleNode = ASImageNode() + handleNode.isLayerBacked = true + handleNodeImpl = handleNode + + let handleNodeContainer = MediaPlayerScrubbingNodeButton() + handleNodeContainer.addSubnode(handleNode) + handleNodeContainerImpl = handleNodeContainer case .line: let handleNode = ASImageNode() handleNode.image = generateHandleBackground(color: foregroundColor) @@ -934,7 +940,7 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { if i == node.chapterNodes.count - 1 { let startPosition = endPosition + lineWidth - chapterNode.frame = CGRect(x: startPosition, y: 0.0, width: backgroundFrame.size.width - startPosition, height: backgroundFrame.size.height) + chapterNode.frame = CGRect(x: startPosition, y: 0.0, width: max(0.0, backgroundFrame.size.width - startPosition), height: backgroundFrame.size.height) } } } else { diff --git a/submodules/MediaPlayer/Sources/MediaPlayerTimeTextNode.swift b/submodules/MediaPlayer/Sources/MediaPlayerTimeTextNode.swift index 824b909963..9ad39a6e68 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerTimeTextNode.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerTimeTextNode.swift @@ -171,6 +171,31 @@ public final class MediaPlayerTimeTextNode: ASDisplayNode { self.updateTimer = nil } + public static func timestampString(for status: MediaPlayerStatus, mode: MediaPlayerTimeTextNodeMode) -> String? { + if Double(0.0).isLess(than: status.duration) { + let timestamp = max(0.0, status.timestamp) + let duration = status.duration + let timestampSeconds: Double + if !status.generationTimestamp.isZero { + timestampSeconds = timestamp + (CACurrentMediaTime() - status.generationTimestamp) + } else { + timestampSeconds = timestamp + } + switch mode { + case .normal: + let timestamp = Int32(truncatingIfNeeded: Int64(floor(timestampSeconds))) + let state = MediaPlayerTimeTextNodeState(hours: timestamp / (60 * 60), minutes: timestamp % (60 * 60) / 60, seconds: timestamp % 60) + return state.string + case .reversed: + let timestamp = abs(Int32(Int32(truncatingIfNeeded: Int64(floor(timestampSeconds - duration))))) + let state = MediaPlayerTimeTextNodeState(hours: timestamp / (60 * 60), minutes: timestamp % (60 * 60) / 60, seconds: timestamp % 60) + return state.string + } + } else { + return nil + } + } + func updateTimestamp() { if ((self.statusValue?.duration ?? 0.0) < 0.1) && self.state.seconds != nil && self.keepPreviousValueOnEmptyState { return @@ -218,14 +243,6 @@ public final class MediaPlayerTimeTextNode: ASDisplayNode { } } - private let digitsSet = CharacterSet(charactersIn: "0123456789") - private func widthForString(_ string: String) -> CGFloat { - let convertedString = string.components(separatedBy: digitsSet).joined(separator: "8") - let text = NSAttributedString(string: convertedString, font: textFont, textColor: .black) - let size = text.boundingRect(with: CGSize(width: 200.0, height: 100.0), options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil).size - return size.width - } - override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { return MediaPlayerTimeTextNodeParameters(state: self.state, alignment: self.alignment, mode: self.mode, textColor: self.textColor, textFont: self.textFont) } diff --git a/submodules/PassportUI/Sources/SecureIdDocumentGalleryController.swift b/submodules/PassportUI/Sources/SecureIdDocumentGalleryController.swift index 7a1d041ca9..34d00e4be4 100644 --- a/submodules/PassportUI/Sources/SecureIdDocumentGalleryController.swift +++ b/submodules/PassportUI/Sources/SecureIdDocumentGalleryController.swift @@ -186,6 +186,8 @@ class SecureIdDocumentGalleryController: ViewController, StandalonePresentableCo }, editMedia: { _ in }, controller: { [weak self] in return self + }, currentItemNode: { [weak self] in + return self?.galleryNode.pager.centralItemNode() }) self.displayNode = GalleryControllerNode(context: self.context, controllerInteraction: controllerInteraction) self.displayNodeDidLoad() diff --git a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift index 72931c6586..15c15c26cc 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift @@ -630,6 +630,8 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr }, editMedia: { _ in }, controller: { [weak self] in return self + }, currentItemNode: { [weak self] in + return self?.galleryNode.pager.centralItemNode() }) self.displayNode = GalleryControllerNode(context: self.context, controllerInteraction: controllerInteraction) self.displayNodeDidLoad() diff --git a/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift b/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift index 136b37aacb..7ddee04daa 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift @@ -212,7 +212,12 @@ func parseTelegramGroupOrChannel(chat: Api.Chat) -> Peer? { info = .broadcast(TelegramChannelBroadcastInfo(flags: [])) } - return TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)), accessHash: .personal(accessHash), title: title, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .kicked, info: info, flags: TelegramChannelFlags(), restrictionInfo: nil, adminRights: nil, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: untilDate ?? Int32.max), defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil, verificationIconFileId: nil, sendPaidMessageStars: nil, linkedMonoforumId: nil) + var channelFlags = TelegramChannelFlags() + if (flags & (1 << 10)) != 0 { + channelFlags.insert(.isMonoforum) + } + + return TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)), accessHash: .personal(accessHash), title: title, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .kicked, info: info, flags: channelFlags, restrictionInfo: nil, adminRights: nil, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: untilDate ?? Int32.max), defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil, verificationIconFileId: nil, sendPaidMessageStars: nil, linkedMonoforumId: nil) } } diff --git a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift index bd4550c429..071b4cf817 100644 --- a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift +++ b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift @@ -552,7 +552,7 @@ private final class AlertScreenComponent: Component { } transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - alertSize.width) / 2.0), y: floorToScreenPixels((availableHeight - alertSize.height) / 2.0)), size: alertSize)) - self.backgroundView.update(size: alertSize, shape: .roundedRect(cornerRadius: 35.0), isDark: environment.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: .white), isInteractive: true, transition: transition) + self.backgroundView.update(size: alertSize, cornerRadius: 35.0, isDark: environment.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: .white), isInteractive: true, transition: transition) return availableSize } diff --git a/submodules/TelegramUI/Components/AsyncListComponent/Sources/AsyncListComponent.swift b/submodules/TelegramUI/Components/AsyncListComponent/Sources/AsyncListComponent.swift index 5d798af850..1a537db8de 100644 --- a/submodules/TelegramUI/Components/AsyncListComponent/Sources/AsyncListComponent.swift +++ b/submodules/TelegramUI/Components/AsyncListComponent/Sources/AsyncListComponent.swift @@ -582,6 +582,9 @@ public final class AsyncListComponent: Component { updateSizeAndInsets.curve = .Spring(duration: duration) case let .custom(a, b, c, d): updateSizeAndInsets.curve = .Custom(duration: duration, a, b, c, d) + case .bounce: + assertionFailure() + updateSizeAndInsets.curve = .Spring(duration: duration) } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index 730327cfaa..08b1e3126c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -3272,6 +3272,10 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } public func scrubberTransition() -> GalleryItemScrubberTransition? { + if "".isEmpty { + return nil + } + final class TimestampContainerTransitionView: UIView { let containerView: UIView let containerMaskView: UIImageView diff --git a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerActionsStackNode.swift b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerActionsStackNode.swift index d53c997d31..5aff5d2d59 100644 --- a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerActionsStackNode.swift @@ -1385,6 +1385,7 @@ private final class ItemSelectionRecognizer: UIGestureRecognizer { public final class ContextControllerActionsStackNodeImpl: ASDisplayNode, ContextControllerActionsStackNode { final class NavigationContainer: ASDisplayNode, ASGestureRecognizerDelegate { let backgroundContainer: GlassBackgroundContainerView + let backgroundContainerInset: CGFloat let backgroundView: GlassBackgroundView var sourceExtractableContainer: ContextExtractableContainer? let contentContainer: UIView @@ -1410,6 +1411,8 @@ public final class ContextControllerActionsStackNodeImpl: ASDisplayNode, Context self.contentContainer.clipsToBounds = true self.backgroundView.contentView.addSubview(self.contentContainer) + self.backgroundContainerInset = 32.0 + super.init() self.view.addSubview(self.backgroundContainer) @@ -1466,11 +1469,12 @@ public final class ContextControllerActionsStackNodeImpl: ASDisplayNode, Context } } - func animateIn(fromExtractableContainer extractableContainer: ContextExtractableContainer) { - let transition: ComponentTransition = .spring(duration: 0.42) + func animateIn(fromExtractableContainer extractableContainer: ContextExtractableContainer, transition: ComponentTransition) { + let normalState = extractableContainer.normalState + let sourceSize = normalState.size + let normalCornerRadius: CGFloat = normalState.cornerRadius - let normalSize = extractableContainer.extractableContentView.bounds.size - let normalCornerRadius: CGFloat = min(normalSize.width, normalSize.height) * 0.5 + let currentSize = self.contentContainer.bounds.size self.sourceExtractableContainer = extractableContainer self.backgroundView.isHidden = true @@ -1485,34 +1489,50 @@ public final class ContextControllerActionsStackNodeImpl: ASDisplayNode, Context } self.sourceExtractableContainer = nil - self.contentContainer.frame = CGRect(origin: CGPoint(), size: normalSize) + self.contentContainer.frame = CGRect(origin: CGPoint(), size: sourceSize) self.contentContainer.layer.cornerRadius = normalCornerRadius - transition.setFrame(view: self.contentContainer, frame: CGRect(origin: CGPoint(), size: self.backgroundContainer.bounds.size)) + extractableContainer.extractableContentView.frame = CGRect(origin: CGPoint(x: (currentSize.width - sourceSize.width) * 0.5, y: (currentSize.height - sourceSize.height) * 0.5), size: sourceSize).offsetBy(dx: self.backgroundContainerInset, dy: self.backgroundContainerInset) + transition.setFrame(view: extractableContainer.extractableContentView, frame: CGRect(origin: CGPoint(x: self.backgroundContainerInset, y: self.backgroundContainerInset), size: currentSize)) + transition.setFrame(view: self.contentContainer, frame: CGRect(origin: CGPoint(), size: currentSize)) transition.setCornerRadius(layer: self.contentContainer.layer, cornerRadius: 30.0) self.contentContainer.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - extractableContainer.updateState(state: .extracted(size: normalSize, cornerRadius: normalCornerRadius, state: .animatedOut), transition: .immediate) - extractableContainer.updateState(state: .extracted(size: self.backgroundContainer.bounds.size, cornerRadius: 30.0, state: .animatedIn), transition: transition.containedViewLayoutTransition) + extractableContainer.updateState(state: .extracted(size: sourceSize, cornerRadius: normalCornerRadius, state: .animatedOut), transition: .transition(.immediate), completion: nil) + let mappedTransition: ContextExtractableContainer.Transition + if case let .curve(duration, curve) = transition.animation, case let .bounce(stiffness, damping) = curve { + mappedTransition = .spring(duration: duration, stiffness: stiffness, damping: damping) + } else { + mappedTransition = .transition(transition.containedViewLayoutTransition) + } + extractableContainer.updateState(state: .extracted(size: currentSize, cornerRadius: 30.0, state: .animatedIn), transition: mappedTransition, completion: nil) } - func animateOut(toExtractableContainer extractableContainer: ContextExtractableContainer, transition: ContainedViewLayoutTransition) { - let transition = ComponentTransition(transition) + func animateOut(toExtractableContainer extractableContainer: ContextExtractableContainer, transition: ComponentTransition) { + let normalState = extractableContainer.normalState + let normalSize = normalState.size + let normalCornerRadius: CGFloat = normalState.cornerRadius - let normalSize = extractableContainer.extractableContentView.bounds.size - let normalCornerRadius: CGFloat = min(normalSize.width, normalSize.height) * 0.5 + let currentSize = self.contentContainer.bounds.size + + transition.setFrame(view: extractableContainer.extractableContentView, frame: CGRect(origin: CGPoint(x: self.backgroundContainerInset, y: self.backgroundContainerInset), size: normalSize).offsetBy(dx: (currentSize.width - normalSize.width) * 0.5, dy: (currentSize.height - normalSize.height) * 0.5)) transition.setFrame(view: self.contentContainer, frame: CGRect(origin: CGPoint(), size: normalSize)) - transition.attachAnimation(view: self.contentContainer, id: "animateOut", completion: { [weak extractableContainer] _ in - guard let extractableContainer else { - return - } - extractableContainer.addSubview(extractableContainer.extractableContentView) - }) transition.setCornerRadius(layer: self.contentContainer.layer, cornerRadius: normalCornerRadius) transition.setAlpha(view: self.contentContainer, alpha: 0.0) - extractableContainer.updateState(state: .normal, transition: transition.containedViewLayoutTransition) + let mappedTransition: ContextExtractableContainer.Transition + if case let .curve(duration, curve) = transition.animation, case let .bounce(stiffness, damping) = curve { + mappedTransition = .spring(duration: duration, stiffness: stiffness, damping: damping) + } else { + mappedTransition = .transition(transition.containedViewLayoutTransition) + } + extractableContainer.updateState(state: .extracted(size: normalSize, cornerRadius: normalCornerRadius, state: .animatedOut), transition: mappedTransition, completion: nil) + } + + func didAnimateOut(toExtractableContainer extractableContainer: ContextExtractableContainer) { + extractableContainer.addSubview(extractableContainer.extractableContentView) + extractableContainer.updateState(state: .normal, transition: .transition(.immediate), completion: nil) } func update(presentationData: PresentationData, presentation: Presentation, size: CGSize, transition: ContainedViewLayoutTransition) { @@ -1520,34 +1540,21 @@ public final class ContextControllerActionsStackNodeImpl: ASDisplayNode, Context transition.setFrame(view: self.contentContainer, frame: CGRect(origin: CGPoint(), size: size)) - var backgroundContainerSize = size - if !transition.animation.isImmediate && (size.width < self.backgroundContainer.bounds.width || size.height < self.backgroundContainer.bounds.height) { - backgroundContainerSize = CGSize( - width: max(self.backgroundContainer.bounds.width, size.width), - height: max(self.backgroundContainer.bounds.height, size.height) - ) - } + let backgroundContainerFrame = CGRect(origin: CGPoint(), size: size).insetBy(dx: -self.backgroundContainerInset, dy: -self.backgroundContainerInset) - if self.backgroundContainer.bounds.size != size { - self.backgroundContainer.update(size: backgroundContainerSize, isDark: presentationData.theme.overallDarkAppearance, transition: transition) - - let isDark = presentationData.theme.overallDarkAppearance - transition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: CGPoint(), size: size), completion: { [weak self] completed in - guard let self, completed else { - return - } - self.backgroundContainer.update(size: self.backgroundContainer.bounds.size, isDark: isDark, transition: .immediate) - }) + if self.backgroundContainer.bounds.size != backgroundContainerFrame.size { + self.backgroundContainer.update(size: backgroundContainerFrame.size, isDark: presentationData.theme.overallDarkAppearance, transition: transition) + transition.setFrame(view: self.backgroundContainer, frame: backgroundContainerFrame) } transition.setCornerRadius(layer: self.contentContainer.layer, cornerRadius: min(30.0, size.height * 0.5)) - transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: self.backgroundContainerInset, y: self.backgroundContainerInset), size: size)) self.backgroundView.update(size: size, cornerRadius: min(30.0, size.height * 0.5), isDark: presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: presentationData.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: transition) if let sourceExtractableContainer = self.sourceExtractableContainer { transition.setFrame(view: sourceExtractableContainer.extractableContentView, frame: CGRect(origin: CGPoint(), size: size)) - sourceExtractableContainer.updateState(state: .extracted(size: size, cornerRadius: min(30.0, size.height * 0.5), state: .animatedIn), transition: transition.containedViewLayoutTransition) + sourceExtractableContainer.updateState(state: .extracted(size: size, cornerRadius: min(30.0, size.height * 0.5), state: .animatedIn), transition: .transition(transition.containedViewLayoutTransition), completion: nil) } } } @@ -2185,11 +2192,15 @@ public final class ContextControllerActionsStackNodeImpl: ASDisplayNode, Context } } - func animateIn(fromExtractableContainer extractableContainer: ContextExtractableContainer) { - self.navigationContainer.animateIn(fromExtractableContainer: extractableContainer) + func animateIn(fromExtractableContainer extractableContainer: ContextExtractableContainer, transition: ComponentTransition) { + self.navigationContainer.animateIn(fromExtractableContainer: extractableContainer, transition: transition) } - func animateOut(toExtractableContainer extractableContainer: ContextExtractableContainer, transition: ContainedViewLayoutTransition) { + func animateOut(toExtractableContainer extractableContainer: ContextExtractableContainer, transition: ComponentTransition) { self.navigationContainer.animateOut(toExtractableContainer: extractableContainer, transition: transition) } + + func didAnimateOut(toExtractableContainer extractableContainer: ContextExtractableContainer) { + self.navigationContainer.didAnimateOut(toExtractableContainer: extractableContainer) + } } diff --git a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift index b0df255488..78cde2f0e3 100644 --- a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift @@ -1345,14 +1345,15 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } if let contextExtractableContainer { - let transition: ContainedViewLayoutTransition = .animated(duration: duration, curve: .spring) + let positionTransition = ComponentTransition(animation: .curve(duration: 0.4, curve: .bounce(stiffness: 900.0, damping: 95.0))) + let transition = ComponentTransition(animation: .curve(duration: 0.5, curve: .bounce(stiffness: 900.0, damping: 95.0))) - transition.animatePositionAdditive(layer: self.actionsContainerNode.layer, offset: CGPoint( - x: contextExtractableContainer.sourceRect.minX - self.actionsContainerNode.frame.minX, - y: contextExtractableContainer.sourceRect.minY - self.actionsContainerNode.frame.minY - )) + positionTransition.animatePosition(layer: self.actionsContainerNode.layer, from: CGPoint( + x: contextExtractableContainer.sourceRect.midX - self.actionsContainerNode.frame.midX, + y: contextExtractableContainer.sourceRect.midY - self.actionsContainerNode.frame.midY + ), to: CGPoint(), additive: true) - self.actionsStackNode.animateIn(fromExtractableContainer: contextExtractableContainer.container) + self.actionsStackNode.animateIn(fromExtractableContainer: contextExtractableContainer.container, transition: transition) } else { self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: self.actionsContainerNode.alpha, duration: 0.05) self.actionsContainerNode.layer.animateSpring( @@ -1701,16 +1702,24 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } if let contextExtractableContainer { - let transition: ContainedViewLayoutTransition = .animated(duration: duration, curve: .easeInOut) + let positionTransition = ComponentTransition(animation: .curve(duration: 0.5, curve: .bounce(stiffness: 900.0, damping: 95.0))) + let transition = ComponentTransition(animation: .curve(duration: 0.12, curve: .easeInOut)) - transition.updateFrame(node: self.actionsContainerNode, frame: CGRect(origin: CGPoint(x: contextExtractableContainer.sourceRect.minX, y: contextExtractableContainer.sourceRect.minY), size: self.actionsContainerNode.bounds.size)) - ComponentTransition(transition).attachAnimation(view: self.actionsContainerNode.view, id: "animateOut", completion: { _ in + let contextExtractableContainerView = contextExtractableContainer.container + + positionTransition.setPosition(view: self.actionsContainerNode.view, position: CGPoint(x: contextExtractableContainer.sourceRect.midX, y: contextExtractableContainer.sourceRect.midY), completion: { _ in if completeWithActionStack { restoreOverlayViews.forEach({ $0() }) completion() } }) + positionTransition.attachAnimation(view: self.actionsContainerNode.view, id: "animateOut", completion: { [weak self, weak contextExtractableContainerView] _ in + if let self, let contextExtractableContainerView { + self.actionsStackNode.didAnimateOut(toExtractableContainer: contextExtractableContainerView) + } + }) + self.actionsStackNode.animateOut(toExtractableContainer: contextExtractableContainer.container, transition: transition) } else { self.actionsContainerNode.layer.animateAlpha(from: self.actionsContainerNode.alpha, to: 0.0, duration: duration, removeOnCompletion: false) diff --git a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift index 5744d55761..cc3fe098e5 100644 --- a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift +++ b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift @@ -250,6 +250,7 @@ public class GlassBackgroundView: UIView { public struct TintColor: Equatable { public enum Kind { case panel + case clear case custom } @@ -411,12 +412,10 @@ public class GlassBackgroundView: UIView { } return nil } - - public func update(size: CGSize, cornerRadius: CGFloat, isDark: Bool, tintColor: TintColor, isInteractive: Bool = false, isVisible: Bool = true, transition: ComponentTransition) { - self.update(size: size, shape: .roundedRect(cornerRadius: cornerRadius), isDark: isDark, tintColor: tintColor, isInteractive: isInteractive, isVisible: isVisible, transition: transition) - } - public func update(size: CGSize, shape: Shape, isDark: Bool, tintColor: TintColor, isInteractive: Bool = false, isVisible: Bool = true, transition: ComponentTransition) { + public func update(size: CGSize, cornerRadius: CGFloat, isDark: Bool, tintColor: TintColor, isInteractive: Bool = false, isVisible: Bool = true, transition: ComponentTransition) { + let shape: Shape = .roundedRect(cornerRadius: cornerRadius) + if let nativeView = self.nativeView, let nativeViewClippingContext = self.nativeViewClippingContext, (nativeView.bounds.size != size || nativeViewClippingContext.shape != shape) { nativeViewClippingContext.update(shape: shape, size: size, transition: transition) @@ -515,6 +514,8 @@ public class GlassBackgroundView: UIView { } else { fillColor = UIColor(white: 1.0, alpha: 0.7) } + case .clear: + fillColor = UIColor(white: 1.0, alpha: 0.0) case .custom: fillColor = tintColor.color } @@ -526,15 +527,20 @@ public class GlassBackgroundView: UIView { var glassEffect: UIGlassEffect? if isVisible { - let glassEffectValue = UIGlassEffect(style: .regular) + let glassEffectValue: UIGlassEffect switch tintColor.kind { case .panel: + glassEffectValue = UIGlassEffect(style: .regular) if isDark { glassEffectValue.tintColor = UIColor(white: 1.0, alpha: 0.025) } else { glassEffectValue.tintColor = UIColor(white: 1.0, alpha: 0.1) } case .custom: + glassEffectValue = UIGlassEffect(style: .regular) + glassEffectValue.tintColor = tintColor.color + case .clear: + glassEffectValue = UIGlassEffect(style: .clear) glassEffectValue.tintColor = tintColor.color } glassEffectValue.isInteractive = params.isInteractive @@ -1113,6 +1119,19 @@ public final class GlassContextExtractableContainer: UIView, ContextExtractableC return self.normalContentView } + public var normalState: NormalState { + guard let normalParams = self.normalParams else { + return NormalState( + size: CGSize(), + cornerRadius: 0.0 + ) + } + return NormalState( + size: normalParams.size, + cornerRadius: normalParams.cornerRadius + ) + } + private let glassView: GlassBackgroundView private var state: State = .normal @@ -1161,24 +1180,36 @@ public final class GlassContextExtractableContainer: UIView, ContextExtractableC self.normalParams = normalParams if case .normal = self.state { - self.applyState(transition: transition) + self.applyState(transition: .transition(transition.containedViewLayoutTransition), completion: nil) } } - public func updateState(state: State, transition: ContainedViewLayoutTransition) { + public func updateState(state: State, transition: Transition, completion: ((Bool) -> Void)?) { self.state = state - self.applyState(transition: ComponentTransition(transition)) + self.applyState(transition: transition, completion: completion) } - private func applyState(transition: ComponentTransition) { + private func applyState(transition: Transition, completion: ((Bool) -> Void)?) { guard let normalParams = self.normalParams else { + completion?(true) return } + + let mappedTransition: ComponentTransition + switch transition { + case let .transition(transition): + mappedTransition = ComponentTransition(transition) + case let .spring(duration, stiffness, damping): + mappedTransition = ComponentTransition(animation: .curve(duration: duration, curve: .bounce(stiffness: stiffness, damping: damping))) + } + switch self.state { case .normal: - transition.setAlpha(view: self.normalContentView, alpha: 1.0) - transition.setFrame(view: self.extractableContentView, frame: CGRect(origin: CGPoint(), size: normalParams.size)) - transition.setFrame(view: self.normalContentView, frame: CGRect(origin: CGPoint(), size: normalParams.size)) + mappedTransition.setAlpha(view: self.normalContentView, alpha: 1.0) + mappedTransition.setFrame(view: self.extractableContentView, frame: CGRect(origin: CGPoint(), size: normalParams.size)) + mappedTransition.setFrame(view: self.normalContentView, frame: CGRect(origin: CGPoint(), size: normalParams.size), completion: { completed in + completion?(completed) + }) self.glassView.update( size: normalParams.size, @@ -1187,12 +1218,14 @@ public final class GlassContextExtractableContainer: UIView, ContextExtractableC tintColor: normalParams.tintColor, isInteractive: normalParams.isInteractive, isVisible: normalParams.isVisible, - transition: transition + transition: mappedTransition, ) case let .extracted(size, cornerRadius, extractionState): switch extractionState { case .animatedOut: - transition.setAlpha(view: self.normalContentView, alpha: 1.0) + mappedTransition.setAlpha(view: self.normalContentView, alpha: 1.0, completion: { completed in + completion?(completed) + }) self.glassView.update( size: normalParams.size, @@ -1201,10 +1234,12 @@ public final class GlassContextExtractableContainer: UIView, ContextExtractableC tintColor: normalParams.tintColor, isInteractive: normalParams.isInteractive, isVisible: normalParams.isVisible, - transition: transition + transition: mappedTransition ) case .animatedIn: - transition.setAlpha(view: self.normalContentView, alpha: 0.0) + mappedTransition.setAlpha(view: self.normalContentView, alpha: 0.0, completion: { completed in + completion?(completed) + }) self.glassView.update( size: size, @@ -1213,7 +1248,7 @@ public final class GlassContextExtractableContainer: UIView, ContextExtractableC tintColor: normalParams.tintColor, isInteractive: normalParams.isInteractive, isVisible: normalParams.isVisible, - transition: transition + transition: mappedTransition ) } } diff --git a/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlGroup.swift b/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlGroup.swift index 6b5638b356..c8990ca441 100644 --- a/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlGroup.swift +++ b/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlGroup.swift @@ -76,10 +76,20 @@ public final class GlassControlGroupComponent: Component { } return true } + + private struct ItemId: Hashable { + let id: AnyHashable + let contentId: AnyHashable + + init(id: AnyHashable, contentId: AnyHashable) { + self.id = id + self.contentId = contentId + } + } public final class View: UIView { private let backgroundView: GlassBackgroundView - private var itemViews: [AnyHashable: ComponentView] = [:] + private var itemViews: [ItemId: ComponentView] = [:] private var component: GlassControlGroupComponent? private weak var state: EmptyComponentState? @@ -97,7 +107,12 @@ public final class GlassControlGroupComponent: Component { } public func itemView(id: AnyHashable) -> UIView? { - return self.itemViews[id]?.view + for (itemId, itemView) in self.itemViews { + if itemId.id == id { + return itemView.view + } + } + return nil } func update(component: GlassControlGroupComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { @@ -106,16 +121,6 @@ public final class GlassControlGroupComponent: Component { self.component = component self.state = state - struct ItemId: Hashable { - var id: AnyHashable - var contentId: AnyHashable - - init(id: AnyHashable, contentId: AnyHashable) { - self.id = id - self.contentId = contentId - } - } - var contentsWidth: CGFloat = 0.0 var validIds: [AnyHashable] = [] var isInteractive = false @@ -196,7 +201,7 @@ public final class GlassControlGroupComponent: Component { contentsWidth += itemSize.width } - var removeIds: [AnyHashable] = [] + var removeIds: [ItemId] = [] for (id, itemView) in self.itemViews { if !validIds.contains(id) { removeIds.append(id) diff --git a/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift b/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift index 811661b633..2e069f9506 100644 --- a/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift +++ b/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift @@ -511,6 +511,8 @@ final class MiniAppListScreenComponent: Component { timingFunction = kCAMediaTimingFunctionSpring case .custom: timingFunction = kCAMediaTimingFunctionSpring + case .bounce: + timingFunction = kCAMediaTimingFunctionSpring } searchBarNode.animateIn(from: placeholderNode, duration: duration, timingFunction: timingFunction) diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift index 6e85dd1eb2..40c15d77e3 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift @@ -1075,6 +1075,8 @@ final class QuickReplySetupScreenComponent: Component { timingFunction = kCAMediaTimingFunctionSpring case .custom: timingFunction = kCAMediaTimingFunctionSpring + case .bounce: + timingFunction = kCAMediaTimingFunctionSpring } searchBarNode.animateIn(from: placeholderNode, duration: duration, timingFunction: timingFunction) diff --git a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift index f41221d719..2487eeb5d1 100644 --- a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift @@ -529,6 +529,8 @@ final class PeerSelectionScreenComponent: Component { timingFunction = kCAMediaTimingFunctionSpring case .custom: timingFunction = kCAMediaTimingFunctionSpring + case .bounce: + timingFunction = kCAMediaTimingFunctionSpring } searchBarNode.animateIn(from: placeholderNode, duration: duration, timingFunction: timingFunction) diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryController.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryController.swift index 64cae7771d..2c9b9f2bbd 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryController.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryController.swift @@ -445,6 +445,8 @@ public class WallpaperGalleryController: ViewController { }, editMedia: { _ in }, controller: { [weak self] in return self + }, currentItemNode: { [weak self] in + return self?.galleryNode.pager.centralItemNode() }) self.displayNode = WallpaperGalleryControllerNode(context: self.context, controllerInteraction: controllerInteraction, pageGap: 0.0, disableTapNavigation: true) self.displayNodeDidLoad() diff --git a/submodules/TelegramUI/Components/Utils/AnimatableProperty/Sources/AnimatableProperty.swift b/submodules/TelegramUI/Components/Utils/AnimatableProperty/Sources/AnimatableProperty.swift index e2ea939b81..0b735bed62 100644 --- a/submodules/TelegramUI/Components/Utils/AnimatableProperty/Sources/AnimatableProperty.swift +++ b/submodules/TelegramUI/Components/Utils/AnimatableProperty/Sources/AnimatableProperty.swift @@ -78,6 +78,9 @@ public final class AnimatableProperty { t = lookupSpringValue(t) case let .custom(x1, y1, x2, y2): t = bezierPoint(CGFloat(x1), CGFloat(y1), CGFloat(x2), CGFloat(y2), t) + case .bounce: + assertionFailure() + t = lookupSpringValue(t) } self.presentationValue = animation.valueAt(t) as! T diff --git a/submodules/TelegramUI/Components/VideoPlaybackControlsComponent/BUILD b/submodules/TelegramUI/Components/VideoPlaybackControlsComponent/BUILD new file mode 100644 index 0000000000..e4b28c07d7 --- /dev/null +++ b/submodules/TelegramUI/Components/VideoPlaybackControlsComponent/BUILD @@ -0,0 +1,25 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "VideoPlaybackControlsComponent", + module_name = "VideoPlaybackControlsComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/TelegramPresentationData", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/AppBundle", + "//submodules/ManagedAnimationNode", + "//submodules/Components/MultilineTextComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/VideoPlaybackControlsComponent/Sources/VideoPlaybackControlsComponent.swift b/submodules/TelegramUI/Components/VideoPlaybackControlsComponent/Sources/VideoPlaybackControlsComponent.swift new file mode 100644 index 0000000000..62e8fd617f --- /dev/null +++ b/submodules/TelegramUI/Components/VideoPlaybackControlsComponent/Sources/VideoPlaybackControlsComponent.swift @@ -0,0 +1,331 @@ +import Foundation +import UIKit +import Display +import TelegramPresentationData +import ComponentFlow +import GlassBackgroundComponent +import ManagedAnimationNode +import MultilineTextComponent +import AppBundle + +public final class VideoPlaybackControlsComponent: Component { + public struct LayoutParams: Equatable { + public var sideButtonSize: CGFloat + public var centerButtonSize: CGFloat + public var spacing: CGFloat + + public init(sideButtonSize: CGFloat, centerButtonSize: CGFloat, spacing: CGFloat) { + self.sideButtonSize = sideButtonSize + self.centerButtonSize = centerButtonSize + self.spacing = spacing + } + } + + let layoutParams: LayoutParams + let isVisible: Bool + let isPlaying: Bool + let displaySeekControls: Bool + let togglePlayback: () -> Void + let seek: (Bool) -> Void + + public init( + layoutParams: LayoutParams, + isVisible: Bool, + isPlaying: Bool, + displaySeekControls: Bool, + togglePlayback: @escaping () -> Void, + seek: @escaping (Bool) -> Void + ) { + self.layoutParams = layoutParams + self.isVisible = isVisible + self.isPlaying = isPlaying + self.displaySeekControls = displaySeekControls + self.togglePlayback = togglePlayback + self.seek = seek + } + + public static func ==(lhs: VideoPlaybackControlsComponent, rhs: VideoPlaybackControlsComponent) -> Bool { + if lhs.layoutParams != rhs.layoutParams { + return false + } + if lhs.isVisible != rhs.isVisible { + return false + } + if lhs.isPlaying != rhs.isPlaying { + return false + } + if lhs.displaySeekControls != rhs.displaySeekControls { + return false + } + return true + } + + public final class View: UIView { + private let backgroundContainer: GlassBackgroundContainerView + private let leftButtonBackgroundView: GlassBackgroundView + private let leftIconView: PlaybackIconView + private let rightButtonBackgroundView: GlassBackgroundView + private let rightIconView: PlaybackIconView + private let centerButtonBackgroundView: GlassBackgroundView + private var centerButtonIconNode: PlayPauseIconNode? + + private var component: VideoPlaybackControlsComponent? + private weak var state: EmptyComponentState? + + public override init(frame: CGRect) { + self.backgroundContainer = GlassBackgroundContainerView() + + self.leftButtonBackgroundView = GlassBackgroundView() + self.leftIconView = PlaybackIconView(isForward: false) + self.leftIconView.isUserInteractionEnabled = false + self.leftButtonBackgroundView.contentView.addSubview(self.leftIconView) + + self.rightButtonBackgroundView = GlassBackgroundView() + self.rightIconView = PlaybackIconView(isForward: true) + self.rightIconView.isUserInteractionEnabled = false + self.rightButtonBackgroundView.contentView.addSubview(self.rightIconView) + + self.centerButtonBackgroundView = GlassBackgroundView() + + super.init(frame: frame) + + self.addSubview(self.backgroundContainer) + + self.backgroundContainer.contentView.addSubview(self.leftButtonBackgroundView) + self.backgroundContainer.contentView.addSubview(self.rightButtonBackgroundView) + self.backgroundContainer.contentView.addSubview(self.centerButtonBackgroundView) + + self.leftButtonBackgroundView.contentView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onLeftTapGesture(_:)))) + self.rightButtonBackgroundView.contentView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onRightTapGesture(_:)))) + self.centerButtonBackgroundView.contentView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:)))) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + @objc private func onLeftTapGesture(_ gesture: UITapGestureRecognizer) { + guard let component = self.component else { + return + } + if case .ended = gesture.state { + component.seek(false) + } + } + + @objc private func onRightTapGesture(_ gesture: UITapGestureRecognizer) { + guard let component = self.component else { + return + } + if case .ended = gesture.state { + component.seek(true) + } + } + + @objc private func onTapGesture(_ gesture: UITapGestureRecognizer) { + guard let component = self.component else { + return + } + if case .ended = gesture.state { + component.togglePlayback() + } + } + + func update(component: VideoPlaybackControlsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let isVisibleChanged = self.component?.isVisible != component.isVisible + + self.component = component + self.state = state + + self.isUserInteractionEnabled = component.isVisible + + let size = CGSize(width: component.layoutParams.sideButtonSize * 2.0 + component.layoutParams.centerButtonSize + component.layoutParams.spacing * 2.0, height: component.layoutParams.centerButtonSize) + + let leftButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - component.layoutParams.sideButtonSize) * 0.5)), size: CGSize(width: component.layoutParams.sideButtonSize, height: component.layoutParams.sideButtonSize)) + let centerButtonFrame = CGRect(origin: CGPoint(x: component.layoutParams.sideButtonSize + component.layoutParams.spacing, y: floorToScreenPixels((size.height - component.layoutParams.centerButtonSize) * 0.5)), size: CGSize(width: component.layoutParams.centerButtonSize, height: component.layoutParams.centerButtonSize)) + let rightButtonFrame = CGRect(origin: CGPoint(x: size.width - component.layoutParams.sideButtonSize, y: floorToScreenPixels((size.height - component.layoutParams.sideButtonSize) * 0.5)), size: CGSize(width: component.layoutParams.sideButtonSize, height: component.layoutParams.sideButtonSize)) + + if isVisibleChanged && !transition.animation.isImmediate { + self.backgroundContainer.isHidden = true + self.backgroundContainer.isHidden = false + } + + transition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: CGPoint(), size: size)) + self.backgroundContainer.update(size: size, isDark: true, transition: transition) + + let areSideButtonsVisible = component.isVisible && component.displaySeekControls + + transition.setFrame(view: self.leftButtonBackgroundView, frame: leftButtonFrame) + self.leftButtonBackgroundView.update(size: leftButtonFrame.size, cornerRadius: leftButtonFrame.height * 0.5, isDark: true, tintColor: .init(kind: .clear, color: .clear), isInteractive: true, isVisible: areSideButtonsVisible, transition: transition) + transition.setFrame(view: self.leftIconView, frame: CGRect(origin: CGPoint(), size: leftButtonFrame.size)) + self.leftIconView.update(size: leftButtonFrame.size) + transition.setAlpha(view: self.leftIconView, alpha: areSideButtonsVisible ? 1.0 : 0.0) + transition.setBlur(layer: self.leftIconView.layer, radius: areSideButtonsVisible ? 0.0 : 10.0) + + transition.setFrame(view: self.rightButtonBackgroundView, frame: rightButtonFrame) + self.rightButtonBackgroundView.update(size: rightButtonFrame.size, cornerRadius: rightButtonFrame.height * 0.5, isDark: true, tintColor: .init(kind: .clear, color: .clear), isInteractive: true, isVisible: areSideButtonsVisible, transition: transition) + transition.setFrame(view: self.rightIconView, frame: CGRect(origin: CGPoint(), size: rightButtonFrame.size)) + self.rightIconView.update(size: rightButtonFrame.size) + transition.setAlpha(view: self.rightIconView, alpha: areSideButtonsVisible ? 1.0 : 0.0) + transition.setBlur(layer: self.rightIconView.layer, radius: areSideButtonsVisible ? 0.0 : 10.0) + + transition.setFrame(view: self.centerButtonBackgroundView, frame: centerButtonFrame) + self.centerButtonBackgroundView.update(size: centerButtonFrame.size, cornerRadius: centerButtonFrame.height * 0.5, isDark: true, tintColor: .init(kind: .clear, color: .clear), isInteractive: true, isVisible: component.isVisible, transition: transition) + + let centerButtonIconNode: PlayPauseIconNode + let centerIconFactor: CGFloat = 0.9 + let centerButtonIconSize = CGSize(width: centerButtonFrame.width * centerIconFactor, height: centerButtonFrame.height * centerIconFactor) + if let current = self.centerButtonIconNode, current.size == centerButtonIconSize { + centerButtonIconNode = current + } else { + centerButtonIconNode = PlayPauseIconNode(size: centerButtonIconSize) + if let current = self.centerButtonIconNode { + centerButtonIconNode.frame = current.frame + current.view.removeFromSuperview() + } + self.centerButtonIconNode = centerButtonIconNode + centerButtonIconNode.isUserInteractionEnabled = false + self.centerButtonBackgroundView.contentView.addSubview(centerButtonIconNode.view) + centerButtonIconNode.enqueueState(component.isPlaying ? .pause : .play, animated: false) + } + transition.setFrame(view: centerButtonIconNode.view, frame: centerButtonIconSize.centered(in: CGRect(origin: CGPoint(), size: centerButtonFrame.size)).offsetBy(dx: component.isPlaying ? 0.0 : 5.0, dy: 0.0)) + centerButtonIconNode.enqueueState(component.isPlaying ? .pause : .play, animated: !transition.animation.isImmediate && component.isVisible && !isVisibleChanged) + transition.setAlpha(view: centerButtonIconNode.view, alpha: component.isVisible ? 1.0 : 0.0) + transition.setBlur(layer: centerButtonIconNode.layer, radius: component.isVisible ? 0.0 : 10.0) + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private enum PlayPauseIconNodeState: Equatable { + case play + case pause +} + +private final class PlayPauseIconNode: ManagedAnimationNode { + let size: CGSize + + private let duration: Double = 0.35 + private var iconState: PlayPauseIconNodeState = .pause + + override init(size: CGSize) { + self.size = size + + super.init(size: size) + + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01)) + } + + func enqueueState(_ state: PlayPauseIconNodeState, animated: Bool) { + guard self.iconState != state else { + return + } + + let previousState = self.iconState + self.iconState = state + + switch previousState { + case .pause: + switch state { + case .play: + if animated { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 83), duration: self.duration)) + } else { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) + } + case .pause: + break + } + case .play: + switch state { + case .pause: + if animated { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 41), duration: self.duration)) + } else { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01)) + } + case .play: + break + } + } + } +} + +private let circleDiameter: CGFloat = 80.0 + +private final class PlaybackIconView: HighlightTrackingButton { + let backgroundIconView: UIImageView + let text = ComponentView() + + let isForward: Bool + + var isPressing = false { + didSet { + if self.isPressing != oldValue && !self.isPressing { + self.highligthedChanged(false) + } + } + } + + init(isForward: Bool) { + self.isForward = isForward + + self.backgroundIconView = UIImageView(image: UIImage(bundleImageName: isForward ? "Media Gallery/ForwardButton" : "Media Gallery/BackwardButton")) + + super.init(frame: CGRect()) + + self.addSubview(self.backgroundIconView) + + self.highligthedChanged = { [weak self] highlighted in + guard let self else { + return + } + if highlighted { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.18, curve: .linear) + let angle = CGFloat.pi / 4.0 + 0.226 + transition.updateTransformRotation(view: self.backgroundIconView, angle: self.isForward ? angle : -angle) + } else if !self.isPressing { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .linear) + transition.updateTransformRotation(view: self.backgroundIconView, angle: 0.0) + } + } + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(size: CGSize) { + if let image = self.backgroundIconView.image { + let factor: CGFloat = 1.4 + self.backgroundIconView.frame = CGSize(width: floor(image.size.width * factor), height: floor(image.size.height * factor)).centered(in: CGRect(origin: CGPoint(), size: size)) + } + + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "15", font: Font.with(size: 16.0, design: .round, weight: .semibold, traits: []), textColor: .white)) + )), + environment: {}, + containerSize: size + ) + if let textView = self.text.view { + if textView.superview == nil { + self.addSubview(textView) + } + textView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: floorToScreenPixels((size.height - textSize.height) / 2.0) + UIScreenPixel), size: textSize) + } + } +} diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.h b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.h index 925b95f9c6..782a77ad67 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.h +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.h @@ -26,8 +26,22 @@ typedef NS_OPTIONS(NSUInteger, UIResponderDisableAutomaticKeyboardHandling) { @end +@interface CALayerSpringParametersOverrideParameters : NSObject + +@property (nonatomic, readonly) CGFloat stiffness; +@property (nonatomic, readonly) CGFloat damping; +@property (nonatomic, readonly) double duration; + +- (instancetype _Nonnull)initWithStiffness:(CGFloat)stiffness damping:(CGFloat)damping duration:(double)duration; + +@end + @interface CALayerSpringParametersOverride : NSObject +@property (nonatomic, strong, readonly) CALayerSpringParametersOverrideParameters * _Nullable parameters; + +- (instancetype _Nonnull)initWithParameters:(CALayerSpringParametersOverrideParameters * _Nullable)parameters; + @end @interface CALayer (TelegramAddAnimation) diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m index 09b23696a4..b16e73e605 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m @@ -4,7 +4,7 @@ #import #import "NSWeakReference.h" - +#import @interface UIViewControllerPresentingProxy : UIViewController @@ -113,8 +113,30 @@ static bool notyfyingShiftState = false; @end +@implementation CALayerSpringParametersOverrideParameters + +- (instancetype _Nonnull)initWithStiffness:(CGFloat)stiffness damping:(CGFloat)damping duration:(double)duration; { + self = [super init]; + if (self != nil) { + _stiffness = stiffness; + _damping = damping; + _duration = duration; + } + return self; +} + +@end + @implementation CALayerSpringParametersOverride +- (instancetype _Nonnull)initWithParameters:(CALayerSpringParametersOverrideParameters * _Nullable)parameters { + self = [super init]; + if (self != nil) { + _parameters = parameters; + } + return self; +} + @end static NSMutableArray *currentSpringParametersOverrideStack() { @@ -145,30 +167,65 @@ static NSMutableArray *currentSpringParameter if (currentSpringParametersOverrideStack().count != 0 && [anim isKindOfClass:[CASpringAnimation class]]) { CALayerSpringParametersOverride *overrideData = [currentSpringParametersOverrideStack() lastObject]; if (overrideData) { - bool isNativeGlass = false; - if (@available(iOS 26.0, *)) { - isNativeGlass = true; - } - if (isNativeGlass && ABS(anim.duration - 0.3832) <= 0.0001) { - } else if (ABS(anim.duration - 0.5) <= 0.0001) { - } else { + if (overrideData.parameters) { CABasicAnimation *sourceAnimation = (CABasicAnimation *)anim; - CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:sourceAnimation.keyPath]; + CASpringAnimation *animation = makeSpringBounceAnimationImpl(sourceAnimation.keyPath, 0.0, overrideData.parameters.damping); + + animation.stiffness = overrideData.parameters.stiffness; animation.fromValue = sourceAnimation.fromValue; animation.toValue = sourceAnimation.toValue; animation.byValue = sourceAnimation.byValue; animation.additive = sourceAnimation.additive; - animation.duration = sourceAnimation.duration; - animation.timingFunction = [[CAMediaTimingFunction alloc] initWithControlPoints:0.380 :0.700 :0.125 :1.000]; animation.removedOnCompletion = sourceAnimation.isRemovedOnCompletion; animation.fillMode = sourceAnimation.fillMode; - animation.speed = sourceAnimation.speed; animation.beginTime = sourceAnimation.beginTime; animation.timeOffset = sourceAnimation.timeOffset; animation.repeatCount = sourceAnimation.repeatCount; animation.autoreverses = sourceAnimation.autoreverses; + + float k = animationDurationFactorImpl(); + __unused float speed = 1.0f; + if (k != 0.0 && k != 1.0) { + speed = 1.0f / k; + } + animation.speed = sourceAnimation.speed * (float)(animation.duration / overrideData.parameters.duration); + updatedAnimation = animation; + } else { + bool isNativeGlass = false; + if (@available(iOS 26.0, *)) { + isNativeGlass = true; + } + if (isNativeGlass && ABS(anim.duration - 0.3832) <= 0.0001) { + } else if (ABS(anim.duration - 0.5) <= 0.0001) { + } else { + CABasicAnimation *sourceAnimation = (CABasicAnimation *)anim; + + CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:sourceAnimation.keyPath]; + animation.fromValue = sourceAnimation.fromValue; + animation.toValue = sourceAnimation.toValue; + animation.byValue = sourceAnimation.byValue; + animation.additive = sourceAnimation.additive; + animation.duration = sourceAnimation.duration; + animation.timingFunction = [[CAMediaTimingFunction alloc] initWithControlPoints:0.380 :0.700 :0.125 :1.000]; + animation.removedOnCompletion = sourceAnimation.isRemovedOnCompletion; + animation.fillMode = sourceAnimation.fillMode; + animation.speed = sourceAnimation.speed; + animation.beginTime = sourceAnimation.beginTime; + animation.timeOffset = sourceAnimation.timeOffset; + animation.repeatCount = sourceAnimation.repeatCount; + animation.autoreverses = sourceAnimation.autoreverses; + + float k = animationDurationFactorImpl(); + float speed = 1.0f; + if (k != 0.0 && k != 1.0) { + speed = 1.0f / k; + } + animation.speed = speed * sourceAnimation.speed; + + updatedAnimation = animation; + } } } } diff --git a/submodules/WallpaperBackgroundNode/Sources/WallpaperEdgeEffectNodeImpl.swift b/submodules/WallpaperBackgroundNode/Sources/WallpaperEdgeEffectNodeImpl.swift index 56c9b39ad1..ff46717703 100644 --- a/submodules/WallpaperBackgroundNode/Sources/WallpaperEdgeEffectNodeImpl.swift +++ b/submodules/WallpaperBackgroundNode/Sources/WallpaperEdgeEffectNodeImpl.swift @@ -158,7 +158,7 @@ final class WallpaperEdgeEffectNodeImpl: ASDisplayNode, WallpaperEdgeEffectNode transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: containerSize)) if blur { - let blurHeight: CGFloat = max(edge.size, bounds.height - 20.0) + let blurHeight: CGFloat = max(edge.size, bounds.height - 24.0) let blurFrame = CGRect(origin: CGPoint(x: 0.0, y: edge.edge == .bottom ? (bounds.height - blurHeight) : 0.0), size: CGSize(width: bounds.width, height: blurHeight)) let blurView: VariableBlurView if let current = self.blurView { diff --git a/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift b/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift index dba4d51172..6ec7dc8eda 100644 --- a/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift +++ b/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift @@ -229,6 +229,8 @@ class WebSearchGalleryController: ViewController { }, editMedia: { _ in }, controller: { [weak self] in return self + }, currentItemNode: { [weak self] in + return self?.galleryNode.pager.centralItemNode() }) self.displayNode = GalleryControllerNode(context: self.context, controllerInteraction: controllerInteraction) self.displayNodeDidLoad()