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>
211 lines
6.1 KiB
Swift
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
|