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:
210
KSPlayer-main/Sources/KSPlayer/Core/UIKitExtend.swift
Normal file
210
KSPlayer-main/Sources/KSPlayer/Core/UIKitExtend.swift
Normal file
@@ -0,0 +1,210 @@
|
||||
//
|
||||
// File.swift
|
||||
// KSPlayer
|
||||
//
|
||||
// Created by kintan on 2018/3/9.
|
||||
//
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
|
||||
public class KSSlider: UXSlider {
|
||||
private var tapGesture: UITapGestureRecognizer!
|
||||
private var panGesture: UIPanGestureRecognizer!
|
||||
weak var delegate: KSSliderDelegate?
|
||||
public var trackHeigt = CGFloat(2)
|
||||
public var isPlayable = false
|
||||
override public init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
tapGesture = UITapGestureRecognizer(target: self, action: #selector(actionTapGesture(sender:)))
|
||||
panGesture = UIPanGestureRecognizer(target: self, action: #selector(actionPanGesture(sender:)))
|
||||
addGestureRecognizer(tapGesture)
|
||||
addGestureRecognizer(panGesture)
|
||||
addTarget(self, action: #selector(progressSliderTouchBegan(_:)), for: .touchDown)
|
||||
addTarget(self, action: #selector(progressSliderValueChanged(_:)), for: .valueChanged)
|
||||
addTarget(self, action: #selector(progressSliderTouchEnded(_:)), for: [.touchUpInside, .touchCancel, .touchUpOutside, .primaryActionTriggered])
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
public required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override open func trackRect(forBounds bounds: CGRect) -> CGRect {
|
||||
var customBounds = super.trackRect(forBounds: bounds)
|
||||
customBounds.origin.y -= trackHeigt / 2
|
||||
customBounds.size.height = trackHeigt
|
||||
return customBounds
|
||||
}
|
||||
|
||||
override open func thumbRect(forBounds bounds: CGRect, trackRect rect: CGRect, value: Float) -> CGRect {
|
||||
let rect = super.thumbRect(forBounds: bounds, trackRect: rect, value: value)
|
||||
return rect.insetBy(dx: -20, dy: -20)
|
||||
}
|
||||
|
||||
// MARK: - handle UI slider actions
|
||||
|
||||
@objc private func progressSliderTouchBegan(_ sender: KSSlider) {
|
||||
guard isPlayable else { return }
|
||||
tapGesture.isEnabled = false
|
||||
panGesture.isEnabled = false
|
||||
value = value
|
||||
delegate?.slider(value: Double(sender.value), event: .touchDown)
|
||||
}
|
||||
|
||||
@objc private func progressSliderValueChanged(_ sender: KSSlider) {
|
||||
guard isPlayable else { return }
|
||||
delegate?.slider(value: Double(sender.value), event: .valueChanged)
|
||||
}
|
||||
|
||||
@objc private func progressSliderTouchEnded(_ sender: KSSlider) {
|
||||
guard isPlayable else { return }
|
||||
tapGesture.isEnabled = true
|
||||
panGesture.isEnabled = true
|
||||
delegate?.slider(value: Double(sender.value), event: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc private func actionTapGesture(sender: UITapGestureRecognizer) {
|
||||
// guard isPlayable else {
|
||||
// return
|
||||
// }
|
||||
let touchPoint = sender.location(in: self)
|
||||
let value = (maximumValue - minimumValue) * Float(touchPoint.x / frame.size.width)
|
||||
self.value = value
|
||||
delegate?.slider(value: Double(value), event: .valueChanged)
|
||||
delegate?.slider(value: Double(value), event: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc private func actionPanGesture(sender: UIPanGestureRecognizer) {
|
||||
// guard isPlayable else {
|
||||
// return
|
||||
// }
|
||||
let touchPoint = sender.location(in: self)
|
||||
let value = (maximumValue - minimumValue) * Float(touchPoint.x / frame.size.width)
|
||||
self.value = value
|
||||
if sender.state == .began {
|
||||
delegate?.slider(value: Double(value), event: .touchDown)
|
||||
} else if sender.state == .ended {
|
||||
delegate?.slider(value: Double(value), event: .touchUpInside)
|
||||
} else {
|
||||
delegate?.slider(value: Double(value), event: .valueChanged)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if os(tvOS)
|
||||
public class UXSlider: UIProgressView {
|
||||
@IBInspectable public var value: Float {
|
||||
get {
|
||||
progress * maximumValue
|
||||
}
|
||||
set {
|
||||
progress = newValue / maximumValue
|
||||
}
|
||||
}
|
||||
|
||||
@IBInspectable public var maximumValue: Float = 1 {
|
||||
didSet {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
@IBInspectable public var minimumValue: Float = 0 {
|
||||
didSet {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
open var minimumTrackTintColor: UIColor? {
|
||||
get {
|
||||
progressTintColor
|
||||
}
|
||||
set {
|
||||
progressTintColor = newValue
|
||||
}
|
||||
}
|
||||
|
||||
open var maximumTrackTintColor: UIColor? {
|
||||
get {
|
||||
trackTintColor
|
||||
}
|
||||
set {
|
||||
trackTintColor = newValue
|
||||
}
|
||||
}
|
||||
|
||||
open func setThumbImage(_: UIImage?, for _: UIControl.State) {}
|
||||
open func addTarget(_: Any?, action _: Selector, for _: UIControl.Event) {}
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setup()
|
||||
}
|
||||
|
||||
public required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
setup()
|
||||
}
|
||||
|
||||
// MARK: - private functions
|
||||
|
||||
private func setup() {
|
||||
refresh()
|
||||
}
|
||||
|
||||
private func refresh() {}
|
||||
open func trackRect(forBounds bounds: CGRect) -> CGRect {
|
||||
bounds
|
||||
}
|
||||
|
||||
open func thumbRect(forBounds bounds: CGRect, trackRect _: CGRect, value _: Float) -> CGRect {
|
||||
bounds
|
||||
}
|
||||
}
|
||||
#else
|
||||
public typealias UXSlider = UISlider
|
||||
#endif
|
||||
|
||||
public typealias UIViewContentMode = UIView.ContentMode
|
||||
extension UIButton {
|
||||
func fillImage() {
|
||||
contentMode = .scaleAspectFill
|
||||
contentHorizontalAlignment = .fill
|
||||
contentVerticalAlignment = .fill
|
||||
}
|
||||
|
||||
var titleFont: UIFont? {
|
||||
get {
|
||||
titleLabel?.font
|
||||
}
|
||||
set {
|
||||
titleLabel?.font = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var title: String? {
|
||||
get {
|
||||
titleLabel?.text
|
||||
}
|
||||
set {
|
||||
titleLabel?.text = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UIView {
|
||||
func image() -> UIImage? {
|
||||
UIGraphicsBeginImageContextWithOptions(bounds.size, isOpaque, 0.0)
|
||||
defer { UIGraphicsEndImageContext() }
|
||||
if let context = UIGraphicsGetCurrentContext() {
|
||||
layer.render(in: context)
|
||||
let image = UIGraphicsGetImageFromCurrentImageContext()
|
||||
return image
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func centerRotate(byDegrees: Double) {
|
||||
transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi * byDegrees / 180.0))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user