diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index 5ab15007ee..1b7de2ec83 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -597,7 +597,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll } let scrubberFrame = CGRect(origin: CGPoint(x: leftInset, y: scrubberY), size: CGSize(width: width - leftInset - rightInset, height: 34.0)) - scrubberView.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset) + scrubberView.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate) transition.updateFrame(layer: scrubberView.layer, frame: scrubberFrame) } transition.updateAlpha(node: self.textNode, alpha: displayCaption ? 1.0 : 0.0) diff --git a/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift b/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift index 9efbcc2e9a..d890460689 100644 --- a/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift +++ b/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift @@ -21,6 +21,10 @@ final class ChatVideoGalleryItemScrubberView: UIView { private var playbackStatus: MediaPlayerStatus? private var fetchStatusDisposable = MetaDisposable() + private var scrubbingDisposable = MetaDisposable() + + private var leftTimestampNodePushed = false + private var rightTimestampNodePushed = false var hideWhenDurationIsUnknown = false { didSet { @@ -103,11 +107,16 @@ final class ChatVideoGalleryItemScrubberView: UIView { fatalError("init(coder:) has not been implemented") } + deinit { + self.scrubbingDisposable.dispose() + self.fetchStatusDisposable.dispose() + } + func setStatusSignal(_ status: Signal?) { let mappedStatus: Signal? if let status = status { mappedStatus = combineLatest(status, self.scrubberNode.scrubbingTimestamp) |> map { status, scrubbingTimestamp -> MediaPlayerStatus in - return MediaPlayerStatus(generationTimestamp: status.generationTimestamp, duration: status.duration, dimensions: status.dimensions, timestamp: scrubbingTimestamp ?? status.timestamp, baseRate: status.baseRate, seekId: status.seekId, status: status.status, soundEnabled: status.soundEnabled) + return MediaPlayerStatus(generationTimestamp: scrubbingTimestamp != nil ? 0 : status.generationTimestamp, duration: status.duration, dimensions: status.dimensions, timestamp: scrubbingTimestamp ?? status.timestamp, baseRate: status.baseRate, seekId: status.seekId, status: status.status, soundEnabled: status.soundEnabled) } } else { mappedStatus = nil @@ -115,6 +124,30 @@ final class ChatVideoGalleryItemScrubberView: UIView { self.scrubberNode.status = mappedStatus self.leftTimestampNode.status = mappedStatus self.rightTimestampNode.status = mappedStatus + + self.scrubbingDisposable.set((self.scrubberNode.scrubbingPosition + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let strongSelf = self else { + return + } + let leftTimestampNodePushed: Bool + let rightTimestampNodePushed: Bool + if let value = value { + leftTimestampNodePushed = value < 0.16 + rightTimestampNodePushed = value > 0.84 + } else { + leftTimestampNodePushed = false + rightTimestampNodePushed = false + } + if leftTimestampNodePushed != strongSelf.leftTimestampNodePushed || rightTimestampNodePushed != strongSelf.rightTimestampNodePushed { + strongSelf.leftTimestampNodePushed = leftTimestampNodePushed + strongSelf.rightTimestampNodePushed = rightTimestampNodePushed + + if let layout = strongSelf.containerLayout { + strongSelf.updateLayout(size: layout.0, leftInset: layout.1, rightInset: layout.2, transition: .animated(duration: 0.35, curve: .spring)) + } + } + })) } func setBufferingStatusSignal(_ status: Signal<(IndexSet, Int)?, NoError>?) { @@ -139,7 +172,7 @@ final class ChatVideoGalleryItemScrubberView: UIView { strongSelf.fileSizeNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: .white) if let (size, leftInset, rightInset) = strongSelf.containerLayout { - strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset) + strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate) } } })) @@ -151,22 +184,25 @@ final class ChatVideoGalleryItemScrubberView: UIView { } } - func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { + func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { self.containerLayout = (size, leftInset, rightInset) let scrubberHeight: CGFloat = 14.0 let scrubberInset: CGFloat - let timestampOffset: CGFloat + let leftTimestampOffset: CGFloat + let rightTimestampOffset: CGFloat if size.width > size.height { scrubberInset = 58.0 - timestampOffset = 4.0 + leftTimestampOffset = 4.0 + rightTimestampOffset = 4.0 } else { scrubberInset = 13.0 - timestampOffset = 22.0 + leftTimestampOffset = 22.0 + (self.leftTimestampNodePushed ? 8.0 : 0.0) + rightTimestampOffset = 22.0 + (self.rightTimestampNodePushed ? 8.0 : 0.0) } - self.leftTimestampNode.frame = CGRect(origin: CGPoint(x: 12.0, y: timestampOffset), size: CGSize(width: 60.0, height: 20.0)) - self.rightTimestampNode.frame = CGRect(origin: CGPoint(x: size.width - leftInset - rightInset - 60.0 - 12.0, y: timestampOffset), size: CGSize(width: 60.0, height: 20.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))) let fileSize = self.fileSizeNode.measure(size) self.fileSizeNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - fileSize.width) / 2.0), y: 22.0), size: fileSize) diff --git a/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift b/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift index 03b4838a63..29e1a9d966 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift @@ -23,6 +23,8 @@ private final class MediaPlayerScrubbingNodeButton: ASDisplayNode { var endScrubbing: ((Bool) -> Void)? var updateScrubbing: ((CGFloat) -> Void)? + var highlighted: ((Bool) -> Void)? + private var scrubbingStartLocation: CGPoint? override func didLoad() { @@ -32,6 +34,28 @@ private final class MediaPlayerScrubbingNodeButton: ASDisplayNode { self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) } + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + super.touchesBegan(touches, with: event) + + self.highlighted?(true) + } + + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + super.touchesEnded(touches, with: event) + + if self.scrubbingStartLocation == nil { + self.highlighted?(false) + } + } + + override func touchesCancelled(_ touches: Set?, with event: UIEvent?) { + super.touchesCancelled(touches, with: event) + + if self.scrubbingStartLocation == nil { + self.highlighted?(false) + } + } + @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { var location = recognizer.location(in: self.view) location.x -= self.bounds.minX @@ -50,6 +74,7 @@ private final class MediaPlayerScrubbingNodeButton: ASDisplayNode { let delta = location.x - scrubbingStartLocation.x self.updateScrubbing?(delta / self.bounds.size.width) self.endScrubbing?(recognizer.state == .ended) + self.highlighted?(false) } default: break @@ -94,9 +119,10 @@ private final class StandardMediaPlayerScrubbingNodeContentNode { let foregroundNode: MediaPlayerScrubbingForegroundNode let handle: MediaPlayerScrubbingNodeHandle let handleNode: ASDisplayNode? + let highlightedHandleNode: ASDisplayNode? let handleNodeContainer: MediaPlayerScrubbingNodeButton? - init(lineHeight: CGFloat, lineCap: MediaPlayerScrubbingNodeCap, backgroundNode: ASImageNode, bufferingNode: MediaPlayerScrubbingBufferingNode, foregroundContentNode: ASImageNode, foregroundNode: MediaPlayerScrubbingForegroundNode, handle: MediaPlayerScrubbingNodeHandle, handleNode: ASDisplayNode?, handleNodeContainer: MediaPlayerScrubbingNodeButton?) { + init(lineHeight: CGFloat, lineCap: MediaPlayerScrubbingNodeCap, backgroundNode: ASImageNode, bufferingNode: MediaPlayerScrubbingBufferingNode, foregroundContentNode: ASImageNode, foregroundNode: MediaPlayerScrubbingForegroundNode, handle: MediaPlayerScrubbingNodeHandle, handleNode: ASDisplayNode?, highlightedHandleNode: ASDisplayNode?, handleNodeContainer: MediaPlayerScrubbingNodeButton?) { self.lineHeight = lineHeight self.lineCap = lineCap self.backgroundNode = backgroundNode @@ -105,6 +131,7 @@ private final class StandardMediaPlayerScrubbingNodeContentNode { self.foregroundNode = foregroundNode self.handle = handle self.handleNode = handleNode + self.highlightedHandleNode = highlightedHandleNode self.handleNodeContainer = handleNodeContainer } } @@ -195,6 +222,11 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { return self._scrubbingTimestamp.get() } + private let _scrubbingPosition = Promise(nil) + public var scrubbingPosition: Signal { + return self._scrubbingPosition.get() + } + public var ignoreSeekId: Int? public var enableScrubbing: Bool = true { @@ -288,6 +320,7 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { foregroundNode.clipsToBounds = true var handleNodeImpl: ASImageNode? + var highlightedHandleNodeImpl: ASImageNode? var handleNodeContainerImpl: MediaPlayerScrubbingNodeButton? switch scrubberHandle { @@ -304,18 +337,27 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { handleNodeContainerImpl = handleNodeContainer case .circle: let handleNode = ASImageNode() - handleNode.image = generateFilledCircleImage(diameter: lineHeight + 4.0, color: foregroundColor) + handleNode.image = generateFilledCircleImage(diameter: lineHeight + 3.0, color: foregroundColor) handleNode.isLayerBacked = true handleNodeImpl = handleNode + let highlightedHandleNode = ASImageNode() + let highlightedHandleImage = generateFilledCircleImage(diameter: lineHeight + 3.0 + 20.0, color: foregroundColor)! + highlightedHandleNode.image = highlightedHandleImage + highlightedHandleNode.bounds = CGRect(origin: CGPoint(), size: highlightedHandleImage.size) + highlightedHandleNode.isLayerBacked = true + highlightedHandleNode.transform = CATransform3DMakeScale(0.1875, 0.1875, 1.0) + highlightedHandleNodeImpl = highlightedHandleNode + let handleNodeContainer = MediaPlayerScrubbingNodeButton() handleNodeContainer.addSubnode(handleNode) + handleNodeContainer.addSubnode(highlightedHandleNode) handleNodeContainerImpl = handleNodeContainer } handleNodeContainerImpl?.isUserInteractionEnabled = enableScrubbing - return .standard(StandardMediaPlayerScrubbingNodeContentNode(lineHeight: lineHeight, lineCap: lineCap, backgroundNode: backgroundNode, bufferingNode: bufferingNode, foregroundContentNode: foregroundContentNode, foregroundNode: foregroundNode, handle: scrubberHandle, handleNode: handleNodeImpl, handleNodeContainer: handleNodeContainerImpl)) + return .standard(StandardMediaPlayerScrubbingNodeContentNode(lineHeight: lineHeight, lineCap: lineCap, backgroundNode: backgroundNode, bufferingNode: bufferingNode, foregroundContentNode: foregroundContentNode, foregroundNode: foregroundNode, handle: scrubberHandle, handleNode: handleNodeImpl, highlightedHandleNode: highlightedHandleNodeImpl, handleNodeContainer: handleNodeContainerImpl)) case let .custom(backgroundNode, foregroundContentNode): let foregroundNode = MediaPlayerScrubbingForegroundNode() foregroundNode.isLayerBacked = true @@ -373,12 +415,39 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { if let handleNodeContainer = node.handleNodeContainer { self.addSubnode(handleNodeContainer) + handleNodeContainer.highlighted = { [weak self] highlighted in + if let strongSelf = self, let highlightedHandleNode = node.highlightedHandleNode, let statusValue = strongSelf.statusValue, Double(0.0).isLess(than: statusValue.duration) { + if highlighted { + strongSelf.displayLink?.isPaused = true + + var timestamp = statusValue.timestamp + if statusValue.generationTimestamp > 0 { + let currentTimestamp = CACurrentMediaTime() + timestamp = timestamp + (currentTimestamp - statusValue.generationTimestamp) * statusValue.baseRate + } + strongSelf.scrubbingTimestampValue = timestamp + strongSelf._scrubbingTimestamp.set(.single(strongSelf.scrubbingTimestampValue)) + strongSelf._scrubbingPosition.set(.single(strongSelf.scrubbingTimestampValue.flatMap { $0 / statusValue.duration })) + + highlightedHandleNode.layer.animateSpring(from: 0.1875 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.65, initialVelocity: 0.0, damping: 80.0, removeOnCompletion: false) + } else { + strongSelf.scrubbingTimestampValue = nil + strongSelf._scrubbingTimestamp.set(.single(nil)) + strongSelf._scrubbingPosition.set(.single(nil)) + strongSelf.updateProgressAnimations() + + highlightedHandleNode.layer.animateSpring(from: 1.0 as NSNumber, to: 0.1875 as NSNumber, keyPath: "transform.scale", duration: 0.65, initialVelocity: 0.0, damping: 120.0, removeOnCompletion: false) + + } + } + } handleNodeContainer.beginScrubbing = { [weak self] in if let strongSelf = self { if let statusValue = strongSelf.statusValue, Double(0.0).isLess(than: statusValue.duration) { strongSelf.scrubbingBeginTimestamp = statusValue.timestamp strongSelf.scrubbingTimestampValue = statusValue.timestamp strongSelf._scrubbingTimestamp.set(.single(strongSelf.scrubbingTimestampValue)) + strongSelf._scrubbingPosition.set(.single(strongSelf.scrubbingTimestampValue.flatMap { $0 / statusValue.duration })) strongSelf.update?(strongSelf.scrubbingTimestampValue, CGFloat(statusValue.timestamp / statusValue.duration)) strongSelf.updateProgressAnimations() } @@ -390,6 +459,7 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { let timestampValue = max(0.0, min(statusValue.duration, scrubbingBeginTimestamp + statusValue.duration * Double(addedFraction))) strongSelf.scrubbingTimestampValue = timestampValue strongSelf._scrubbingTimestamp.set(.single(strongSelf.scrubbingTimestampValue)) + strongSelf._scrubbingPosition.set(.single(strongSelf.scrubbingTimestampValue.flatMap { $0 / statusValue.duration })) strongSelf.update?(timestampValue, CGFloat(timestampValue / statusValue.duration)) strongSelf.updateProgressAnimations() } @@ -401,6 +471,7 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { let scrubbingTimestampValue = strongSelf.scrubbingTimestampValue strongSelf.scrubbingTimestampValue = nil strongSelf._scrubbingTimestamp.set(.single(nil)) + strongSelf._scrubbingPosition.set(.single(nil)) if let scrubbingTimestampValue = scrubbingTimestampValue, apply { if let statusValue = strongSelf.statusValue { switch statusValue.status { @@ -508,7 +579,14 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { node.foregroundContentNode.backgroundColor = foregroundColor } if let handleNode = node.handleNode as? ASImageNode { - handleNode.image = generateHandleBackground(color: foregroundColor) + switch node.handle { + case .line: + handleNode.image = generateHandleBackground(color: foregroundColor) + case .circle: + handleNode.image = generateFilledCircleImage(diameter: node.lineHeight + 3.0, color: foregroundColor) + case .none: + break + } } case .custom: break @@ -522,7 +600,7 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { if !self.isInHierarchyValue { needsAnimation = false - } else if let _ = scrubbingTimestampValue { + } else if let _ = self.scrubbingTimestampValue { needsAnimation = false } else if let statusValue = self.statusValue { if case .buffering(true, _) = statusValue.status { @@ -594,10 +672,16 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { if let handleNode = node.handleNode { var handleSize: CGSize = CGSize(width: 2.0, height: bounds.size.height) + var handleOffset: CGFloat = 0.0 if case .circle = node.handle, let handleNode = handleNode as? ASImageNode, let image = handleNode.image { handleSize = image.size + handleOffset = -1.0 + UIScreenPixel + } + handleNode.frame = CGRect(origin: CGPoint(x: -handleSize.width / 2.0, y: floor((bounds.size.height - handleSize.height) / 2.0) + handleOffset), size: handleSize) + + if let highlightedHandleNode = node.highlightedHandleNode { + highlightedHandleNode.position = handleNode.position } - handleNode.frame = CGRect(origin: CGPoint(x: -handleSize.width / 2.0, y: floor((bounds.size.height - handleSize.height) / 2.0)), size: handleSize) } if let handleNodeContainer = node.handleNodeContainer { @@ -605,7 +689,7 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { } if let (timestamp, duration) = timestampAndDuration { - if let scrubbingTimestampValue = scrubbingTimestampValue { + if let scrubbingTimestampValue = self.scrubbingTimestampValue { var progress = CGFloat(scrubbingTimestampValue / duration) if progress.isNaN || !progress.isFinite { progress = 0.0 diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index e0b0f89941..cf0c8a16d4 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -586,13 +586,13 @@ public final class ShareController: ViewController { url = "https://t.me/\(addressName)/\(message.id.id)" } } - var peer: Peer? + var peerId: PeerId? if let authorPeerId = message.author?.id { - peer = message.peers[authorPeerId] + peerId = authorPeerId } else if let mainPeer = messageMainPeer(message) { - peer = mainPeer + peerId = mainPeer.id } - collectableItems.append(CollectableExternalShareItem(url: url, text: message.text, author: peer?.displayTitle, timestamp: message.timestamp, mediaReference: selectedMedia.flatMap({ AnyMediaReference.message(message: MessageReference(message), media: $0) }))) + collectableItems.append(CollectableExternalShareItem(url: url, text: message.text, author: nil, timestamp: message.timestamp, mediaReference: selectedMedia.flatMap({ AnyMediaReference.message(message: MessageReference(message), media: $0) }))) } case .fromExternal: break diff --git a/submodules/TelegramCore/TelegramCore/ApplyUpdateMessage.swift b/submodules/TelegramCore/TelegramCore/ApplyUpdateMessage.swift index 2f691fe3e5..a3d862f92b 100644 --- a/submodules/TelegramCore/TelegramCore/ApplyUpdateMessage.swift +++ b/submodules/TelegramCore/TelegramCore/ApplyUpdateMessage.swift @@ -145,13 +145,21 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes } if forwardInfo == nil { - for media in media { + inner: for media in message.media { if let file = media as? TelegramMediaFile { - if file.isSticker { - sentStickers.append(file) - } else if file.isVideo && file.isAnimated { - sentGifs.append(file) + for attribute in file.attributes { + switch attribute { + case let .Sticker(_, packReference, _): + if packReference != nil { + sentStickers.append(file) + } + case .Animated: + sentGifs.append(file) + default: + break + } } + break inner } } } @@ -296,13 +304,21 @@ func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManage } if storeForwardInfo == nil { - for media in media { + inner: for media in message.media { if let file = media as? TelegramMediaFile { - if file.isSticker { - sentStickers.append(file) - } else if file.isVideo && file.isAnimated { - sentGifs.append(file) + for attribute in file.attributes { + switch attribute { + case let .Sticker(_, packReference, _): + if packReference != nil { + sentStickers.append(file) + } + case .Animated: + sentGifs.append(file) + default: + break + } } + break inner } } } diff --git a/submodules/TelegramUI/TelegramUI/ChatController.swift b/submodules/TelegramUI/TelegramUI/ChatController.swift index 64fefe25e5..b9f4a3e874 100644 --- a/submodules/TelegramUI/TelegramUI/ChatController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatController.swift @@ -5326,7 +5326,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G legacyController.bind(controller: controller) legacyController.deferScreenEdgeGestures = [.top] - configureLegacyAssetPicker(controller, context: strongSelf.context, peer: peer, initialCaption: inputText.string, hasSchedule: !strongSelf.presentationInterfaceState.isScheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, presentWebSearch: { [weak self, weak legacyController] in + configureLegacyAssetPicker(controller, context: strongSelf.context, peer: peer, initialCaption: inputText.string, hasSchedule: !strongSelf.presentationInterfaceState.isScheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, presentWebSearch: editingMedia ? nil : { [weak self, weak legacyController] in if let strongSelf = self { let controller = WebSearchController(context: strongSelf.context, peer: peer, configuration: searchBotsConfiguration, mode: .media(completion: { results, selectionState, editingState, silentPosting in if let legacyController = legacyController { diff --git a/submodules/TelegramUI/TelegramUI/ChatTextInputPanelNode.swift b/submodules/TelegramUI/TelegramUI/ChatTextInputPanelNode.swift index 78a5ec1fdf..571889e880 100644 --- a/submodules/TelegramUI/TelegramUI/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatTextInputPanelNode.swift @@ -1616,7 +1616,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let aspectRatio = min(image.size.width, image.size.height) / maxSide if isMemoji || (imageHasTransparency(cgImage) && aspectRatio > 0.85) { self.paste(.sticker(image, isMemoji)) - return false + return true } } diff --git a/submodules/TelegramUI/TelegramUI/OverlayPlayerControlsNode.swift b/submodules/TelegramUI/TelegramUI/OverlayPlayerControlsNode.swift index f688efe8ea..5d15e61911 100644 --- a/submodules/TelegramUI/TelegramUI/OverlayPlayerControlsNode.swift +++ b/submodules/TelegramUI/TelegramUI/OverlayPlayerControlsNode.swift @@ -31,8 +31,8 @@ private func generateShareIcon(theme: PresentationTheme) -> UIImage? { }) } -private let titleFont = Font.medium(16.0) -private let descriptionFont = Font.regular(12.0) +private let titleFont = Font.semibold(17.0) +private let descriptionFont = Font.regular(17.0) private func stringsForDisplayData(_ data: SharedMediaPlaybackDisplayData?, theme: PresentationTheme) -> (NSAttributedString?, NSAttributedString?) { var titleString: NSAttributedString? @@ -109,6 +109,10 @@ final class OverlayPlayerControlsNode: ASDisplayNode { private var currentFileReference: FileMediaReference? private var statusDisposable: Disposable? + private var scrubbingDisposable: Disposable? + private var leftDurationLabelPushed = false + private var rightDurationLabelPushed = false + private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat)? init(account: Account, accountManager: AccountManager, theme: PresentationTheme, status: Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading)?, NoError>) { @@ -223,96 +227,121 @@ final class OverlayPlayerControlsNode: ASDisplayNode { self.leftDurationLabel.status = mappedStatus self.rightDurationLabel.status = mappedStatus + self.scrubbingDisposable = (self.scrubberNode.scrubbingPosition + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let strongSelf = self else { + return + } + let leftDurationLabelPushed: Bool + let rightDurationLabelPushed: Bool + if let value = value { + leftDurationLabelPushed = value < 0.16 + rightDurationLabelPushed = value > (strongSelf.rateButton.isHidden ? 0.84 : 0.74) + } else { + leftDurationLabelPushed = false + rightDurationLabelPushed = false + } + if leftDurationLabelPushed != strongSelf.leftDurationLabelPushed || rightDurationLabelPushed != strongSelf.rightDurationLabelPushed { + strongSelf.leftDurationLabelPushed = leftDurationLabelPushed + strongSelf.rightDurationLabelPushed = rightDurationLabelPushed + + if let layout = strongSelf.validLayout { + strongSelf.updateLayout(width: layout.0, leftInset: layout.1, rightInset: layout.2, maxHeight: layout.3, transition: .animated(duration: 0.35, curve: .spring)) + } + } + }) + self.statusDisposable = (delayedStatus |> deliverOnMainQueue).start(next: { [weak self] value in - if let strongSelf = self { - var valueItemId: SharedMediaPlaylistItemId? - if let (_, value) = value, case let .state(state) = value { - valueItemId = state.item.id + guard let strongSelf = self else { + return + } + var valueItemId: SharedMediaPlaylistItemId? + if let (_, value) = value, case let .state(state) = value { + valueItemId = state.item.id + } + if !areSharedMediaPlaylistItemIdsEqual(valueItemId, strongSelf.currentItemId) { + strongSelf.currentItemId = valueItemId + strongSelf.scrubberNode.ignoreSeekId = nil + } + strongSelf.shareNode.isHidden = false + var displayData: SharedMediaPlaybackDisplayData? + if let (_, valueOrLoading) = value, case let .state(value) = valueOrLoading { + let isPaused: Bool + switch value.status.status { + case .playing: + isPaused = false + case .paused: + isPaused = true + case let .buffering(_, whilePlaying): + isPaused = !whilePlaying } - if !areSharedMediaPlaylistItemIdsEqual(valueItemId, strongSelf.currentItemId) { - strongSelf.currentItemId = valueItemId - strongSelf.scrubberNode.ignoreSeekId = nil - } - strongSelf.shareNode.isHidden = false - var displayData: SharedMediaPlaybackDisplayData? - if let (_, valueOrLoading) = value, case let .state(value) = valueOrLoading { - let isPaused: Bool - switch value.status.status { - case .playing: - isPaused = false - case .paused: - isPaused = true - case let .buffering(_, whilePlaying): - isPaused = !whilePlaying - } - if strongSelf.currentIsPaused != isPaused { - strongSelf.currentIsPaused = isPaused - - strongSelf.updatePlayPauseButton(paused: isPaused) - } + if strongSelf.currentIsPaused != isPaused { + strongSelf.currentIsPaused = isPaused - strongSelf.playPauseButton.isEnabled = true - strongSelf.backwardButton.isEnabled = true - strongSelf.forwardButton.isEnabled = true - - displayData = value.item.displayData - - if value.order != strongSelf.currentOrder { - strongSelf.updateOrder?(value.order) - strongSelf.currentOrder = value.order - strongSelf.updateOrderButton(value.order) - } - if value.looping != strongSelf.currentLooping { - strongSelf.currentLooping = value.looping - strongSelf.updateLoopButton(value.looping) - } - - let baseRate: AudioPlaybackRate - if !value.status.baseRate.isEqual(to: 1.0) { - baseRate = .x2 - } else { - baseRate = .x1 - } - if baseRate != strongSelf.currentRate { - strongSelf.currentRate = baseRate - strongSelf.updateRateButton(baseRate) - } - - if let displayData = displayData, case let .music(_, _, _, long) = displayData, long { - strongSelf.rateButton.isHidden = false - } else { - strongSelf.rateButton.isHidden = true - } - } else { - strongSelf.playPauseButton.isEnabled = false - strongSelf.backwardButton.isEnabled = false - strongSelf.forwardButton.isEnabled = false - strongSelf.rateButton.isHidden = true - displayData = nil + strongSelf.updatePlayPauseButton(paused: isPaused) } - if strongSelf.displayData != displayData { - strongSelf.displayData = displayData - - if let (_, valueOrLoading) = value, case let .state(value) = valueOrLoading, let source = value.item.playbackData?.source { - switch source { - case let .telegramFile(fileReference): - strongSelf.currentFileReference = fileReference - if let size = fileReference.media.size { - strongSelf.scrubberNode.bufferingStatus = strongSelf.postbox.mediaBox.resourceRangesStatus(fileReference.media.resource) - |> map { ranges -> (IndexSet, Int) in - return (ranges, size) - } - } else { - strongSelf.scrubberNode.bufferingStatus = nil - } - } - } else { - strongSelf.scrubberNode.bufferingStatus = nil - } - strongSelf.updateLabels(transition: .immediate) + strongSelf.playPauseButton.isEnabled = true + strongSelf.backwardButton.isEnabled = true + strongSelf.forwardButton.isEnabled = true + + displayData = value.item.displayData + + if value.order != strongSelf.currentOrder { + strongSelf.updateOrder?(value.order) + strongSelf.currentOrder = value.order + strongSelf.updateOrderButton(value.order) } + if value.looping != strongSelf.currentLooping { + strongSelf.currentLooping = value.looping + strongSelf.updateLoopButton(value.looping) + } + + let baseRate: AudioPlaybackRate + if !value.status.baseRate.isEqual(to: 1.0) { + baseRate = .x2 + } else { + baseRate = .x1 + } + if baseRate != strongSelf.currentRate { + strongSelf.currentRate = baseRate + strongSelf.updateRateButton(baseRate) + } + + if let displayData = displayData, case let .music(_, _, _, long) = displayData, long { + strongSelf.rateButton.isHidden = false + } else { + strongSelf.rateButton.isHidden = true + } + } else { + strongSelf.playPauseButton.isEnabled = false + strongSelf.backwardButton.isEnabled = false + strongSelf.forwardButton.isEnabled = false + strongSelf.rateButton.isHidden = true + displayData = nil + } + + if strongSelf.displayData != displayData { + strongSelf.displayData = displayData + + if let (_, valueOrLoading) = value, case let .state(value) = valueOrLoading, let source = value.item.playbackData?.source { + switch source { + case let .telegramFile(fileReference): + strongSelf.currentFileReference = fileReference + if let size = fileReference.media.size { + strongSelf.scrubberNode.bufferingStatus = strongSelf.postbox.mediaBox.resourceRangesStatus(fileReference.media.resource) + |> map { ranges -> (IndexSet, Int) in + return (ranges, size) + } + } else { + strongSelf.scrubberNode.bufferingStatus = nil + } + } + } else { + strongSelf.scrubberNode.bufferingStatus = nil + } + strongSelf.updateLabels(transition: .immediate) } }) @@ -332,6 +361,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { deinit { self.statusDisposable?.dispose() + self.scrubbingDisposable?.dispose() } override func didLoad() { @@ -378,7 +408,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { let sideInset: CGFloat = 20.0 - let infoLabelsLeftInset: CGFloat = 64.0 + let infoLabelsLeftInset: CGFloat = 60.0 let infoLabelsRightInset: CGFloat = 32.0 let infoVerticalOrigin: CGFloat = panelHeight - OverlayPlayerControlsNode.basePanelHeight + 36.0 @@ -392,7 +422,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: self.isExpanded ? floor((width - titleLayout.size.width) / 2.0) : (leftInset + sideInset + infoLabelsLeftInset), y: infoVerticalOrigin + 1.0), size: titleLayout.size)) let _ = titleApply() - transition.updateFrame(node: self.descriptionNode, frame: CGRect(origin: CGPoint(x: self.isExpanded ? floor((width - descriptionLayout.size.width) / 2.0) : (leftInset + sideInset + infoLabelsLeftInset), y: infoVerticalOrigin + 27.0), size: descriptionLayout.size)) + transition.updateFrame(node: self.descriptionNode, frame: CGRect(origin: CGPoint(x: self.isExpanded ? floor((width - descriptionLayout.size.width) / 2.0) : (leftInset + sideInset + infoLabelsLeftInset), y: infoVerticalOrigin + 26.0), size: descriptionLayout.size)) let _ = descriptionApply() var albumArt: SharedMediaPlaybackAlbumArt? @@ -469,7 +499,6 @@ final class OverlayPlayerControlsNode: ASDisplayNode { func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { self.validLayout = (width, leftInset, rightInset, maxHeight) - let panelHeight = OverlayPlayerControlsNode.heightForLayout(width: width, leftInset: leftInset, rightInset: rightInset, maxHeight: maxHeight, isExpanded: self.isExpanded) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelHeight), size: CGSize(width: width, height: UIScreenPixel))) @@ -562,10 +591,14 @@ final class OverlayPlayerControlsNode: ASDisplayNode { let scrubberVerticalOrigin: CGFloat = infoVerticalOrigin + 64.0 transition.updateFrame(node: self.scrubberNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: scrubberVerticalOrigin - 8.0), size: CGSize(width: width - sideInset * 2.0 - leftInset - rightInset, height: 10.0 + 8.0 * 2.0))) - transition.updateFrame(node: self.leftDurationLabel, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: scrubberVerticalOrigin + 14.0), size: CGSize(width: 100.0, height: 20.0))) - transition.updateFrame(node: self.rightDurationLabel, frame: CGRect(origin: CGPoint(x: width - sideInset - rightInset - 100.0, y: scrubberVerticalOrigin + 14.0), size: CGSize(width: 100.0, height: 20.0))) - transition.updateFrame(node: self.rateButton, frame: CGRect(origin: CGPoint(x: width - sideInset - rightInset - 100.0 + 24.0, y: scrubberVerticalOrigin + 10.0), size: CGSize(width: 24.0, height: 24.0))) + var leftLabelVerticalOffset: CGFloat = self.leftDurationLabelPushed ? 6.0 : 0.0 + transition.updateFrame(node: self.leftDurationLabel, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: scrubberVerticalOrigin + 14.0 + leftLabelVerticalOffset), size: CGSize(width: 100.0, height: 20.0))) + + var rightLabelVerticalOffset: CGFloat = self.rightDurationLabelPushed ? 6.0 : 0.0 + transition.updateFrame(node: self.rightDurationLabel, frame: CGRect(origin: CGPoint(x: width - sideInset - rightInset - 100.0, y: scrubberVerticalOrigin + 14.0 + rightLabelVerticalOffset), size: CGSize(width: 100.0, height: 20.0))) + + transition.updateFrame(node: self.rateButton, frame: CGRect(origin: CGPoint(x: width - sideInset - rightInset - 100.0 + 24.0, y: scrubberVerticalOrigin + 10.0 + rightLabelVerticalOffset), size: CGSize(width: 24.0, height: 24.0))) transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -8.0), size: CGSize(width: width, height: panelHeight + 8.0))) diff --git a/submodules/WalletUI/Sources/WalletQrScanScreen.swift b/submodules/WalletUI/Sources/WalletQrScanScreen.swift index 215408d765..5172490e6a 100644 --- a/submodules/WalletUI/Sources/WalletQrScanScreen.swift +++ b/submodules/WalletUI/Sources/WalletQrScanScreen.swift @@ -293,9 +293,21 @@ private final class WalletQrScanScreenNode: ViewControllerTracingNode, UIScrollV let sideInset: CGFloat = 66.0 let titleSpacing: CGFloat = 48.0 - let bounds = CGRect(origin: CGPoint(), size: layout.size) + if case .tablet = layout.deviceMetrics.type { + if case .landscape = layout.orientation { + let rotation: CGFloat + if UIDevice.current.orientation == .landscapeLeft { + rotation = CGFloat.pi / 2.0 + } else { + rotation = -CGFloat.pi / 2.0 + } + self.previewNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + } else { + self.previewNode.transform = CATransform3DIdentity + } + } transition.updateFrame(node: self.previewNode, frame: bounds) transition.updateFrame(node: self.fadeNode, frame: bounds) @@ -307,27 +319,26 @@ private final class WalletQrScanScreenNode: ViewControllerTracingNode, UIScrollV let dimRect: CGRect let controlsAlpha: CGFloat if let focusedRect = self.focusedRect { - dimAlpha = 1.0 controlsAlpha = 0.0 + dimAlpha = 1.0 let side = max(bounds.width * focusedRect.width, bounds.height * focusedRect.height) * 0.6 let center = CGPoint(x: (1.0 - focusedRect.center.y) * bounds.width, y: focusedRect.center.x * bounds.height) dimRect = CGRect(x: center.x - side / 2.0, y: center.y - side / 2.0, width: side, height: side) } else { - dimAlpha = 0.625 controlsAlpha = 1.0 + dimAlpha = 0.625 dimRect = CGRect(x: dimInset, y: dimHeight, width: layout.size.width - dimInset * 2.0, height: layout.size.height - dimHeight * 2.0) } - transition.updateFrame(node: self.topDimNode, frame: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: dimRect.minY)) - transition.updateFrame(node: self.bottomDimNode, frame: CGRect(x: 0.0, y: dimRect.maxY, width: layout.size.width, height: max(0.0, layout.size.height - dimRect.maxY))) - transition.updateFrame(node: self.leftDimNode, frame: CGRect(x: 0.0, y: dimRect.minY, width: max(0.0, dimRect.minX), height: dimRect.height)) - transition.updateFrame(node: self.rightDimNode, frame: CGRect(x: dimRect.maxX, y: dimRect.minY, width: max(0.0, layout.size.width - dimRect.maxX), height: dimRect.height)) - transition.updateAlpha(node: self.topDimNode, alpha: dimAlpha) transition.updateAlpha(node: self.bottomDimNode, alpha: dimAlpha) transition.updateAlpha(node: self.leftDimNode, alpha: dimAlpha) transition.updateAlpha(node: self.rightDimNode, alpha: dimAlpha) + transition.updateFrame(node: self.topDimNode, frame: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: dimRect.minY)) + transition.updateFrame(node: self.bottomDimNode, frame: CGRect(x: 0.0, y: dimRect.maxY, width: layout.size.width, height: max(0.0, layout.size.height - dimRect.maxY))) + transition.updateFrame(node: self.leftDimNode, frame: CGRect(x: 0.0, y: dimRect.minY, width: max(0.0, dimRect.minX), height: dimRect.height)) + transition.updateFrame(node: self.rightDimNode, frame: CGRect(x: dimRect.maxX, y: dimRect.minY, width: max(0.0, layout.size.width - dimRect.maxX), height: dimRect.height)) transition.updateFrame(node: self.frameNode, frame: dimRect.insetBy(dx: -2.0, dy: -2.0)) let buttonSize = CGSize(width: 72.0, height: 72.0) diff --git a/submodules/WalletUI/Sources/WalletQrViewScreen.swift b/submodules/WalletUI/Sources/WalletQrViewScreen.swift index 87a5c8b34d..c2dba3e0eb 100644 --- a/submodules/WalletUI/Sources/WalletQrViewScreen.swift +++ b/submodules/WalletUI/Sources/WalletQrViewScreen.swift @@ -155,8 +155,10 @@ private final class WalletQrViewScreenNode: ViewControllerTracingNode { let imageFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: floor((layout.size.height - imageSize.height - layout.intrinsicInsets.bottom) / 2.0)), size: imageSize) transition.updateFrame(node: self.imageNode, frame: imageFrame) - let iconFrame = imageFrame.insetBy(dx: 106.0, dy: 106.0).offsetBy(dx: 0.0, dy: -2.0) - self.iconNode.updateLayout(size: iconFrame.size) - transition.updateFrameAsPositionAndBounds(node: self.iconNode, frame: iconFrame) + let iconSide = floor(imageSide * 0.24) + let iconSize = CGSize(width: iconSide, height: iconSide) + self.iconNode.updateLayout(size: iconSize) + transition.updateBounds(node: self.iconNode, bounds: CGRect(origin: CGPoint(), size: iconSize)) + transition.updatePosition(node: self.iconNode, position: imageFrame.center.offsetBy(dx: 0.0, dy: -1.0)) } }