Files
simvision/KSPlayer-main/Sources/KSPlayer/Core/UIKitExtend.swift
Michael Simard 872354b834 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>
2026-01-21 22:12:08 -06:00

211 lines
6.1 KiB
Swift

//
// 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