Initial commit: SimVision tvOS streaming app
Features: - VOD library with movie grouping and version detection - TV show library with season/episode organization - TMDB integration for trending shows and recently aired episodes - Recent releases section with TMDB release date sorting - Watch history tracking with continue watching - Playlist caching (12-hour TTL) for offline support - M3U playlist parsing with XStream API support - Authentication with credential storage Technical: - SwiftUI for tvOS - Actor-based services for thread safety - Persistent caching for playlists, TMDB data, and watch history - KSPlayer integration for video playback Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
282
KSPlayer-main/Sources/KSPlayer/Core/PlayerToolBar.swift
Normal file
282
KSPlayer-main/Sources/KSPlayer/Core/PlayerToolBar.swift
Normal file
@@ -0,0 +1,282 @@
|
||||
//
|
||||
// PlayerToolBar.swift
|
||||
// Pods
|
||||
//
|
||||
// Created by kintan on 16/5/21.
|
||||
//
|
||||
//
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
import AVKit
|
||||
|
||||
public class PlayerToolBar: UIStackView {
|
||||
public let srtButton = UIButton()
|
||||
public let timeLabel = UILabel()
|
||||
public let currentTimeLabel = UILabel()
|
||||
public let totalTimeLabel = UILabel()
|
||||
public let playButton = UIButton()
|
||||
public let timeSlider = KSSlider()
|
||||
public let playbackRateButton = UIButton()
|
||||
public let videoSwitchButton = UIButton()
|
||||
public let audioSwitchButton = UIButton()
|
||||
public let definitionButton = UIButton()
|
||||
public let pipButton = UIButton()
|
||||
public var onFocusUpdate: ((_ cofusedItem: UIView) -> Void)?
|
||||
public var timeType = TimeType.minOrHour {
|
||||
didSet {
|
||||
if timeType != oldValue {
|
||||
let currentTimeText = currentTime.toString(for: timeType)
|
||||
let totalTimeText = totalTime.toString(for: timeType)
|
||||
currentTimeLabel.text = currentTimeText
|
||||
totalTimeLabel.text = totalTimeText
|
||||
timeLabel.text = "\(currentTimeText) / \(totalTimeText)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var currentTime: TimeInterval = 0 {
|
||||
didSet {
|
||||
guard !currentTime.isNaN else {
|
||||
currentTime = 0
|
||||
return
|
||||
}
|
||||
if currentTime != oldValue {
|
||||
let text = currentTime.toString(for: timeType)
|
||||
currentTimeLabel.text = text
|
||||
timeLabel.text = "\(text) / \(totalTime.toString(for: timeType))"
|
||||
if isLiveStream {
|
||||
timeSlider.value = Float(todayInterval)
|
||||
} else {
|
||||
timeSlider.value = Float(currentTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lazy var startDateTimeInteral: TimeInterval = {
|
||||
let date = Date()
|
||||
let calendar = Calendar.current
|
||||
let components = calendar.dateComponents([.year, .month, .day], from: date)
|
||||
let startDate = calendar.date(from: components)
|
||||
return startDate?.timeIntervalSince1970 ?? 0
|
||||
}()
|
||||
|
||||
var todayInterval: TimeInterval {
|
||||
Date().timeIntervalSince1970 - startDateTimeInteral
|
||||
}
|
||||
|
||||
public var totalTime: TimeInterval = 0 {
|
||||
didSet {
|
||||
guard !totalTime.isNaN else {
|
||||
totalTime = 0
|
||||
return
|
||||
}
|
||||
if totalTime != oldValue {
|
||||
let text = totalTime.toString(for: timeType)
|
||||
totalTimeLabel.text = text
|
||||
timeLabel.text = "\(currentTime.toString(for: timeType)) / \(text)"
|
||||
timeSlider.maximumValue = Float(totalTime)
|
||||
}
|
||||
if isLiveStream {
|
||||
timeSlider.maximumValue = Float(60 * 60 * 24)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var isLiveStream: Bool {
|
||||
totalTime == 0
|
||||
}
|
||||
|
||||
public var isSeekable: Bool = true {
|
||||
didSet {
|
||||
timeSlider.isUserInteractionEnabled = isSeekable
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
initUI()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func initUI() {
|
||||
let focusColor = UIColor.white
|
||||
let tintColor = UIColor.gray
|
||||
distribution = .fill
|
||||
currentTimeLabel.textColor = UIColor(rgb: 0x9B9B9B)
|
||||
currentTimeLabel.font = UIFont.monospacedDigitSystemFont(ofSize: 14, weight: .regular)
|
||||
currentTimeLabel.text = 0.toString(for: timeType)
|
||||
totalTimeLabel.textColor = UIColor(rgb: 0x9B9B9B)
|
||||
totalTimeLabel.font = UIFont.monospacedDigitSystemFont(ofSize: 14, weight: .regular)
|
||||
totalTimeLabel.text = 0.toString(for: timeType)
|
||||
|
||||
timeLabel.textColor = UIColor(rgb: 0x9B9B9B)
|
||||
timeLabel.textAlignment = .left
|
||||
timeLabel.font = UIFont.monospacedDigitSystemFont(ofSize: 14, weight: .regular)
|
||||
timeLabel.text = "\(0.toString(for: timeType)) / \(0.toString(for: timeType))"
|
||||
timeSlider.minimumValue = 0
|
||||
#if os(iOS)
|
||||
if #available(macCatalyst 15.0, iOS 15.0, *) {
|
||||
timeSlider.preferredBehavioralStyle = .pad
|
||||
timeSlider.maximumTrackTintColor = focusColor.withAlphaComponent(0.2)
|
||||
timeSlider.minimumTrackTintColor = focusColor
|
||||
}
|
||||
#endif
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
timeSlider.maximumTrackTintColor = focusColor.withAlphaComponent(0.2)
|
||||
timeSlider.minimumTrackTintColor = focusColor
|
||||
#endif
|
||||
playButton.tag = PlayerButtonType.play.rawValue
|
||||
playButton.setTitleColor(focusColor, for: .focused)
|
||||
playButton.setTitleColor(tintColor, for: .normal)
|
||||
playbackRateButton.tag = PlayerButtonType.rate.rawValue
|
||||
playbackRateButton.titleFont = .systemFont(ofSize: 14, weight: .medium)
|
||||
playbackRateButton.setTitleColor(focusColor, for: .focused)
|
||||
playbackRateButton.setTitleColor(tintColor, for: .normal)
|
||||
definitionButton.tag = PlayerButtonType.definition.rawValue
|
||||
definitionButton.titleFont = .systemFont(ofSize: 14, weight: .medium)
|
||||
definitionButton.setTitleColor(focusColor, for: .focused)
|
||||
definitionButton.setTitleColor(tintColor, for: .normal)
|
||||
audioSwitchButton.tag = PlayerButtonType.audioSwitch.rawValue
|
||||
audioSwitchButton.titleFont = .systemFont(ofSize: 14, weight: .medium)
|
||||
audioSwitchButton.setTitleColor(focusColor, for: .focused)
|
||||
audioSwitchButton.setTitleColor(tintColor, for: .normal)
|
||||
videoSwitchButton.tag = PlayerButtonType.videoSwitch.rawValue
|
||||
videoSwitchButton.titleFont = .systemFont(ofSize: 14, weight: .medium)
|
||||
videoSwitchButton.setTitleColor(focusColor, for: .focused)
|
||||
videoSwitchButton.setTitleColor(tintColor, for: .normal)
|
||||
srtButton.tag = PlayerButtonType.srt.rawValue
|
||||
srtButton.titleFont = .systemFont(ofSize: 14, weight: .medium)
|
||||
srtButton.setTitleColor(focusColor, for: .focused)
|
||||
srtButton.setTitleColor(tintColor, for: .normal)
|
||||
pipButton.tag = PlayerButtonType.pictureInPicture.rawValue
|
||||
pipButton.titleFont = .systemFont(ofSize: 14, weight: .medium)
|
||||
pipButton.setTitleColor(focusColor, for: .focused)
|
||||
pipButton.setTitleColor(tintColor, for: .normal)
|
||||
if #available(macOS 11.0, *) {
|
||||
pipButton.setImage(UIImage(systemName: "pip.enter"), for: .normal)
|
||||
pipButton.setImage(UIImage(systemName: "pip.exit"), for: .selected)
|
||||
playButton.setImage(UIImage(systemName: "play.fill"), for: .normal)
|
||||
playButton.setImage(UIImage(systemName: "pause.fill"), for: .selected)
|
||||
srtButton.setImage(UIImage(systemName: "captions.bubble"), for: .normal)
|
||||
definitionButton.setImage(UIImage(systemName: "arrow.up.right.video"), for: .normal)
|
||||
audioSwitchButton.setImage(UIImage(systemName: "waveform"), for: .normal)
|
||||
videoSwitchButton.setImage(UIImage(systemName: "video.badge.ellipsis"), for: .normal)
|
||||
playbackRateButton.setImage(UIImage(systemName: "speedometer"), for: .normal)
|
||||
}
|
||||
playButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
srtButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
if #available(tvOS 14.0, *) {
|
||||
pipButton.isHidden = !AVPictureInPictureController.isPictureInPictureSupported()
|
||||
}
|
||||
#if os(tvOS)
|
||||
srtButton.fillImage()
|
||||
pipButton.fillImage()
|
||||
playButton.fillImage()
|
||||
definitionButton.fillImage()
|
||||
audioSwitchButton.fillImage()
|
||||
videoSwitchButton.fillImage()
|
||||
playbackRateButton.fillImage()
|
||||
playButton.tintColor = tintColor
|
||||
playbackRateButton.tintColor = tintColor
|
||||
definitionButton.tintColor = tintColor
|
||||
audioSwitchButton.tintColor = tintColor
|
||||
videoSwitchButton.tintColor = tintColor
|
||||
srtButton.tintColor = tintColor
|
||||
pipButton.tintColor = tintColor
|
||||
timeSlider.tintColor = tintColor
|
||||
NSLayoutConstraint.activate([
|
||||
playButton.widthAnchor.constraint(equalTo: playButton.heightAnchor),
|
||||
playbackRateButton.widthAnchor.constraint(equalTo: playbackRateButton.heightAnchor),
|
||||
definitionButton.widthAnchor.constraint(equalTo: definitionButton.heightAnchor),
|
||||
audioSwitchButton.widthAnchor.constraint(equalTo: audioSwitchButton.heightAnchor),
|
||||
videoSwitchButton.widthAnchor.constraint(equalTo: videoSwitchButton.heightAnchor),
|
||||
srtButton.widthAnchor.constraint(equalTo: srtButton.heightAnchor),
|
||||
pipButton.widthAnchor.constraint(equalTo: pipButton.heightAnchor),
|
||||
heightAnchor.constraint(equalToConstant: 40),
|
||||
])
|
||||
#else
|
||||
timeSlider.tintColor = .white
|
||||
playButton.tintColor = .white
|
||||
playbackRateButton.tintColor = .white
|
||||
definitionButton.tintColor = .white
|
||||
audioSwitchButton.tintColor = .white
|
||||
videoSwitchButton.tintColor = .white
|
||||
srtButton.tintColor = .white
|
||||
pipButton.tintColor = .white
|
||||
NSLayoutConstraint.activate([
|
||||
playButton.widthAnchor.constraint(equalToConstant: 30),
|
||||
heightAnchor.constraint(equalToConstant: 49),
|
||||
srtButton.widthAnchor.constraint(equalToConstant: 40),
|
||||
])
|
||||
#endif
|
||||
}
|
||||
|
||||
override public func addArrangedSubview(_ view: UIView) {
|
||||
super.addArrangedSubview(view)
|
||||
view.isHidden = false
|
||||
}
|
||||
|
||||
#if canImport(UIKit)
|
||||
override open func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
|
||||
super.didUpdateFocus(in: context, with: coordinator)
|
||||
if let nextFocusedItem = context.nextFocusedItem {
|
||||
if let nextFocusedButton = nextFocusedItem as? UIButton {
|
||||
nextFocusedButton.tintColor = nextFocusedButton.titleColor(for: .focused)
|
||||
}
|
||||
if context.previouslyFocusedItem != nil,
|
||||
let nextFocusedView = nextFocusedItem as? UIView
|
||||
{
|
||||
onFocusUpdate?(nextFocusedView)
|
||||
}
|
||||
}
|
||||
if let previouslyFocusedItem = context.previouslyFocusedItem as? UIButton {
|
||||
if previouslyFocusedItem.isSelected {
|
||||
previouslyFocusedItem.tintColor = previouslyFocusedItem.titleColor(for: .selected)
|
||||
} else if previouslyFocusedItem.isHighlighted {
|
||||
previouslyFocusedItem.tintColor = previouslyFocusedItem.titleColor(for: .highlighted)
|
||||
} else {
|
||||
previouslyFocusedItem.tintColor = previouslyFocusedItem.titleColor(for: .normal)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
open func addTarget(_ target: AnyObject?, action: Selector) {
|
||||
playButton.addTarget(target, action: action, for: .primaryActionTriggered)
|
||||
playbackRateButton.addTarget(target, action: action, for: .primaryActionTriggered)
|
||||
definitionButton.addTarget(target, action: action, for: .primaryActionTriggered)
|
||||
audioSwitchButton.addTarget(target, action: action, for: .primaryActionTriggered)
|
||||
videoSwitchButton.addTarget(target, action: action, for: .primaryActionTriggered)
|
||||
srtButton.addTarget(target, action: action, for: .primaryActionTriggered)
|
||||
pipButton.addTarget(target, action: action, for: .primaryActionTriggered)
|
||||
}
|
||||
|
||||
public func reset() {
|
||||
currentTime = 0
|
||||
totalTime = 0
|
||||
playButton.isSelected = false
|
||||
timeSlider.value = 0.0
|
||||
timeSlider.isPlayable = false
|
||||
playbackRateButton.setTitle(NSLocalizedString("speed", comment: ""), for: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
extension KSOptions {
|
||||
static func image(named: String) -> UIImage? {
|
||||
#if canImport(UIKit)
|
||||
return UIImage(named: named, in: .module, compatibleWith: nil)
|
||||
#else
|
||||
return Bundle.module.image(forResource: named)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user