Swiftgram/submodules/InstantPageUI/Sources/InstantPageAudioNode.swift
2023-04-19 23:47:38 +04:00

285 lines
14 KiB
Swift

import Foundation
import UIKit
import TelegramCore
import SwiftSignalKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import UniversalMediaPlayer
import AccountContext
import RadialStatusNode
private func generatePlayButton(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 48.0, height: 48.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
context.setStrokeColor(color.cgColor)
context.setLineWidth(1.65)
let _ = try? drawSvgPath(context, path: "M24,0.825 C11.2008009,0.825 0.825,11.2008009 0.825,24 C0.825,36.7991991 11.2008009,47.175 24,47.175 C36.7991991,47.175 47.175,36.7991991 47.175,24 C47.175,11.2008009 36.7991991,0.825 24,0.825 S ")
let _ = try? drawSvgPath(context, path: "M19,16.8681954 L19,32.1318046 L19,32.1318046 C19,32.6785665 19.4432381,33.1218046 19.99,33.1218046 C20.1882157,33.1218046 20.3818677,33.0623041 20.5458864,32.9510057 L31.7927564,25.319201 L31.7927564,25.319201 C32.2451886,25.0121934 32.3630786,24.3965458 32.056071,23.9441136 C31.9857457,23.8404762 31.8963938,23.7511243 31.7927564,23.680799 L20.5458864,16.0489943 L20.5458864,16.0489943 C20.0934542,15.7419868 19.4778066,15.8598767 19.170799,16.312309 C19.0595006,16.4763277 19,16.6699796 19,16.8681954 Z ")
})
}
private func generatePauseButton(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 48.0, height: 48.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
context.setStrokeColor(color.cgColor)
context.setLineWidth(1.65)
let _ = try? drawSvgPath(context, path: "M24,0.825 C11.2008009,0.825 0.825,11.2008009 0.825,24 C0.825,36.7991991 11.2008009,47.175 24,47.175 C36.7991991,47.175 47.175,36.7991991 47.175,24 C47.175,11.2008009 36.7991991,0.825 24,0.825 S ")
let _ = try? drawSvgPath(context, path: "M17,16 L21,16 C21.5567619,16 22,16.4521029 22,17 L22,32 C22,32.5478971 21.5567619,33 21,33 L17,33 C16.4432381,33 16,32.5478971 16,32 L16,17 C16,16.4521029 16.4432381,16 17,16 Z ")
let _ = try? drawSvgPath(context, path: "M26.99,16 L31.01,16 C31.5567619,16 32,16.4432381 32,16.99 L32,32.01 C32,32.5567619 31.5567619,33 31.01,33 L26.99,33 C26.4432381,33 26,32.5567619 26,32.01 L26,16.99 C26,16.4432381 26.4432381,16 26.99,16 Z ")
})
}
private func titleString(media: InstantPageMedia, theme: InstantPageTheme, strings: PresentationStrings) -> NSAttributedString {
let string = NSMutableAttributedString()
if case let .file(file) = media.media {
loop: for attribute in file.attributes {
if case let .Audio(isVoice, _, title, performer, _) = attribute, !isVoice {
let titleText: String = title ?? strings.MediaPlayer_UnknownTrack
let subtitleText: String = performer ?? strings.MediaPlayer_UnknownArtist
let titleString = NSAttributedString(string: titleText, font: Font.semibold(17.0), textColor: theme.textCategories.paragraph.color)
let subtitleString = NSAttributedString(string: "\(subtitleText)", font: Font.regular(17.0), textColor: theme.textCategories.paragraph.color)
string.append(titleString)
string.append(subtitleString)
break loop
}
}
}
return string
}
final class InstantPageAudioNode: ASDisplayNode, InstantPageNode {
private let context: AccountContext
let media: InstantPageMedia
private let openMedia: (InstantPageMedia) -> Void
private var strings: PresentationStrings
private var theme: InstantPageTheme
private let playlistType: MediaManagerPlayerType
private var playImage: UIImage
private var pauseImage: UIImage
private let buttonNode: HighlightableButtonNode
private let statusNode: RadialStatusNode
private let titleNode: ASTextNode
private let scrubbingNode: MediaPlayerScrubbingNode
private var playbackStatusDisposable: Disposable?
private var playerStatusDisposable: Disposable?
private var isPlaying: Bool = false
private var playbackState: SharedMediaPlayerItemPlaybackState?
init(context: AccountContext, strings: PresentationStrings, theme: InstantPageTheme, webPage: TelegramMediaWebpage, media: InstantPageMedia, openMedia: @escaping (InstantPageMedia) -> Void) {
self.context = context
self.strings = strings
self.theme = theme
self.media = media
self.openMedia = openMedia
self.playImage = generatePlayButton(color: theme.textCategories.paragraph.color)!
self.pauseImage = generatePauseButton(color: theme.textCategories.paragraph.color)!
self.buttonNode = HighlightableButtonNode()
self.statusNode = RadialStatusNode(backgroundNodeColor: .clear)
self.titleNode = ASTextNode()
self.titleNode.maximumNumberOfLines = 1
var backgroundAlpha: CGFloat = 0.1
var brightness: CGFloat = 0.0
theme.textCategories.paragraph.color.getHue(nil, saturation: nil, brightness: &brightness, alpha: nil)
if brightness > 0.5 {
backgroundAlpha = 0.4
}
self.scrubbingNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 3.0, lineCap: .round, scrubberHandle: .line, backgroundColor: theme.textCategories.paragraph.color.withAlphaComponent(backgroundAlpha), foregroundColor: theme.textCategories.paragraph.color, bufferingColor: theme.textCategories.paragraph.color.withAlphaComponent(0.5), chapters: []))
let playlistType: MediaManagerPlayerType
if case let .file(file) = self.media.media {
playlistType = file.isVoice ? .voice : .music
} else {
playlistType = .music
}
self.playlistType = playlistType
super.init()
self.titleNode.attributedText = titleString(media: media, theme: theme, strings: strings)
self.addSubnode(self.statusNode)
self.addSubnode(self.buttonNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.scrubbingNode)
self.statusNode.transitionToState(RadialStatusNodeState.customIcon(self.playImage), animated: false, completion: {})
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.statusNode.layer.removeAnimation(forKey: "opacity")
strongSelf.statusNode.alpha = 0.4
} else {
strongSelf.statusNode.alpha = 1.0
strongSelf.statusNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.scrubbingNode.seek = { [weak self] timestamp in
if let strongSelf = self {
if let _ = strongSelf.playbackState {
strongSelf.context.sharedContext.mediaManager.playlistControl(.seek(timestamp), type: strongSelf.playlistType)
}
}
}
/*if let applicationContext = account.applicationContext as? TelegramApplicationContext, let (playlistId, itemId) = instantPageAudioPlaylistAndItemIds(webpage: webpage, media: self.media) {
let playbackStatus: Signal<MediaPlayerPlaybackStatus?, NoError> = applicationContext.mediaManager.filteredPlaylistPlayerStateAndStatus(playlistId: playlistId, itemId: itemId)
|> mapToSignal { status -> Signal<MediaPlayerPlaybackStatus?, NoError> in
if let status = status, let playbackStatus = status.status {
return playbackStatus
|> map { playbackStatus -> MediaPlayerPlaybackStatus? in
return playbackStatus.status
}
|> distinctUntilChanged(isEqual: { lhs, rhs in
return lhs == rhs
})
} else {
return .single(nil)
}
}*/
/*self.playbackStatusDisposable = (playbackStatus |> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self {
var isPlaying = false
if let status = status {
switch status {
case .paused:
break
case let .buffering(_, whilePlaying):
isPlaying = whilePlaying
case .playing:
isPlaying = true
}
}
if strongSelf.isPlaying != isPlaying {
strongSelf.isPlaying = isPlaying
if isPlaying {
strongSelf.statusNode.transitionToState(RadialStatusNodeState.customIcon(strongSelf.pauseImage), animated: false, completion: {})
} else {
strongSelf.statusNode.transitionToState(RadialStatusNodeState.customIcon(strongSelf.playImage), animated: false, completion: {})
}
}
}
})*/
self.scrubbingNode.status = context.sharedContext.mediaManager.filteredPlaylistState(accountId: context.account.id, playlistId: InstantPageMediaPlaylistId(webpageId: webPage.webpageId), itemId: InstantPageMediaPlaylistItemId(index: self.media.index), type: self.playlistType)
|> map { playbackState -> MediaPlayerStatus in
return playbackState?.status ?? MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true)
}
self.playerStatusDisposable = (context.sharedContext.mediaManager.filteredPlaylistState(accountId: context.account.id, playlistId: InstantPageMediaPlaylistId(webpageId: webPage.webpageId), itemId: InstantPageMediaPlaylistItemId(index: self.media.index), type: playlistType)
|> deliverOnMainQueue).start(next: { [weak self] playbackState in
guard let strongSelf = self else {
return
}
strongSelf.playbackState = playbackState
let isPlaying: Bool
if let status = playbackState?.status {
if case .playing = status.status {
isPlaying = true
} else {
isPlaying = false
}
} else {
isPlaying = false
}
if strongSelf.isPlaying != isPlaying {
strongSelf.isPlaying = isPlaying
if isPlaying {
strongSelf.statusNode.transitionToState(RadialStatusNodeState.customIcon(strongSelf.pauseImage), animated: false, completion: {})
} else {
strongSelf.statusNode.transitionToState(RadialStatusNodeState.customIcon(strongSelf.playImage), animated: false, completion: {})
}
}
})
}
deinit {
self.playerStatusDisposable?.dispose()
}
func update(strings: PresentationStrings, theme: InstantPageTheme) {
if self.strings !== strings || self.theme !== theme {
let themeUpdated = self.theme !== theme
self.strings = strings
self.theme = theme
if themeUpdated {
self.playImage = generatePlayButton(color: theme.textCategories.paragraph.color)!
self.pauseImage = generatePauseButton(color: theme.textCategories.paragraph.color)!
self.titleNode.attributedText = titleString(media: self.media, theme: theme, strings: strings)
var brightness: CGFloat = 0.0
theme.textCategories.paragraph.color.getHue(nil, saturation: nil, brightness: &brightness, alpha: nil)
self.setNeedsLayout()
}
}
}
func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
return nil
}
func updateHiddenMedia(media: InstantPageMedia?) {
}
func updateIsVisible(_ isVisible: Bool) {
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
}
@objc func buttonPressed() {
if let _ = self.playbackState {
self.context.sharedContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: self.playlistType)
} else {
self.openMedia(self.media)
}
}
override func layout() {
super.layout()
let size = self.bounds.size
let insets = UIEdgeInsets(top: 18.0, left: 17.0, bottom: 18.0, right: 17.0)
let leftInset: CGFloat = 46.0 + 10.0
let rightInset: CGFloat = 0.0
let maxTitleWidth = max(1.0, size.width - insets.left - leftInset - rightInset - insets.right)
let titleSize = self.titleNode.measure(CGSize(width: maxTitleWidth, height: size.height))
self.titleNode.frame = CGRect(origin: CGPoint(x: insets.left + leftInset, y: 2.0), size: titleSize)
self.buttonNode.frame = CGRect(origin: CGPoint(x: insets.left, y: 0.0), size: CGSize(width: 48.0, height: 48.0))
self.statusNode.frame = CGRect(origin: CGPoint(x: insets.left, y: 0.0), size: CGSize(width: 48.0, height: 48.0))
var topOffset: CGFloat = 0.0
if self.titleNode.attributedText == nil || self.titleNode.attributedText!.length == 0 {
topOffset = -10.0
}
let leftScrubberInset: CGFloat = insets.left + 46.0 + 10.0
let rightScrubberInset: CGFloat = insets.right
self.scrubbingNode.frame = CGRect(origin: CGPoint(x: leftScrubberInset, y: 26.0 + topOffset), size: CGSize(width: size.width - leftScrubberInset - rightScrubberInset, height: 15.0))
}
}