import Foundation import AsyncDisplayKit import SwiftSignalKit import Display private let textFont = Font.regular(13.0) public enum MediaPlayerTimeTextNodeMode { case normal case reversed } private struct MediaPlayerTimeTextNodeState: Equatable { let hours: Int32? let minutes: Int32? let seconds: Int32? init() { self.hours = nil self.minutes = nil self.seconds = nil } init(hours: Int32, minutes: Int32, seconds: Int32) { self.hours = hours self.minutes = minutes self.seconds = seconds } static func ==(lhs: MediaPlayerTimeTextNodeState, rhs: MediaPlayerTimeTextNodeState) -> Bool { if lhs.hours != rhs.hours || lhs.minutes != rhs.minutes || lhs.seconds != rhs.seconds { return false } return true } } 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 let mode: MediaPlayerTimeTextNodeMode let textColor: UIColor init(state: MediaPlayerTimeTextNodeState, alignment: NSTextAlignment, mode: MediaPlayerTimeTextNodeMode, textColor: UIColor) { self.state = state self.alignment = alignment self.mode = mode self.textColor = textColor super.init() } } public final class MediaPlayerTimeTextNode: ASDisplayNode { public var alignment: NSTextAlignment = .left public var mode: MediaPlayerTimeTextNodeMode = .normal public var keepPreviousValueOnEmptyState = false public var textColor: UIColor { didSet { self.updateTimestamp() } } public var defaultDuration: Double? { didSet { self.updateTimestamp() } } private var updateTimer: SwiftSignalKit.Timer? private var statusValue: MediaPlayerStatus? { didSet { if self.statusValue != oldValue { if let statusValue = statusValue, case .playing = statusValue.status { self.ensureHasTimer() } else { self.stopTimer() } self.updateTimestamp() } } } private var state = MediaPlayerTimeTextNodeState() { didSet { if self.state != oldValue { self.setNeedsDisplay() } } } private var statusDisposable: Disposable? private var statusValuePromise = Promise() public var status: Signal? { didSet { if let status = self.status { self.statusValuePromise.set(status) } else { self.statusValuePromise.set(.never()) } } } public init(textColor: UIColor) { self.textColor = textColor super.init() self.isOpaque = false self.statusDisposable = (self.statusValuePromise.get() |> deliverOnMainQueue).start(next: { [weak self] status in if let strongSelf = self { strongSelf.statusValue = status } }) } deinit { self.statusDisposable?.dispose() self.updateTimer?.invalidate() } private func ensureHasTimer() { if self.updateTimer == nil { let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in self?.updateTimestamp() }, queue: Queue.mainQueue()) self.updateTimer = timer timer.start() } } private func stopTimer() { self.updateTimer?.invalidate() self.updateTimer = nil } func updateTimestamp() { if ((self.statusValue?.duration ?? 0.0) < 0.1) && self.state.seconds != nil && self.keepPreviousValueOnEmptyState { return } if let statusValue = self.statusValue, Double(0.0).isLess(than: statusValue.duration) { let timestampSeconds: Double if !statusValue.generationTimestamp.isZero { timestampSeconds = statusValue.timestamp + (CACurrentMediaTime() - statusValue.generationTimestamp) } else { timestampSeconds = statusValue.timestamp } switch self.mode { case .normal: let timestamp = Int32(truncatingIfNeeded: Int64(floor(timestampSeconds))) self.state = MediaPlayerTimeTextNodeState(hours: timestamp / (60 * 60), minutes: timestamp % (60 * 60) / 60, seconds: timestamp % 60) case .reversed: let timestamp = abs(Int32(Int32(truncatingIfNeeded: Int64(floor(timestampSeconds - statusValue.duration))))) self.state = MediaPlayerTimeTextNodeState(hours: timestamp / (60 * 60), minutes: timestamp % (60 * 60) / 60, seconds: timestamp % 60) } } else if let defaultDuration = self.defaultDuration { let timestamp = Int32(defaultDuration) self.state = MediaPlayerTimeTextNodeState(hours: timestamp / (60 * 60), minutes: timestamp % (60 * 60) / 60, seconds: timestamp % 60) } else { self.state = MediaPlayerTimeTextNodeState() } } 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) } @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { let context = UIGraphicsGetCurrentContext()! if !isRasterizing { context.setBlendMode(.copy) context.setFillColor(UIColor.clear.cgColor) context.fill(bounds) } if let parameters = parameters as? MediaPlayerTimeTextNodeParameters { 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 { string.draw(at: CGPoint(x: bounds.size.width - size.width, y: 0.0)) } } } }