Album art view

This commit is contained in:
Ilya Laktyushin
2026-03-31 13:51:10 +02:00
parent c1d8b98fda
commit 36440d6566
4 changed files with 131 additions and 114 deletions
@@ -441,7 +441,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
if case let .assets(_, mode) = controller.subject {
switch mode {
case .default, .story, .poll:
case .default, .poll:
self.containerNode.view.addSubview(self.bottomEdgeEffectView)
default:
break
@@ -3162,7 +3162,6 @@ private func albumArtFullSizeDatas(engine: TelegramEngine, file: FileMediaRefere
return .single(Tuple(nil, nil, false))
}
}
}
}
|> distinctUntilChanged(isEqual: { lhs, rhs in
@@ -3229,25 +3228,32 @@ public func playerAlbumArt(postbox: Postbox, engine: TelegramEngine, fileReferen
}
)
}
var immediateArtworkData: Signal<Tuple3<Data?, Data?, Bool>, NoError> = .single(Tuple(nil, nil, false))
if let fileReference = fileReference, let smallestRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) {
func previewArtworkData(fileReference: FileMediaReference) -> Signal<Data?, NoError> {
guard let smallestRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) else {
return .single(nil)
}
let thumbnailResource = smallestRepresentation.resource
let fetchedThumbnail = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: .other, userContentType: .image, reference: fileReference.resourceReference(thumbnailResource))
let thumbnail = Signal<Data?, NoError> { subscriber in
return Signal<Data?, NoError> { subscriber in
let fetchedDisposable = fetchedThumbnail.start()
let thumbnailDisposable = postbox.mediaBox.resourceData(thumbnailResource, attemptSynchronously: attemptSynchronously).start(next: { next in
subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []))
}, error: subscriber.putError, completed: subscriber.putCompletion)
return ActionDisposable {
fetchedDisposable.dispose()
thumbnailDisposable.dispose()
}
}
}
var immediateArtworkData: Signal<Tuple3<Data?, Data?, Bool>, NoError> = .single(Tuple(nil, nil, false))
if let fileReference = fileReference, thumbnail, smallestImageRepresentation(fileReference.media.previewRepresentations) != nil {
let thumbnail = previewArtworkData(fileReference: fileReference)
immediateArtworkData = thumbnail
|> map { thumbnailData in
return Tuple(thumbnailData, nil, false)
@@ -3259,7 +3265,36 @@ public func playerAlbumArt(postbox: Postbox, engine: TelegramEngine, fileReferen
return Tuple(thumbnailData, nil, false)
}
} else {
immediateArtworkData = albumArtFullSizeDatas(engine: engine, file: fileReference, thumbnail: albumArt.thumbnailResource, fullSize: albumArt.fullSizeResource)
let previewData: Signal<Data?, NoError>
if let fileReference = fileReference {
previewData = previewArtworkData(fileReference: fileReference)
} else {
previewData = .single(nil)
}
immediateArtworkData = combineLatest(
albumArtFullSizeDatas(engine: engine, file: fileReference, thumbnail: albumArt.thumbnailResource, fullSize: albumArt.fullSizeResource),
previewData
)
|> map { remoteArtworkData, previewData in
let remoteFullSizeData = remoteArtworkData._1
let shouldUsePreviewFallback: Bool
if remoteArtworkData._2 {
if let remoteFullSizeData = remoteFullSizeData {
shouldUsePreviewFallback = remoteFullSizeData.isEmpty
} else {
shouldUsePreviewFallback = true
}
} else {
shouldUsePreviewFallback = false
}
if shouldUsePreviewFallback {
return Tuple(previewData, nil, false)
} else {
return remoteArtworkData
}
}
}
} else {
immediateArtworkData = .single(Tuple(nil, nil, false))
@@ -17,6 +17,7 @@ import UndoUI
import ChatHistoryEntry
import MultilineTextComponent
import GlassControls
import PhotoResources
final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestureRecognizerDelegate {
let ready = Promise<Bool>()
@@ -61,6 +62,10 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
private var replacementHistoryNode: ChatHistoryListNodeImpl?
private var replacementHistoryNodeFloatingOffset: CGFloat?
private var currentAlbumArt: (FileMediaReference, SharedMediaPlaybackAlbumArt)?
private let albumArtBackground: UIVisualEffectView
private let albumArtNode = TransformImageNode()
private var saveMediaDisposable: MetaDisposable?
private var validLayout: ContainerViewLayout?
@@ -335,6 +340,10 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
self.collapseNode.displaysAsynchronously = false
self.collapseNode.setImage(generateCollapseIcon(theme: self.presentationData.theme), for: [])
self.albumArtBackground = UIVisualEffectView()
self.albumArtBackground.contentView.addSubview(self.albumArtNode.view)
self.albumArtNode.isUserInteractionEnabled = false
super.init()
self.backgroundColor = nil
@@ -376,6 +385,19 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
}
}
self.controlsNode.requestAlbumArtDisplay = { [weak self] fileReferenceAndAlbumArt in
guard let self, let layout = self.validLayout else {
return
}
self.currentAlbumArt = fileReferenceAndAlbumArt
if let (fileReference, albumArt) = fileReferenceAndAlbumArt {
self.albumArtNode.setSignal(playerAlbumArt(postbox: self.context.account.postbox, engine: self.context.engine, fileReference: fileReference, albumArt: albumArt, thumbnail: false))
}
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.25, curve: .easeInOut))
}
self.controlsNode.requestCollapse = { [weak self] in
self?.requestDismiss()
}
@@ -542,6 +564,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
)
self.setupReordering()
self.albumArtBackground.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.albumArtTapped(_:))))
}
deinit {
@@ -841,7 +865,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
let controlsHeight = self.controlsNode.updateLayout(width: layout.size.width, leftInset: 0.0, rightInset: 0.0, bottomInset: layout.intrinsicInsets.bottom, maxHeight: layout.size.height, savedMusic: self.isSaved, transition: transition)
let controlsFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - controlsHeight), size: CGSize(width: layout.size.width, height: controlsHeight))
transition.updateFrame(node: self.controlsNode, frame: controlsFrame)
let controlsTransition = self.controlsNode.frame.width > 0.0 ? transition : .immediate
controlsTransition.updateFrame(node: self.controlsNode, frame: controlsFrame)
let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top)
var insets = UIEdgeInsets()
@@ -962,6 +987,49 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
self.updateHistoryContentOffset(self.historyNode.visibleContentOffset(), transition: transition)
self.albumArtBackground.frame = CGRect(origin: .zero, size: layout.size)
if let _ = self.currentAlbumArt {
var animateIn = false
if self.albumArtBackground.superview == nil {
self.view.addSubview(self.albumArtBackground)
animateIn = true
}
let albumArtSide = min(360.0, layout.size.width - 32.0)
let albumArtSize = CGSize(width: albumArtSide, height: albumArtSide)
self.albumArtNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - albumArtSize.width) / 2.0), y: floorToScreenPixels((layout.size.height - albumArtSize.height) / 2.0)), size: albumArtSize)
let makeLargeAlbumArtLayout = self.albumArtNode.asyncLayout()
let applyLargeAlbumArt = makeLargeAlbumArtLayout(TransformImageArguments(corners: ImageCorners(radius: 4.0), imageSize: albumArtSize, boundingSize: albumArtSize, intrinsicInsets: UIEdgeInsets()))
applyLargeAlbumArt()
if animateIn {
self.controlsNode.albumArtNode.alpha = 0.0
let sourceFrame = self.controlsNode.albumArtNode.view.convert(self.controlsNode.albumArtNode.bounds, to: self.albumArtBackground.contentView)
ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring).animateFrame(node: self.albumArtNode, from: sourceFrame)
UIView.animate(withDuration: 0.2, animations: {
self.albumArtBackground.effect = UIBlurEffect(style: self.presentationData.theme.overallDarkAppearance ? .dark : .light)
})
}
} else {
self.controlsNode.albumArtNode.alpha = 1.0
if self.albumArtBackground.superview != nil {
let fadeTransition = ComponentTransition(transition)
fadeTransition.setBlur(layer: self.albumArtNode.layer, radius: 10.0)
fadeTransition.setAlpha(layer: self.albumArtNode.layer, alpha: 0.0)
UIView.animate(withDuration: 0.2, animations: {
self.albumArtBackground.effect = nil
}, completion: { _ in
self.albumArtBackground.removeFromSuperview()
ComponentTransition.immediate.setBlur(layer: self.albumArtNode.layer, radius: 0.0)
ComponentTransition.immediate.setAlpha(layer: self.albumArtNode.layer, alpha: 1.0)
})
}
}
var layout = layout
layout.intrinsicInsets.bottom = controlsHeight + (self.historyNode.hasAnyMessages ? 0.0 : 8.0)
self.getParentController()?.presentationContext.containerLayoutUpdated(layout, transition: transition)
@@ -999,6 +1067,13 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
self.requestDismiss()
}
}
@objc func albumArtTapped(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state, let layout = self.validLayout {
self.currentAlbumArt = nil
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.3, curve: .easeInOut))
}
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let recognizer = gestureRecognizer as? UIPanGestureRecognizer {
@@ -1566,7 +1641,7 @@ private func generateCollapseIcon(theme: PresentationTheme) -> UIImage? {
private func generateCornersImage(theme: PresentationTheme) -> UIImage? {
return generateImage(CGSize(width: 56.0, height: 56.0), rotatedContext: { (size, context) in
let bounds = CGRect(origin: CGPoint(), size: size)
context.setFillColor(theme.list.blocksBackgroundColor.cgColor)
context.setFillColor(theme.list.modalBlocksBackgroundColor.cgColor)
context.fill(bounds)
context.setBlendMode(.clear)
@@ -148,8 +148,7 @@ final class OverlayAudioPlayerControlsNode: ASDisplayNode {
private let backgroundNode: ASImageNode
private let albumArtNode: TransformImageNode
private var largeAlbumArtNode: TransformImageNode?
let albumArtNode: TransformImageNode
private let titleNode: TextNode
private let title: ComponentView<Empty>
private let descriptionNode: TextNode
@@ -193,6 +192,7 @@ final class OverlayAudioPlayerControlsNode: ASDisplayNode {
var isExpanded = false
var updateIsExpanded: (() -> Void)?
var requestAlbumArtDisplay: (((FileMediaReference, SharedMediaPlaybackAlbumArt)?) -> Void)?
var requestCollapse: (() -> Void)?
var requestShare: ((ShareControllerSubject) -> Void)?
@@ -222,7 +222,6 @@ final class OverlayAudioPlayerControlsNode: ASDisplayNode {
private let hapticFeedback = HapticFeedback()
private var scrubbingDisposable: Disposable?
private var leftDurationLabelPushed = false
private var rightDurationLabelPushed = false
private var infoNodePushed = false
@@ -356,33 +355,7 @@ final class OverlayAudioPlayerControlsNode: ASDisplayNode {
self.scrubberNode.status = mappedStatus
self.leftDurationLabel.status = mappedStatus
self.rightDurationLabel.status = mappedStatus
// self.scrubbingDisposable = (self.scrubberNode.scrubbingPosition
// |> deliverOnMainQueue).startStrict(next: { [weak self] value in
// guard let strongSelf = self else {
// return
// }
// let leftDurationLabelPushed: Bool
// let rightDurationLabelPushed: Bool
// let infoNodePushed: Bool
// if let value = value {
// leftDurationLabelPushed = value < 0.16
// rightDurationLabelPushed = value > (strongSelf.rateButton.isHidden ? 0.84 : 0.74)
// infoNodePushed = value >= 0.16 && value <= 0.84
// } else {
// leftDurationLabelPushed = false
// rightDurationLabelPushed = false
// infoNodePushed = false
// }
// if leftDurationLabelPushed != strongSelf.leftDurationLabelPushed || rightDurationLabelPushed != strongSelf.rightDurationLabelPushed || infoNodePushed != strongSelf.infoNodePushed {
// strongSelf.leftDurationLabelPushed = leftDurationLabelPushed
// strongSelf.rightDurationLabelPushed = rightDurationLabelPushed
// strongSelf.infoNodePushed = infoNodePushed
//
// strongSelf.requestLayout(transition: .animated(duration: 0.35, curve: .spring))
// }
// })
self.statusDisposable = combineLatest(
queue: Queue.mainQueue(),
delayedStatus,
@@ -592,7 +565,6 @@ final class OverlayAudioPlayerControlsNode: ASDisplayNode {
deinit {
self.statusDisposable?.dispose()
self.chapterDisposable?.dispose()
self.scrubbingDisposable?.dispose()
}
override func didLoad() {
@@ -788,9 +760,7 @@ final class OverlayAudioPlayerControlsNode: ASDisplayNode {
self.currentAlbumArtInitialized = true
self.currentAlbumArt = albumArt
self.albumArtNode.setSignal(playerAlbumArt(postbox: self.account.postbox, engine: self.engine, fileReference: self.currentFileReference, albumArt: albumArt, thumbnail: true))
if let largeAlbumArtNode = self.largeAlbumArtNode {
largeAlbumArtNode.setSignal(playerAlbumArt(postbox: self.account.postbox, engine: self.engine, fileReference: self.currentFileReference, albumArt: albumArt, thumbnail: false))
}
self.requestAlbumArtDisplay?(nil)
}
}
@@ -880,72 +850,8 @@ final class OverlayAudioPlayerControlsNode: ASDisplayNode {
let applyAlbumArt = makeAlbumArtLayout(TransformImageArguments(corners: ImageCorners(radius: 10.0), imageSize: albumArtSize, boundingSize: albumArtSize, intrinsicInsets: UIEdgeInsets()))
applyAlbumArt()
let albumArtFrame = CGRect(origin: CGPoint(x: leftInset + sideInset, y: infoVerticalOrigin - 3.0), size: albumArtSize)
let previousAlbumArtNodeFrame = self.albumArtNode.frame
transition.updateFrame(node: self.albumArtNode, frame: albumArtFrame)
if self.isExpanded {
let largeAlbumArtNode: TransformImageNode
var animateIn = false
if let current = self.largeAlbumArtNode {
largeAlbumArtNode = current
} else {
animateIn = true
largeAlbumArtNode = TransformImageNode()
self.largeAlbumArtNode = largeAlbumArtNode
self.addSubnode(largeAlbumArtNode)
if self.currentAlbumArtInitialized {
largeAlbumArtNode.setSignal(playerAlbumArt(postbox: self.account.postbox, engine: self.engine, fileReference: self.currentFileReference, albumArt: self.currentAlbumArt, thumbnail: false))
}
}
let albumArtHeight = max(1.0, panelHeight - OverlayAudioPlayerControlsNode.basePanelHeight - 24.0)
let largeAlbumArtSize = CGSize(width: albumArtHeight, height: albumArtHeight)
let makeLargeAlbumArtLayout = largeAlbumArtNode.asyncLayout()
let applyLargeAlbumArt = makeLargeAlbumArtLayout(TransformImageArguments(corners: ImageCorners(radius: 4.0), imageSize: largeAlbumArtSize, boundingSize: largeAlbumArtSize, intrinsicInsets: UIEdgeInsets()))
applyLargeAlbumArt()
let largeAlbumArtFrame = CGRect(origin: CGPoint(x: floor((width - largeAlbumArtSize.width) / 2.0), y: 34.0), size: largeAlbumArtSize)
if animateIn && transition.isAnimated {
largeAlbumArtNode.frame = largeAlbumArtFrame
transition.animatePositionAdditive(node: largeAlbumArtNode, offset: CGPoint(x: previousAlbumArtNodeFrame.center.x - largeAlbumArtFrame.center.x, y: previousAlbumArtNodeFrame.center.y - largeAlbumArtFrame.center.y))
//largeAlbumArtNode.layer.animatePosition(from: CGPoint(x: -50.0, y: 0.0), to: CGPoint(), duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, additive: true)
transition.animateTransformScale(node: largeAlbumArtNode, from: previousAlbumArtNodeFrame.size.height / largeAlbumArtFrame.size.height)
largeAlbumArtNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12)
if let copyView = self.albumArtNode.view.snapshotContentTree() {
copyView.frame = previousAlbumArtNodeFrame
copyView.center = largeAlbumArtFrame.center
self.view.insertSubview(copyView, belowSubview: largeAlbumArtNode.view)
transition.animatePositionAdditive(layer: copyView.layer, offset: CGPoint(x: previousAlbumArtNodeFrame.center.x - largeAlbumArtFrame.center.x, y: previousAlbumArtNodeFrame.center.y - largeAlbumArtFrame.center.y), completion: { [weak copyView] _ in
copyView?.removeFromSuperview()
})
//copyView.layer.animatePosition(from: CGPoint(x: -50.0, y: 0.0), to: CGPoint(), duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, additive: true)
copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.28, removeOnCompletion: false)
transition.updateTransformScale(layer: copyView.layer, scale: largeAlbumArtFrame.size.height / previousAlbumArtNodeFrame.size.height)
}
} else {
transition.updateFrame(node: largeAlbumArtNode, frame: largeAlbumArtFrame)
}
self.albumArtNode.isHidden = true
} else if let largeAlbumArtNode = self.largeAlbumArtNode {
self.largeAlbumArtNode = nil
self.albumArtNode.isHidden = false
if transition.isAnimated {
transition.animatePosition(node: self.albumArtNode, from: largeAlbumArtNode.frame.center)
transition.animateTransformScale(node: self.albumArtNode, from: largeAlbumArtNode.frame.height / self.albumArtNode.frame.height)
self.albumArtNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12)
transition.updatePosition(node: largeAlbumArtNode, position: self.albumArtNode.frame.center, completion: { [weak largeAlbumArtNode] _ in
largeAlbumArtNode?.removeFromSupernode()
})
largeAlbumArtNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.28, removeOnCompletion: false)
transition.updateTransformScale(node: largeAlbumArtNode, scale: self.albumArtNode.frame.height / largeAlbumArtNode.frame.height)
} else {
largeAlbumArtNode.removeFromSupernode()
}
}
let scrubberVerticalOrigin: CGFloat = infoVerticalOrigin + 58.0
let scrubberInset: CGFloat = 9.0
@@ -1283,15 +1189,16 @@ final class OverlayAudioPlayerControlsNode: ASDisplayNode {
}
@objc func albumArtTap(_ recognizer: UITapGestureRecognizer) {
if !"".isEmpty, case .ended = recognizer.state {
if case .ended = recognizer.state {
if let supernode = self.supernode {
let bounds = supernode.bounds
if bounds.width > bounds.height {
return
}
}
self.isExpanded = !self.isExpanded
self.updateIsExpanded?()
if let currentFileReference = self.currentFileReference, let currentAlbumArt = self.currentAlbumArt {
self.requestAlbumArtDisplay?((currentFileReference, currentAlbumArt))
}
}
}