mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
764 lines
39 KiB
Swift
764 lines
39 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import Postbox
|
|
import TelegramCore
|
|
import SyncCore
|
|
import SwiftSignalKit
|
|
import TelegramPresentationData
|
|
import UniversalMediaPlayer
|
|
import TelegramUIPreferences
|
|
import AccountContext
|
|
import PhotoResources
|
|
import AppBundle
|
|
|
|
private func generateBackground(theme: PresentationTheme) -> UIImage? {
|
|
return generateImage(CGSize(width: 20.0, height: 10.0 + 8.0), rotatedContext: { size, context in
|
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
context.setShadow(offset: CGSize(width: 0.0, height: -4.0), blur: 20.0, color: UIColor(white: 0.0, alpha: 0.3).cgColor)
|
|
context.setFillColor(theme.list.plainBackgroundColor.cgColor)
|
|
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 8.0), size: CGSize(width: 20.0, height: 20.0)))
|
|
})?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 10 + 8)
|
|
}
|
|
|
|
private func generateCollapseIcon(theme: PresentationTheme) -> UIImage? {
|
|
return generateImage(CGSize(width: 38.0, height: 5.0), rotatedContext: { size, context in
|
|
let bounds = CGRect(origin: CGPoint(), size: size)
|
|
context.clear(bounds)
|
|
|
|
let path = UIBezierPath(roundedRect: bounds, cornerRadius: 2.5)
|
|
context.setFillColor(theme.list.controlSecondaryColor.cgColor)
|
|
context.addPath(path.cgPath)
|
|
context.fillPath()
|
|
})
|
|
}
|
|
|
|
private let digitsSet = CharacterSet(charactersIn: "0123456789")
|
|
private func timestampLabelWidthForDuration(_ timestamp: Double) -> CGFloat {
|
|
let text: String
|
|
if timestamp > 0 {
|
|
let timestamp = Int32(timestamp)
|
|
let hours = timestamp / (60 * 60)
|
|
let minutes = timestamp % (60 * 60) / 60
|
|
let seconds = timestamp % 60
|
|
if hours != 0 {
|
|
text = String(format: "%d:%02d:%02d", hours, minutes, seconds)
|
|
} else {
|
|
text = String(format: "%d:%02d", minutes, seconds)
|
|
}
|
|
} else {
|
|
text = "-:--"
|
|
}
|
|
|
|
let convertedString = text.components(separatedBy: digitsSet).joined(separator: "8")
|
|
let string = NSAttributedString(string: convertedString, 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
|
|
return size.width
|
|
}
|
|
|
|
private let titleFont = Font.semibold(17.0)
|
|
private let descriptionFont = Font.regular(17.0)
|
|
|
|
private func stringsForDisplayData(_ data: SharedMediaPlaybackDisplayData?, presentationData: PresentationData) -> (NSAttributedString?, NSAttributedString?) {
|
|
var titleString: NSAttributedString?
|
|
var descriptionString: NSAttributedString?
|
|
|
|
if let data = data {
|
|
let titleText: String
|
|
let subtitleText: String
|
|
switch data {
|
|
case let .music(title, performer, _, _):
|
|
titleText = title ?? presentationData.strings.MediaPlayer_UnknownTrack
|
|
subtitleText = performer ?? presentationData.strings.MediaPlayer_UnknownArtist
|
|
case .voice, .instantVideo:
|
|
titleText = ""
|
|
subtitleText = ""
|
|
}
|
|
|
|
titleString = NSAttributedString(string: titleText, font: titleFont, textColor: presentationData.theme.list.itemPrimaryTextColor)
|
|
descriptionString = NSAttributedString(string: subtitleText, font: descriptionFont, textColor: presentationData.theme.list.itemSecondaryTextColor)
|
|
}
|
|
|
|
return (titleString, descriptionString)
|
|
}
|
|
|
|
final class OverlayPlayerControlsNode: ASDisplayNode {
|
|
private let accountManager: AccountManager
|
|
private let postbox: Postbox
|
|
private var presentationData: PresentationData
|
|
|
|
private let backgroundNode: ASImageNode
|
|
|
|
private let collapseNode: HighlightableButtonNode
|
|
|
|
private let albumArtNode: TransformImageNode
|
|
private var largeAlbumArtNode: TransformImageNode?
|
|
private let titleNode: TextNode
|
|
private let descriptionNode: TextNode
|
|
private let shareNode: HighlightableButtonNode
|
|
|
|
private let scrubberNode: MediaPlayerScrubbingNode
|
|
private let leftDurationLabel: MediaPlayerTimeTextNode
|
|
private let rightDurationLabel: MediaPlayerTimeTextNode
|
|
|
|
private let backwardButton: IconButtonNode
|
|
private let forwardButton: IconButtonNode
|
|
|
|
private var currentIsPaused: Bool?
|
|
private let playPauseButton: IconButtonNode
|
|
|
|
private var currentOrder: MusicPlaybackSettingsOrder?
|
|
private let orderButton: IconButtonNode
|
|
|
|
private var currentLooping: MusicPlaybackSettingsLooping?
|
|
private let loopingButton: IconButtonNode
|
|
|
|
private var currentRate: AudioPlaybackRate?
|
|
private let rateButton: HighlightableButtonNode
|
|
|
|
let separatorNode: ASDisplayNode
|
|
|
|
var isExpanded = false
|
|
var updateIsExpanded: (() -> Void)?
|
|
|
|
var requestCollapse: (() -> Void)?
|
|
var requestShare: ((MessageId) -> Void)?
|
|
|
|
var updateOrder: ((MusicPlaybackSettingsOrder) -> Void)?
|
|
var control: ((SharedMediaPlayerControlAction) -> Void)?
|
|
|
|
private(set) var currentItemId: SharedMediaPlaylistItemId?
|
|
private var displayData: SharedMediaPlaybackDisplayData?
|
|
private var currentAlbumArtInitialized = false
|
|
private var currentAlbumArt: SharedMediaPlaybackAlbumArt?
|
|
private var currentFileReference: FileMediaReference?
|
|
private var statusDisposable: Disposable?
|
|
|
|
private var scrubbingDisposable: Disposable?
|
|
private var leftDurationLabelPushed = false
|
|
private var rightDurationLabelPushed = false
|
|
private var currentDuration: Double = 0.0
|
|
|
|
private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat)?
|
|
|
|
init(account: Account, accountManager: AccountManager, presentationData: PresentationData, status: Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading)?, NoError>) {
|
|
self.accountManager = accountManager
|
|
self.postbox = account.postbox
|
|
self.presentationData = presentationData
|
|
|
|
self.backgroundNode = ASImageNode()
|
|
self.backgroundNode.isLayerBacked = true
|
|
self.backgroundNode.displayWithoutProcessing = true
|
|
self.backgroundNode.displaysAsynchronously = false
|
|
self.backgroundNode.image = generateBackground(theme: presentationData.theme)
|
|
|
|
self.collapseNode = HighlightableButtonNode()
|
|
self.collapseNode.displaysAsynchronously = false
|
|
self.collapseNode.setImage(generateCollapseIcon(theme: presentationData.theme), for: [])
|
|
|
|
self.albumArtNode = TransformImageNode()
|
|
|
|
self.titleNode = TextNode()
|
|
self.titleNode.isUserInteractionEnabled = false
|
|
self.titleNode.displaysAsynchronously = false
|
|
|
|
self.descriptionNode = TextNode()
|
|
self.descriptionNode.isUserInteractionEnabled = false
|
|
self.descriptionNode.displaysAsynchronously = false
|
|
|
|
self.shareNode = HighlightableButtonNode()
|
|
self.shareNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Share"), color: presentationData.theme.list.itemAccentColor), for: [])
|
|
|
|
self.scrubberNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 3.0, lineCap: .round, scrubberHandle: .circle, backgroundColor: presentationData.theme.list.controlSecondaryColor, foregroundColor: presentationData.theme.list.itemAccentColor, bufferingColor: presentationData.theme.list.itemAccentColor.withAlphaComponent(0.4), chapters: []))
|
|
self.leftDurationLabel = MediaPlayerTimeTextNode(textColor: presentationData.theme.list.itemSecondaryTextColor)
|
|
self.leftDurationLabel.displaysAsynchronously = false
|
|
self.leftDurationLabel.keepPreviousValueOnEmptyState = true
|
|
self.rightDurationLabel = MediaPlayerTimeTextNode(textColor: presentationData.theme.list.itemSecondaryTextColor)
|
|
self.rightDurationLabel.displaysAsynchronously = false
|
|
self.rightDurationLabel.mode = .reversed
|
|
self.rightDurationLabel.alignment = .right
|
|
self.rightDurationLabel.keepPreviousValueOnEmptyState = true
|
|
|
|
self.rateButton = HighlightableButtonNode()
|
|
self.rateButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -4.0, bottom: -8.0, right: -4.0)
|
|
self.rateButton.displaysAsynchronously = false
|
|
|
|
self.backwardButton = IconButtonNode()
|
|
self.backwardButton.displaysAsynchronously = false
|
|
|
|
self.forwardButton = IconButtonNode()
|
|
self.forwardButton.displaysAsynchronously = false
|
|
|
|
self.orderButton = IconButtonNode()
|
|
self.orderButton.displaysAsynchronously = false
|
|
|
|
self.loopingButton = IconButtonNode()
|
|
self.loopingButton.displaysAsynchronously = false
|
|
|
|
self.playPauseButton = IconButtonNode()
|
|
self.playPauseButton.displaysAsynchronously = false
|
|
|
|
self.backwardButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Previous"), color: presentationData.theme.list.itemPrimaryTextColor)
|
|
self.forwardButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Next"), color: presentationData.theme.list.itemPrimaryTextColor)
|
|
|
|
self.separatorNode = ASDisplayNode()
|
|
self.separatorNode.isLayerBacked = true
|
|
self.separatorNode.backgroundColor = presentationData.theme.list.itemPlainSeparatorColor
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.backgroundNode)
|
|
|
|
self.addSubnode(self.collapseNode)
|
|
|
|
self.addSubnode(self.albumArtNode)
|
|
self.addSubnode(self.titleNode)
|
|
self.addSubnode(self.descriptionNode)
|
|
self.addSubnode(self.shareNode)
|
|
|
|
self.addSubnode(self.leftDurationLabel)
|
|
self.addSubnode(self.rightDurationLabel)
|
|
self.addSubnode(self.rateButton)
|
|
self.addSubnode(self.scrubberNode)
|
|
|
|
self.addSubnode(self.orderButton)
|
|
self.addSubnode(self.loopingButton)
|
|
self.addSubnode(self.backwardButton)
|
|
self.addSubnode(self.forwardButton)
|
|
self.addSubnode(self.playPauseButton)
|
|
|
|
self.addSubnode(self.separatorNode)
|
|
|
|
let accountId = account.id
|
|
let delayedStatus = status
|
|
|> mapToSignal { value -> Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading)?, NoError> in
|
|
guard let value = value, value.0.id == accountId else {
|
|
return .single(nil)
|
|
}
|
|
switch value.1 {
|
|
case .state:
|
|
return .single(value)
|
|
case .loading:
|
|
return .single(value)
|
|
|> delay(0.1, queue: .mainQueue())
|
|
}
|
|
}
|
|
|
|
let mappedStatus = combineLatest(delayedStatus, self.scrubberNode.scrubbingTimestamp) |> map { value, scrubbingTimestamp -> MediaPlayerStatus in
|
|
if let (_, valueOrLoading) = value, case let .state(value) = valueOrLoading {
|
|
return MediaPlayerStatus(generationTimestamp: scrubbingTimestamp != nil ? 0 : value.status.generationTimestamp, duration: value.status.duration, dimensions: value.status.dimensions, timestamp: scrubbingTimestamp ?? value.status.timestamp, baseRate: value.status.baseRate, seekId: value.status.seekId, status: value.status.status, soundEnabled: value.status.soundEnabled)
|
|
} else {
|
|
return MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true)
|
|
}
|
|
}
|
|
self.scrubberNode.status = mappedStatus
|
|
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
|
|
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
|
|
}
|
|
|
|
var rateButtonIsHidden = true
|
|
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)
|
|
}
|
|
|
|
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.scrubberNode.enableFineScrubbing = true
|
|
rateButtonIsHidden = false
|
|
} else {
|
|
strongSelf.scrubberNode.enableFineScrubbing = false
|
|
rateButtonIsHidden = true
|
|
}
|
|
|
|
let duration = value.status.duration
|
|
if duration != strongSelf.currentDuration && !duration.isZero {
|
|
strongSelf.currentDuration = duration
|
|
if let layout = strongSelf.validLayout {
|
|
strongSelf.updateLayout(width: layout.0, leftInset: layout.1, rightInset: layout.2, maxHeight: layout.3, transition: .immediate)
|
|
}
|
|
}
|
|
|
|
strongSelf.rateButton.isHidden = rateButtonIsHidden
|
|
} 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)
|
|
}
|
|
})
|
|
|
|
self.scrubberNode.seek = { [weak self] value in
|
|
self?.control?(.seek(value))
|
|
}
|
|
|
|
self.collapseNode.addTarget(self, action: #selector(self.collapsePressed), forControlEvents: .touchUpInside)
|
|
self.shareNode.addTarget(self, action: #selector(self.sharePressed), forControlEvents: .touchUpInside)
|
|
self.orderButton.addTarget(self, action: #selector(self.orderPressed), forControlEvents: .touchUpInside)
|
|
self.loopingButton.addTarget(self, action: #selector(self.loopingPressed), forControlEvents: .touchUpInside)
|
|
self.backwardButton.addTarget(self, action: #selector(self.backwardPressed), forControlEvents: .touchUpInside)
|
|
self.forwardButton.addTarget(self, action: #selector(self.forwardPressed), forControlEvents: .touchUpInside)
|
|
self.playPauseButton.addTarget(self, action: #selector(self.playPausePressed), forControlEvents: .touchUpInside)
|
|
self.rateButton.addTarget(self, action: #selector(self.rateButtonPressed), forControlEvents: .touchUpInside)
|
|
}
|
|
|
|
deinit {
|
|
self.statusDisposable?.dispose()
|
|
self.scrubbingDisposable?.dispose()
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
self.albumArtNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.albumArtTap(_:))))
|
|
}
|
|
|
|
func updatePresentationData(_ presentationData: PresentationData) {
|
|
guard self.presentationData.theme !== presentationData.theme else {
|
|
return
|
|
}
|
|
self.presentationData = presentationData
|
|
|
|
self.backgroundNode.image = generateBackground(theme: presentationData.theme)
|
|
self.collapseNode.setImage(generateCollapseIcon(theme: presentationData.theme), for: [])
|
|
self.shareNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Share"), color: presentationData.theme.list.itemAccentColor), for: [])
|
|
self.scrubberNode.updateColors(backgroundColor: presentationData.theme.list.controlSecondaryColor, foregroundColor: presentationData.theme.list.itemAccentColor)
|
|
self.leftDurationLabel.textColor = presentationData.theme.list.itemSecondaryTextColor
|
|
self.rightDurationLabel.textColor = presentationData.theme.list.itemSecondaryTextColor
|
|
self.backwardButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Previous"), color: presentationData.theme.list.itemPrimaryTextColor)
|
|
self.forwardButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Next"), color: presentationData.theme.list.itemPrimaryTextColor)
|
|
if let isPaused = self.currentIsPaused {
|
|
self.updatePlayPauseButton(paused: isPaused)
|
|
}
|
|
if let order = self.currentOrder {
|
|
self.updateOrderButton(order)
|
|
}
|
|
if let looping = self.currentLooping {
|
|
self.updateLoopButton(looping)
|
|
}
|
|
if let rate = self.currentRate {
|
|
self.updateRateButton(rate)
|
|
}
|
|
self.separatorNode.backgroundColor = presentationData.theme.list.itemPlainSeparatorColor
|
|
}
|
|
|
|
private func updateLabels(transition: ContainedViewLayoutTransition) {
|
|
guard let (width, leftInset, rightInset, maxHeight) = self.validLayout else {
|
|
return
|
|
}
|
|
|
|
let panelHeight = OverlayPlayerControlsNode.heightForLayout(width: width, leftInset: leftInset, rightInset: rightInset, maxHeight: maxHeight, isExpanded: self.isExpanded)
|
|
|
|
let sideInset: CGFloat = 20.0
|
|
|
|
let infoLabelsLeftInset: CGFloat = 60.0
|
|
let infoLabelsRightInset: CGFloat = 32.0
|
|
|
|
let infoVerticalOrigin: CGFloat = panelHeight - OverlayPlayerControlsNode.basePanelHeight + 36.0
|
|
|
|
let (titleString, descriptionString) = stringsForDisplayData(self.displayData, presentationData: self.presentationData)
|
|
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
|
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - sideInset * 2.0 - leftInset - rightInset - infoLabelsLeftInset - infoLabelsRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
|
|
let makeDescriptionLayout = TextNode.asyncLayout(self.descriptionNode)
|
|
let (descriptionLayout, descriptionApply) = makeDescriptionLayout(TextNodeLayoutArguments(attributedString: descriptionString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - sideInset * 2.0 - leftInset - rightInset - infoLabelsLeftInset - infoLabelsRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
|
|
|
|
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 + 26.0), size: descriptionLayout.size))
|
|
let _ = descriptionApply()
|
|
|
|
var albumArt: SharedMediaPlaybackAlbumArt?
|
|
if let displayData = self.displayData {
|
|
switch displayData {
|
|
case let .music(_, _, value, _):
|
|
albumArt = value
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
if self.currentAlbumArt != albumArt || !self.currentAlbumArtInitialized {
|
|
self.currentAlbumArtInitialized = true
|
|
self.currentAlbumArt = albumArt
|
|
self.albumArtNode.setSignal(playerAlbumArt(postbox: self.postbox, fileReference: self.currentFileReference, albumArt: albumArt, thumbnail: true))
|
|
if let largeAlbumArtNode = self.largeAlbumArtNode {
|
|
largeAlbumArtNode.setSignal(playerAlbumArt(postbox: self.postbox, fileReference: self.currentFileReference, albumArt: albumArt, thumbnail: false))
|
|
}
|
|
}
|
|
}
|
|
|
|
private func updatePlayPauseButton(paused: Bool) {
|
|
if paused {
|
|
self.playPauseButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Play"), color: self.presentationData.theme.list.itemPrimaryTextColor)
|
|
} else {
|
|
self.playPauseButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Pause"), color: self.presentationData.theme.list.itemPrimaryTextColor)
|
|
}
|
|
}
|
|
|
|
private func updateOrderButton(_ order: MusicPlaybackSettingsOrder) {
|
|
let baseColor = self.presentationData.theme.list.itemSecondaryTextColor
|
|
switch order {
|
|
case .regular:
|
|
self.orderButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/OrderReverse"), color: baseColor)
|
|
case .reversed:
|
|
self.orderButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/OrderReverse"), color: self.presentationData.theme.list.itemAccentColor)
|
|
case .random:
|
|
self.orderButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/OrderRandom"), color: self.presentationData.theme.list.itemAccentColor)
|
|
}
|
|
}
|
|
|
|
private func updateLoopButton(_ looping: MusicPlaybackSettingsLooping) {
|
|
let baseColor = self.presentationData.theme.list.itemSecondaryTextColor
|
|
switch looping {
|
|
case .none:
|
|
self.loopingButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Repeat"), color: baseColor)
|
|
case .item:
|
|
self.loopingButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/RepeatOne"), color: self.presentationData.theme.list.itemAccentColor)
|
|
case .all:
|
|
self.loopingButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Repeat"), color: self.presentationData.theme.list.itemAccentColor)
|
|
}
|
|
}
|
|
|
|
private func updateRateButton(_ baseRate: AudioPlaybackRate) {
|
|
switch baseRate {
|
|
case .x2:
|
|
self.rateButton.setImage(PresentationResourcesRootController.navigationPlayerMaximizedRateActiveIcon(self.presentationData.theme), for: [])
|
|
default:
|
|
self.rateButton.setImage(PresentationResourcesRootController.navigationPlayerMaximizedRateInactiveIcon(self.presentationData.theme), for: [])
|
|
}
|
|
}
|
|
|
|
static let basePanelHeight: CGFloat = 220.0
|
|
|
|
static func heightForLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, isExpanded: Bool) -> CGFloat {
|
|
var panelHeight: CGFloat = OverlayPlayerControlsNode.basePanelHeight
|
|
if isExpanded {
|
|
let sideInset: CGFloat = 20.0
|
|
panelHeight += width - leftInset - rightInset - sideInset * 2.0 + 24.0
|
|
}
|
|
return min(panelHeight, maxHeight)
|
|
}
|
|
|
|
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)))
|
|
|
|
transition.updateFrame(node: self.collapseNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 2.0), size: CGSize(width: width, height: 30.0)))
|
|
|
|
let sideInset: CGFloat = 20.0
|
|
let sideButtonsInset: CGFloat = sideInset + 36.0
|
|
|
|
let infoVerticalOrigin: CGFloat = panelHeight - OverlayPlayerControlsNode.basePanelHeight + 36.0
|
|
|
|
self.updateLabels(transition: transition)
|
|
|
|
transition.updateFrame(node: self.shareNode, frame: CGRect(origin: CGPoint(x: width - rightInset - sideInset - 32.0, y: infoVerticalOrigin + 2.0), size: CGSize(width: 42.0, height: 42.0)))
|
|
|
|
let albumArtSize = CGSize(width: 48.0, height: 48.0)
|
|
let makeAlbumArtLayout = self.albumArtNode.asyncLayout()
|
|
let applyAlbumArt = makeAlbumArtLayout(TransformImageArguments(corners: ImageCorners(radius: 4.0), imageSize: albumArtSize, boundingSize: albumArtSize, intrinsicInsets: UIEdgeInsets()))
|
|
applyAlbumArt()
|
|
let albumArtFrame = CGRect(origin: CGPoint(x: leftInset + sideInset, y: infoVerticalOrigin - 1.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()
|
|
if self.isNodeLoaded {
|
|
largeAlbumArtNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.albumArtTap(_:))))
|
|
}
|
|
self.largeAlbumArtNode = largeAlbumArtNode
|
|
self.addSubnode(largeAlbumArtNode)
|
|
if self.currentAlbumArtInitialized {
|
|
largeAlbumArtNode.setSignal(playerAlbumArt(postbox: self.postbox, fileReference: self.currentFileReference, albumArt: self.currentAlbumArt, thumbnail: false))
|
|
}
|
|
}
|
|
|
|
let albumArtHeight = max(1.0, panelHeight - OverlayPlayerControlsNode.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 + 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)))
|
|
|
|
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)))
|
|
|
|
let rateRightOffset = timestampLabelWidthForDuration(self.currentDuration)
|
|
transition.updateFrame(node: self.rateButton, frame: CGRect(origin: CGPoint(x: width - sideInset - rightInset - rateRightOffset - 28.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)))
|
|
|
|
let buttonSize = CGSize(width: 64.0, height: 64.0)
|
|
let buttonsWidth = min(width - leftInset - rightInset - sideButtonsInset * 2.0, 320.0)
|
|
let buttonsRect = CGRect(origin: CGPoint(x: floor((width - buttonsWidth) / 2.0), y: scrubberVerticalOrigin + 36.0), size: CGSize(width: buttonsWidth, height: buttonSize.height))
|
|
|
|
transition.updateFrame(node: self.orderButton, frame: CGRect(origin: CGPoint(x: leftInset + sideInset - 22.0, y: buttonsRect.minY), size: buttonSize))
|
|
transition.updateFrame(node: self.loopingButton, frame: CGRect(origin: CGPoint(x: width - rightInset - sideInset - buttonSize.width + 22.0, y: buttonsRect.minY), size: buttonSize))
|
|
|
|
transition.updateFrame(node: self.backwardButton, frame: CGRect(origin: buttonsRect.origin, size: buttonSize))
|
|
transition.updateFrame(node: self.forwardButton, frame: CGRect(origin: CGPoint(x: buttonsRect.maxX - buttonSize.width, y: buttonsRect.minY), size: buttonSize))
|
|
transition.updateFrame(node: self.playPauseButton, frame: CGRect(origin: CGPoint(x: buttonsRect.minX + floor((buttonsRect.width - buttonSize.width) / 2.0), y: buttonsRect.minY), size: buttonSize))
|
|
|
|
return panelHeight
|
|
}
|
|
|
|
func collapse() {
|
|
if self.isExpanded {
|
|
self.isExpanded = false
|
|
self.updateIsExpanded?()
|
|
}
|
|
}
|
|
|
|
@objc func collapsePressed() {
|
|
self.requestCollapse?()
|
|
}
|
|
|
|
@objc func sharePressed() {
|
|
if let itemId = self.currentItemId as? PeerMessagesMediaPlaylistItemId {
|
|
self.requestShare?(itemId.messageId)
|
|
}
|
|
}
|
|
|
|
@objc func orderPressed() {
|
|
if let order = self.currentOrder {
|
|
let nextOrder: MusicPlaybackSettingsOrder
|
|
switch order {
|
|
case .regular:
|
|
nextOrder = .reversed
|
|
case .reversed:
|
|
nextOrder = .random
|
|
case .random:
|
|
nextOrder = .regular
|
|
}
|
|
let _ = updateMusicPlaybackSettingsInteractively(accountManager: self.accountManager, {
|
|
return $0.withUpdatedOrder(nextOrder)
|
|
}).start()
|
|
self.control?(.setOrder(nextOrder))
|
|
}
|
|
}
|
|
|
|
@objc func loopingPressed() {
|
|
if let looping = self.currentLooping {
|
|
let nextLooping: MusicPlaybackSettingsLooping
|
|
switch looping {
|
|
case .none:
|
|
nextLooping = .item
|
|
case .item:
|
|
nextLooping = .all
|
|
case .all:
|
|
nextLooping = .none
|
|
}
|
|
let _ = updateMusicPlaybackSettingsInteractively(accountManager: self.accountManager, {
|
|
return $0.withUpdatedLooping(nextLooping)
|
|
}).start()
|
|
self.control?(.setLooping(nextLooping))
|
|
}
|
|
}
|
|
|
|
@objc func backwardPressed() {
|
|
self.control?(.previous)
|
|
}
|
|
|
|
@objc func forwardPressed() {
|
|
self.control?(.next)
|
|
}
|
|
|
|
@objc func playPausePressed() {
|
|
self.control?(.playback(.togglePlayPause))
|
|
}
|
|
|
|
@objc func rateButtonPressed() {
|
|
var nextRate: AudioPlaybackRate
|
|
if let currentRate = self.currentRate {
|
|
switch currentRate {
|
|
case .x1:
|
|
nextRate = .x2
|
|
default:
|
|
nextRate = .x1
|
|
}
|
|
} else {
|
|
nextRate = .x2
|
|
}
|
|
self.control?(.setBaseRate(nextRate))
|
|
}
|
|
|
|
@objc func albumArtTap(_ recognizer: UITapGestureRecognizer) {
|
|
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?()
|
|
}
|
|
}
|
|
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
let result = super.hitTest(point, with: event)
|
|
if result == self.view {
|
|
return nil
|
|
}
|
|
return result
|
|
}
|
|
}
|