Merge commit '4903eafb12bbe1c5dab121ec30c42c4bc8e85241'
@ -52,7 +52,7 @@ final class ChatVideoGalleryItemScrubberView: UIView {
|
||||
var seek: (Double) -> Void = { _ in }
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrubberNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 5.0, lineCap: .round, scrubberHandle: .circle, backgroundColor: UIColor(white: 1.0, alpha: 0.42), foregroundColor: .white))
|
||||
self.scrubberNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 5.0, lineCap: .round, scrubberHandle: .circle, backgroundColor: UIColor(white: 1.0, alpha: 0.42), foregroundColor: .white, bufferingColor: UIColor(rgb: 0xffffff, alpha: 0.5)))
|
||||
|
||||
self.leftTimestampNode = MediaPlayerTimeTextNode(textColor: .white)
|
||||
self.rightTimestampNode = MediaPlayerTimeTextNode(textColor: .white)
|
||||
|
||||
@ -98,7 +98,7 @@ final class InstantPageAudioNode: ASDisplayNode, InstantPageNode {
|
||||
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))
|
||||
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)))
|
||||
|
||||
let playlistType: MediaManagerPlayerType
|
||||
if let file = self.media.media as? TelegramMediaFile {
|
||||
|
||||
@ -18,20 +18,40 @@ private func generateHandleBackground(color: UIColor) -> UIImage? {
|
||||
})?.stretchableImage(withLeftCapWidth: 0, topCapHeight: 2)
|
||||
}
|
||||
|
||||
private final class MediaPlayerScrubbingNodeButton: ASDisplayNode {
|
||||
private final class MediaPlayerScrubbingNodeButton: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
var beginScrubbing: (() -> Void)?
|
||||
var endScrubbing: ((Bool) -> Void)?
|
||||
var updateScrubbing: ((CGFloat) -> Void)?
|
||||
var updateScrubbing: ((CGFloat, Double) -> Void)?
|
||||
var updateMultiplier: ((Double) -> Void)?
|
||||
|
||||
var highlighted: ((Bool) -> Void)?
|
||||
|
||||
var verticalPanEnabled = false
|
||||
var hapticFeedback = HapticFeedback()
|
||||
|
||||
private var scrubbingMultiplier: Double = 1.0
|
||||
private var scrubbingStartLocation: CGPoint?
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.view.disablesInteractiveTransitionGestureRecognizer = true
|
||||
self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))))
|
||||
|
||||
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
|
||||
gestureRecognizer.delegate = self
|
||||
self.view.addGestureRecognizer(gestureRecognizer)
|
||||
}
|
||||
|
||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
guard let gestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer else {
|
||||
return !self.verticalPanEnabled
|
||||
}
|
||||
let translation = gestureRecognizer.translation(in: gestureRecognizer.view)
|
||||
if self.verticalPanEnabled {
|
||||
return abs(translation.x) > abs(translation.y) || translation.y > 0.0
|
||||
} else {
|
||||
return abs(translation.x) > abs(translation.y)
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
@ -66,15 +86,38 @@ private final class MediaPlayerScrubbingNodeButton: ASDisplayNode {
|
||||
case .changed:
|
||||
if let scrubbingStartLocation = self.scrubbingStartLocation {
|
||||
let delta = location.x - scrubbingStartLocation.x
|
||||
self.updateScrubbing?(delta / self.bounds.size.width)
|
||||
var multiplier: Double = 1.0
|
||||
var skipUpdate = false
|
||||
if self.verticalPanEnabled, location.y > scrubbingStartLocation.y {
|
||||
let verticalDelta = abs(location.y - scrubbingStartLocation.y)
|
||||
if verticalDelta > 150.0 {
|
||||
multiplier = 0.01
|
||||
} else if verticalDelta > 100.0 {
|
||||
multiplier = 0.25
|
||||
} else if verticalDelta > 50.0 {
|
||||
multiplier = 0.5
|
||||
}
|
||||
if multiplier != self.scrubbingMultiplier {
|
||||
skipUpdate = true
|
||||
self.scrubbingMultiplier = multiplier
|
||||
self.scrubbingStartLocation = CGPoint(x: location.x, y: scrubbingStartLocation.y)
|
||||
self.updateMultiplier?(multiplier)
|
||||
|
||||
self.hapticFeedback.impact()
|
||||
}
|
||||
}
|
||||
if !skipUpdate {
|
||||
self.updateScrubbing?(delta / self.bounds.size.width, multiplier)
|
||||
}
|
||||
}
|
||||
case .ended, .cancelled:
|
||||
if let scrubbingStartLocation = self.scrubbingStartLocation {
|
||||
self.scrubbingStartLocation = nil
|
||||
let delta = location.x - scrubbingStartLocation.x
|
||||
self.updateScrubbing?(delta / self.bounds.size.width)
|
||||
self.updateScrubbing?(delta / self.bounds.size.width, self.scrubbingMultiplier)
|
||||
self.endScrubbing?(recognizer.state == .ended)
|
||||
self.highlighted?(false)
|
||||
self.scrubbingMultiplier = 1.0
|
||||
}
|
||||
default:
|
||||
break
|
||||
@ -106,7 +149,7 @@ public enum MediaPlayerScrubbingNodeHandle {
|
||||
}
|
||||
|
||||
public enum MediaPlayerScrubbingNodeContent {
|
||||
case standard(lineHeight: CGFloat, lineCap: MediaPlayerScrubbingNodeCap, scrubberHandle: MediaPlayerScrubbingNodeHandle, backgroundColor: UIColor, foregroundColor: UIColor)
|
||||
case standard(lineHeight: CGFloat, lineCap: MediaPlayerScrubbingNodeCap, scrubberHandle: MediaPlayerScrubbingNodeHandle, backgroundColor: UIColor, foregroundColor: UIColor, bufferingColor: UIColor)
|
||||
case custom(backgroundNode: ASDisplayNode, foregroundContentNode: ASDisplayNode)
|
||||
}
|
||||
|
||||
@ -194,10 +237,12 @@ private final class MediaPlayerScrubbingBufferingNode: ASDisplayNode {
|
||||
for range in ranges.0.rangeView {
|
||||
let rangeWidth = min(size.width, (CGFloat(range.count) / CGFloat(ranges.1)) * size.width)
|
||||
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: rangeWidth, height: size.height)))
|
||||
transition.updateAlpha(node: self.foregroundNode, alpha: abs(size.width - rangeWidth) < 1.0 ? 0.0 : 1.0)
|
||||
break
|
||||
}
|
||||
} else {
|
||||
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: 0.0, height: size.height)))
|
||||
transition.updateAlpha(node: self.foregroundNode, alpha: 0.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -240,6 +285,17 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
public var enableFineScrubbing: Bool = false {
|
||||
didSet {
|
||||
switch self.contentNodes {
|
||||
case let .standard(node):
|
||||
node.handleNodeContainer?.verticalPanEnabled = self.enableFineScrubbing
|
||||
case let .custom(node):
|
||||
node.handleNodeContainer?.verticalPanEnabled = self.enableFineScrubbing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var _statusValue: MediaPlayerStatus?
|
||||
private var statusValue: MediaPlayerStatus? {
|
||||
get {
|
||||
@ -293,13 +349,13 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
|
||||
|
||||
private static func contentNodesFromContent(_ content: MediaPlayerScrubbingNodeContent, enableScrubbing: Bool) -> MediaPlayerScrubbingNodeContentNodes {
|
||||
switch content {
|
||||
case let .standard(lineHeight, lineCap, scrubberHandle, backgroundColor, foregroundColor):
|
||||
case let .standard(lineHeight, lineCap, scrubberHandle, backgroundColor, foregroundColor, bufferingColor):
|
||||
let backgroundNode = ASImageNode()
|
||||
backgroundNode.isLayerBacked = true
|
||||
backgroundNode.displaysAsynchronously = false
|
||||
backgroundNode.displayWithoutProcessing = true
|
||||
|
||||
let bufferingNode = MediaPlayerScrubbingBufferingNode(color: foregroundColor.withAlphaComponent(0.5), lineCap: lineCap, lineHeight: lineHeight)
|
||||
let bufferingNode = MediaPlayerScrubbingBufferingNode(color: bufferingColor, lineCap: lineCap, lineHeight: lineHeight)
|
||||
|
||||
let foregroundContentNode = ASImageNode()
|
||||
foregroundContentNode.isLayerBacked = true
|
||||
@ -421,7 +477,7 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
|
||||
strongSelf.displayLink?.isPaused = true
|
||||
|
||||
var timestamp = statusValue.timestamp
|
||||
if statusValue.generationTimestamp > 0 {
|
||||
if statusValue.generationTimestamp > 0 && statusValue.status == .playing {
|
||||
let currentTimestamp = CACurrentMediaTime()
|
||||
timestamp = timestamp + (currentTimestamp - statusValue.generationTimestamp) * statusValue.baseRate
|
||||
}
|
||||
@ -437,7 +493,6 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
|
||||
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)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -453,10 +508,11 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
handleNodeContainer.updateScrubbing = { [weak self] addedFraction in
|
||||
handleNodeContainer.updateScrubbing = { [weak self] addedFraction, multiplier in
|
||||
if let strongSelf = self {
|
||||
if let statusValue = strongSelf.statusValue, let scrubbingBeginTimestamp = strongSelf.scrubbingBeginTimestamp, Double(0.0).isLess(than: statusValue.duration) {
|
||||
let timestampValue = max(0.0, min(statusValue.duration, scrubbingBeginTimestamp + statusValue.duration * Double(addedFraction)))
|
||||
let delta: Double = (statusValue.duration * Double(addedFraction)) * multiplier
|
||||
let timestampValue = max(0.0, min(statusValue.duration, scrubbingBeginTimestamp + delta))
|
||||
strongSelf.scrubbingTimestampValue = timestampValue
|
||||
strongSelf._scrubbingTimestamp.set(.single(strongSelf.scrubbingTimestampValue))
|
||||
strongSelf._scrubbingPosition.set(.single(strongSelf.scrubbingTimestampValue.flatMap { $0 / statusValue.duration }))
|
||||
@ -465,6 +521,13 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
handleNodeContainer.updateMultiplier = { [weak self] multiplier in
|
||||
if let strongSelf = self {
|
||||
if let statusValue = strongSelf.statusValue, let scrubbingBeginTimestamp = strongSelf.scrubbingBeginTimestamp, Double(0.0).isLess(than: statusValue.duration) {
|
||||
strongSelf.scrubbingBeginTimestamp = strongSelf.scrubbingTimestampValue
|
||||
}
|
||||
}
|
||||
}
|
||||
handleNodeContainer.endScrubbing = { [weak self] apply in
|
||||
if let strongSelf = self {
|
||||
strongSelf.scrubbingBeginTimestamp = nil
|
||||
@ -513,14 +576,21 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
handleNodeContainer.updateScrubbing = { [weak self] addedFraction in
|
||||
handleNodeContainer.updateScrubbing = { [weak self] addedFraction, multiplier in
|
||||
if let strongSelf = self {
|
||||
if let statusValue = strongSelf.statusValue, let scrubbingBeginTimestamp = strongSelf.scrubbingBeginTimestamp, Double(0.0).isLess(than: statusValue.duration) {
|
||||
strongSelf.scrubbingTimestampValue = scrubbingBeginTimestamp + statusValue.duration * Double(addedFraction)
|
||||
strongSelf.scrubbingTimestampValue = scrubbingBeginTimestamp + (statusValue.duration * Double(addedFraction)) * multiplier
|
||||
strongSelf.updateProgressAnimations()
|
||||
}
|
||||
}
|
||||
}
|
||||
handleNodeContainer.updateMultiplier = { [weak self] multiplier in
|
||||
if let strongSelf = self {
|
||||
if let statusValue = strongSelf.statusValue, let scrubbingBeginTimestamp = strongSelf.scrubbingBeginTimestamp, Double(0.0).isLess(than: statusValue.duration) {
|
||||
strongSelf.scrubbingBeginTimestamp = strongSelf.scrubbingTimestampValue
|
||||
}
|
||||
}
|
||||
}
|
||||
handleNodeContainer.endScrubbing = { [weak self] apply in
|
||||
if let strongSelf = self {
|
||||
strongSelf.scrubbingBeginTimestamp = nil
|
||||
|
||||
@ -35,6 +35,20 @@ private struct MediaPlayerTimeTextNodeState: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
private extension MediaPlayerTimeTextNodeState {
|
||||
var string: String {
|
||||
if let hours = self.hours, let minutes = self.minutes, let seconds = self.seconds {
|
||||
if hours != 0 {
|
||||
return String(format: "%d:%02d:%02d", hours, minutes, seconds)
|
||||
} else {
|
||||
return String(format: "%d:%02d", minutes, seconds)
|
||||
}
|
||||
} else {
|
||||
return "-:--"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class MediaPlayerTimeTextNodeParameters: NSObject {
|
||||
let state: MediaPlayerTimeTextNodeState
|
||||
let alignment: NSTextAlignment
|
||||
@ -160,6 +174,14 @@ public final class MediaPlayerTimeTextNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
private let digitsSet = CharacterSet(charactersIn: "0123456789")
|
||||
private func widthForString(_ string: String) -> CGFloat {
|
||||
let convertedString = string.components(separatedBy: digitsSet).joined(separator: "8")
|
||||
let text = NSAttributedString(string: convertedString, font: textFont, textColor: .black)
|
||||
let size = text.boundingRect(with: CGSize(width: 200.0, height: 100.0), options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil).size
|
||||
return size.width
|
||||
}
|
||||
|
||||
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
|
||||
return MediaPlayerTimeTextNodeParameters(state: self.state, alignment: self.alignment, mode: self.mode, textColor: self.textColor)
|
||||
}
|
||||
@ -174,19 +196,8 @@ public final class MediaPlayerTimeTextNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
if let parameters = parameters as? MediaPlayerTimeTextNodeParameters {
|
||||
let text: String
|
||||
if let hours = parameters.state.hours, let minutes = parameters.state.minutes, let seconds = parameters.state.seconds {
|
||||
if hours != 0 {
|
||||
text = String(format: "%d:%02d:%02d", hours, minutes, seconds)
|
||||
} else {
|
||||
text = String(format: "%d:%02d", minutes, seconds)
|
||||
}
|
||||
} else {
|
||||
text = "-:--"
|
||||
}
|
||||
let string = NSAttributedString(string: text, font: textFont, textColor: parameters.textColor)
|
||||
let string = NSAttributedString(string: parameters.state.string, font: textFont, textColor: parameters.textColor)
|
||||
let size = string.boundingRect(with: CGSize(width: 200.0, height: 100.0), options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil).size
|
||||
|
||||
if parameters.alignment == .left {
|
||||
string.draw(at: CGPoint())
|
||||
} else {
|
||||
|
||||
@ -258,7 +258,7 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
|
||||
self.actionPlayNode.image = PresentationResourcesRootController.navigationPlayerPlayIcon(self.theme)
|
||||
self.actionPlayNode.isHidden = true
|
||||
|
||||
self.scrubbingNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 2.0, lineCap: .square, scrubberHandle: .none, backgroundColor: .clear, foregroundColor: self.theme.rootController.navigationBar.accentTextColor))
|
||||
self.scrubbingNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 2.0, lineCap: .square, scrubberHandle: .none, backgroundColor: .clear, foregroundColor: self.theme.rootController.navigationBar.accentTextColor, bufferingColor: self.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(0.5)))
|
||||
|
||||
self.separatorNode = ASDisplayNode()
|
||||
self.separatorNode.isLayerBacked = true
|
||||
@ -371,7 +371,7 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
|
||||
self.actionPlayNode.image = PresentationResourcesRootController.navigationPlayerPlayIcon(self.theme)
|
||||
self.actionPauseNode.image = PresentationResourcesRootController.navigationPlayerPauseIcon(self.theme)
|
||||
self.separatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor
|
||||
self.scrubbingNode.updateContent(.standard(lineHeight: 2.0, lineCap: .square, scrubberHandle: .none, backgroundColor: .clear, foregroundColor: self.theme.rootController.navigationBar.accentTextColor))
|
||||
self.scrubbingNode.updateContent(.standard(lineHeight: 2.0, lineCap: .square, scrubberHandle: .none, backgroundColor: .clear, foregroundColor: self.theme.rootController.navigationBar.accentTextColor, bufferingColor: self.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(0.5)))
|
||||
|
||||
if let playbackBaseRate = self.playbackBaseRate {
|
||||
switch playbackBaseRate {
|
||||
|
||||
@ -26,15 +26,10 @@ public enum PresentationResourceKey: Int32 {
|
||||
|
||||
case navigationPlayerPlayIcon
|
||||
case navigationPlayerPauseIcon
|
||||
case navigationPlayerMaximizedPlayIcon
|
||||
case navigationPlayerMaximizedPauseIcon
|
||||
case navigationPlayerMaximizedPreviousIcon
|
||||
case navigationPlayerMaximizedNextIcon
|
||||
case navigationPlayerMaximizedShuffleIcon
|
||||
case navigationPlayerMaximizedRepeatIcon
|
||||
case navigationPlayerHandleIcon
|
||||
case navigationPlayerRateActiveIcon
|
||||
case navigationPlayerRateInactiveIcon
|
||||
case navigationPlayerMaximizedRateActiveIcon
|
||||
case navigationPlayerMaximizedRateInactiveIcon
|
||||
|
||||
case itemListDisclosureArrow
|
||||
case itemListCheckIcon
|
||||
|
||||
@ -120,6 +120,18 @@ public struct PresentationResourcesRootController {
|
||||
})
|
||||
}
|
||||
|
||||
public static func navigationPlayerMaximizedRateActiveIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.navigationPlayerMaximizedRateActiveIcon.rawValue, { theme in
|
||||
return generatePlayerRateIcon(theme.list.itemAccentColor)
|
||||
})
|
||||
}
|
||||
|
||||
public static func navigationPlayerMaximizedRateInactiveIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.navigationPlayerMaximizedRateInactiveIcon.rawValue, { theme in
|
||||
return generatePlayerRateIcon(theme.list.itemSecondaryTextColor)
|
||||
})
|
||||
}
|
||||
|
||||
public static func navigationPlayerPauseIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.navigationPlayerPauseIcon.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/MinimizedPause"), color: theme.rootController.navigationBar.accentTextColor)
|
||||
@ -132,48 +144,6 @@ public struct PresentationResourcesRootController {
|
||||
})
|
||||
}
|
||||
|
||||
public static func navigationPlayerMaximizedPlayIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.navigationPlayerMaximizedPlayIcon.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Play"), color: theme.rootController.navigationBar.primaryTextColor)
|
||||
})
|
||||
}
|
||||
|
||||
public static func navigationPlayerMaximizedPauseIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.navigationPlayerMaximizedPauseIcon.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Pause"), color: theme.rootController.navigationBar.primaryTextColor)
|
||||
})
|
||||
}
|
||||
|
||||
public static func navigationPlayerMaximizedPreviousIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.navigationPlayerMaximizedPreviousIcon.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Previous"), color: theme.rootController.navigationBar.primaryTextColor)
|
||||
})
|
||||
}
|
||||
|
||||
public static func navigationPlayerMaximizedNextIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.navigationPlayerMaximizedNextIcon.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Next"), color: theme.rootController.navigationBar.primaryTextColor)
|
||||
})
|
||||
}
|
||||
|
||||
public static func navigationPlayerMaximizedShuffleIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.navigationPlayerMaximizedShuffleIcon.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Shuffle"), color: theme.rootController.navigationBar.primaryTextColor)
|
||||
})
|
||||
}
|
||||
|
||||
public static func navigationPlayerMaximizedRepeatIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.navigationPlayerMaximizedRepeatIcon.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Repeat"), color: theme.rootController.navigationBar.primaryTextColor)
|
||||
})
|
||||
}
|
||||
|
||||
public static func navigationPlayerHandleIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.navigationPlayerHandleIcon.rawValue, { theme in
|
||||
return generateStretchableFilledCircleImage(diameter: 7.0, color: theme.rootController.navigationBar.controlColor)
|
||||
})
|
||||
}
|
||||
|
||||
public static func inAppNotificationBackground(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.inAppNotificationBackground.rawValue, { theme in
|
||||
let inset: CGFloat = 16.0
|
||||
|
||||
@ -2,17 +2,7 @@
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MusicPlayerControlForward@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MusicPlayerControlForward@3x.png",
|
||||
"scale" : "3x"
|
||||
"filename" : "ic_musicnext.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
BIN
submodules/TelegramUI/Images.xcassets/GlobalMusicPlayer/Next.imageset/ic_musicnext.pdf
vendored
Normal file
@ -2,17 +2,7 @@
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MusicPlayerControlShuffle@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MusicPlayerControlShuffle@3x.png",
|
||||
"scale" : "3x"
|
||||
"filename" : "ic_musicshuffle.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
|
Before Width: | Height: | Size: 549 B |
|
Before Width: | Height: | Size: 497 B |
BIN
submodules/TelegramUI/Images.xcassets/GlobalMusicPlayer/OrderRandom.imageset/ic_musicshuffle.pdf
vendored
Normal file
@ -2,17 +2,7 @@
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MusicPlayerControlReverse@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MusicPlayerControlReverse@3x.png",
|
||||
"scale" : "3x"
|
||||
"filename" : "ic_musicflipover.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
|
Before Width: | Height: | Size: 316 B |
|
Before Width: | Height: | Size: 381 B |
BIN
submodules/TelegramUI/Images.xcassets/GlobalMusicPlayer/OrderReverse.imageset/ic_musicflipover.pdf
vendored
Normal file
@ -2,17 +2,7 @@
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MusicPlayerControlPause@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MusicPlayerControlPause@3x.png",
|
||||
"scale" : "3x"
|
||||
"filename" : "ic_musicpause.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
|
Before Width: | Height: | Size: 371 B |
|
Before Width: | Height: | Size: 273 B |
BIN
submodules/TelegramUI/Images.xcassets/GlobalMusicPlayer/Pause.imageset/ic_musicpause.pdf
vendored
Normal file
@ -2,17 +2,7 @@
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MusicPlayerControlPlay@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MusicPlayerControlPlay@3x.png",
|
||||
"scale" : "3x"
|
||||
"filename" : "ic_musicplay.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
|
Before Width: | Height: | Size: 565 B |
|
Before Width: | Height: | Size: 704 B |
BIN
submodules/TelegramUI/Images.xcassets/GlobalMusicPlayer/Play.imageset/ic_musicplay.pdf
vendored
Normal file
@ -2,17 +2,7 @@
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MusicPlayerControlBack@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MusicPlayerControlBack@3x.png",
|
||||
"scale" : "3x"
|
||||
"filename" : "ic_musicprevious.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
|
Before Width: | Height: | Size: 683 B |
|
Before Width: | Height: | Size: 591 B |
BIN
submodules/TelegramUI/Images.xcassets/GlobalMusicPlayer/Previous.imageset/ic_musicprevious.pdf
vendored
Normal file
@ -2,17 +2,7 @@
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MusicPlayerControlRepeat@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MusicPlayerControlRepeat@3x.png",
|
||||
"scale" : "3x"
|
||||
"filename" : "ic_musicrepeat.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
|
Before Width: | Height: | Size: 391 B |
|
Before Width: | Height: | Size: 429 B |
BIN
submodules/TelegramUI/Images.xcassets/GlobalMusicPlayer/Repeat.imageset/ic_musicrepeat.pdf
vendored
Normal file
@ -2,17 +2,7 @@
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MusicPlayerControlRepeatOne@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MusicPlayerControlRepeatOne@3x.png",
|
||||
"scale" : "3x"
|
||||
"filename" : "ic_musicrepeatone.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
|
Before Width: | Height: | Size: 437 B |
|
Before Width: | Height: | Size: 515 B |
BIN
submodules/TelegramUI/Images.xcassets/GlobalMusicPlayer/RepeatOne.imageset/ic_musicrepeatone.pdf
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/GlobalMusicPlayer/Share.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_musicshare.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/GlobalMusicPlayer/Share.imageset/ic_musicshare.pdf
vendored
Normal file
@ -1,22 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MusicPlayerControlShuffle@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MusicPlayerControlShuffle@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 549 B |
|
Before Width: | Height: | Size: 497 B |
@ -402,6 +402,8 @@ final class AuthorizedApplicationContext {
|
||||
}
|
||||
})
|
||||
if !isMuted {
|
||||
if firstMessage.id.peerId == context.account.peerId, !firstMessage.flags.contains(.WasScheduled) {
|
||||
} else {
|
||||
if inAppNotificationSettings.playSounds {
|
||||
serviceSoundManager.playIncomingMessageSound()
|
||||
}
|
||||
@ -410,6 +412,7 @@ final class AuthorizedApplicationContext {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if chatIsVisible {
|
||||
return
|
||||
|
||||
@ -352,6 +352,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
|
||||
if let gestureRecognizers = view.gestureRecognizers, view != self.view {
|
||||
for gestureRecognizer in gestureRecognizers {
|
||||
if let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer, gestureRecognizer.isEnabled {
|
||||
if panGestureRecognizer.state != .began {
|
||||
panGestureRecognizer.isEnabled = false
|
||||
panGestureRecognizer.isEnabled = true
|
||||
}
|
||||
@ -359,6 +360,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@ -21,16 +21,41 @@ private func generateBackground(theme: PresentationTheme) -> UIImage? {
|
||||
})?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 10 + 8)
|
||||
}
|
||||
|
||||
private func generateShareIcon(theme: PresentationTheme) -> UIImage? {
|
||||
return generateImage(CGSize(width: 19.0, height: 5.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(theme.list.itemAccentColor.cgColor)
|
||||
for i in 0 ..< 3 {
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: CGFloat(i) * (5.0 + 2.0), y: 0.0), size: CGSize(width: 5.0, height: 5.0)))
|
||||
}
|
||||
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)
|
||||
|
||||
@ -112,6 +137,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
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)?
|
||||
|
||||
@ -128,7 +154,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
|
||||
self.collapseNode = HighlightableButtonNode()
|
||||
self.collapseNode.displaysAsynchronously = false
|
||||
self.collapseNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/CollapseArrow"), color: theme.list.controlSecondaryColor), for: [])
|
||||
self.collapseNode.setImage(generateCollapseIcon(theme: theme), for: [])
|
||||
|
||||
self.albumArtNode = TransformImageNode()
|
||||
|
||||
@ -141,9 +167,9 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
self.descriptionNode.displaysAsynchronously = false
|
||||
|
||||
self.shareNode = HighlightableButtonNode()
|
||||
self.shareNode.setImage(generateShareIcon(theme: theme), for: [])
|
||||
self.shareNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Share"), color: theme.list.itemAccentColor), for: [])
|
||||
|
||||
self.scrubberNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 3.0, lineCap: .round, scrubberHandle: .circle, backgroundColor: theme.list.controlSecondaryColor, foregroundColor: theme.list.itemAccentColor))
|
||||
self.scrubberNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 3.0, lineCap: .round, scrubberHandle: .circle, backgroundColor: theme.list.controlSecondaryColor, foregroundColor: theme.list.itemAccentColor, bufferingColor: theme.list.itemAccentColor.withAlphaComponent(0.4)))
|
||||
self.leftDurationLabel = MediaPlayerTimeTextNode(textColor: theme.list.itemSecondaryTextColor)
|
||||
self.leftDurationLabel.displaysAsynchronously = false
|
||||
self.rightDurationLabel = MediaPlayerTimeTextNode(textColor: theme.list.itemSecondaryTextColor)
|
||||
@ -264,6 +290,8 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
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 {
|
||||
@ -310,10 +338,22 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
if let displayData = displayData, case let .music(_, _, _, long) = displayData, long {
|
||||
strongSelf.rateButton.isHidden = false
|
||||
strongSelf.scrubberNode.enableFineScrubbing = true
|
||||
rateButtonIsHidden = false
|
||||
} else {
|
||||
strongSelf.rateButton.isHidden = true
|
||||
strongSelf.scrubberNode.enableFineScrubbing = false
|
||||
rateButtonIsHidden = true
|
||||
}
|
||||
|
||||
let duration = value.status.duration
|
||||
if duration != strongSelf.currentDuration {
|
||||
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 && strongSelf.currentDuration.isZero
|
||||
} else {
|
||||
strongSelf.playPauseButton.isEnabled = false
|
||||
strongSelf.backwardButton.isEnabled = false
|
||||
@ -377,8 +417,8 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
self.theme = theme
|
||||
|
||||
self.backgroundNode.image = generateBackground(theme: theme)
|
||||
self.collapseNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/CollapseArrow"), color: theme.list.controlSecondaryColor), for: [])
|
||||
self.shareNode.setImage(generateShareIcon(theme: theme), for: [])
|
||||
self.collapseNode.setImage(generateCollapseIcon(theme: theme), for: [])
|
||||
self.shareNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Share"), color: theme.list.itemAccentColor), for: [])
|
||||
self.scrubberNode.updateColors(backgroundColor: theme.list.controlSecondaryColor, foregroundColor: theme.list.itemAccentColor)
|
||||
self.leftDurationLabel.textColor = theme.list.itemSecondaryTextColor
|
||||
self.rightDurationLabel.textColor = theme.list.itemSecondaryTextColor
|
||||
@ -479,9 +519,9 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
private func updateRateButton(_ baseRate: AudioPlaybackRate) {
|
||||
switch baseRate {
|
||||
case .x2:
|
||||
self.rateButton.setImage(PresentationResourcesRootController.navigationPlayerRateActiveIcon(self.theme), for: [])
|
||||
self.rateButton.setImage(PresentationResourcesRootController.navigationPlayerMaximizedRateActiveIcon(self.theme), for: [])
|
||||
default:
|
||||
self.rateButton.setImage(PresentationResourcesRootController.navigationPlayerRateInactiveIcon(self.theme), for: [])
|
||||
self.rateButton.setImage(PresentationResourcesRootController.navigationPlayerMaximizedRateInactiveIcon(self.theme), for: [])
|
||||
}
|
||||
}
|
||||
|
||||
@ -506,7 +546,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
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 + 30.0
|
||||
let sideButtonsInset: CGFloat = sideInset + 36.0
|
||||
|
||||
let infoVerticalOrigin: CGFloat = panelHeight - OverlayPlayerControlsNode.basePanelHeight + 36.0
|
||||
|
||||
@ -598,7 +638,8 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
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)))
|
||||
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)))
|
||||
|
||||
|
||||
@ -297,9 +297,9 @@ private final class WalletQrScanScreenNode: ViewControllerTracingNode, UIScrollV
|
||||
|
||||
if case .tablet = layout.deviceMetrics.type {
|
||||
if UIDevice.current.orientation == .landscapeLeft {
|
||||
self.previewNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
} else if UIDevice.current.orientation == .landscapeRight {
|
||||
self.previewNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
} else if UIDevice.current.orientation == .landscapeRight {
|
||||
self.previewNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
} else {
|
||||
self.previewNode.transform = CATransform3DIdentity
|
||||
}
|
||||
|
||||
@ -129,7 +129,7 @@ private final class WalletQrViewScreenNode: ViewControllerTracingNode {
|
||||
|
||||
self.iconNode = AnimatedStickerNode()
|
||||
if let path = getAppBundle().path(forResource: "WalletIntroStatic", ofType: "tgs") {
|
||||
self.iconNode.setup(account: context.account, resource: .localFile(path), width: 120, height: 120, mode: .direct)
|
||||
self.iconNode.setup(account: context.account, resource: .localFile(path), width: 240, height: 240, mode: .direct)
|
||||
self.iconNode.visibility = true
|
||||
}
|
||||
|
||||
|
||||