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:
209
KSPlayer-main/Sources/KSPlayer/Video/MacVideoPlayerView.swift
Normal file
209
KSPlayer-main/Sources/KSPlayer/Video/MacVideoPlayerView.swift
Normal file
@@ -0,0 +1,209 @@
|
||||
//
|
||||
// MacVideoPlayerView.swift
|
||||
// Pods
|
||||
//
|
||||
// Created by kintan on 2018/10/31.
|
||||
//
|
||||
#if !canImport(UIKit)
|
||||
|
||||
import AppKit
|
||||
import AVFoundation
|
||||
|
||||
public extension NSPasteboard.PasteboardType {
|
||||
static let nsURL = NSPasteboard.PasteboardType("NSURL")
|
||||
static let nsFilenames = NSPasteboard.PasteboardType("NSFilenamesPboardType")
|
||||
}
|
||||
|
||||
public extension NSDraggingInfo {
|
||||
@MainActor
|
||||
func getUrl() -> URL? {
|
||||
guard let types = draggingPasteboard.types else { return nil }
|
||||
|
||||
if types.contains(.nsFilenames) {
|
||||
guard let paths = draggingPasteboard.propertyList(forType: .nsFilenames) as? [String] else { return nil }
|
||||
let urls = paths.map { URL(fileURLWithPath: $0) }
|
||||
return urls.first
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
open class MacVideoPlayerView: VideoPlayerView {
|
||||
override open func customizeUIComponents() {
|
||||
super.customizeUIComponents()
|
||||
registerForDraggedTypes([.nsFilenames, .nsURL, .string])
|
||||
}
|
||||
|
||||
override open func player(layer: KSPlayerLayer, state: KSPlayerState) {
|
||||
super.player(layer: layer, state: state)
|
||||
if state == .readyToPlay {
|
||||
let naturalSize = layer.player.naturalSize
|
||||
window?.aspectRatio = naturalSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MacVideoPlayerView {
|
||||
override open func updateTrackingAreas() {
|
||||
for trackingArea in trackingAreas {
|
||||
removeTrackingArea(trackingArea)
|
||||
}
|
||||
let trackingArea = NSTrackingArea(rect: bounds, options: [.mouseEnteredAndExited, .mouseMoved, .activeInKeyWindow], owner: self, userInfo: nil)
|
||||
addTrackingArea(trackingArea)
|
||||
}
|
||||
|
||||
override open func mouseEntered(with _: NSEvent) {
|
||||
isMaskShow = true
|
||||
}
|
||||
|
||||
override open func mouseMoved(with _: NSEvent) {
|
||||
isMaskShow = true
|
||||
}
|
||||
|
||||
override open func mouseExited(with _: NSEvent) {
|
||||
isMaskShow = false
|
||||
}
|
||||
|
||||
override open func scrollWheel(with event: NSEvent) {
|
||||
if event.phase.contains(.began) {
|
||||
if event.scrollingDeltaX != 0 {
|
||||
scrollDirection = .horizontal
|
||||
tmpPanValue = toolBar.timeSlider.value
|
||||
} else if event.scrollingDeltaY != 0 {
|
||||
scrollDirection = .vertical
|
||||
tmpPanValue = 1
|
||||
}
|
||||
} else if event.phase.contains(.changed) {
|
||||
let delta = scrollDirection == .horizontal ? event.scrollingDeltaX : event.scrollingDeltaY
|
||||
if scrollDirection == .horizontal {
|
||||
tmpPanValue += Float(delta / 10000) * Float(totalTime)
|
||||
showSeekToView(second: Double(tmpPanValue), isAdd: delta > 0)
|
||||
} else {
|
||||
if KSOptions.enableVolumeGestures {
|
||||
tmpPanValue -= Float(delta / 1000)
|
||||
tmpPanValue = max(min(tmpPanValue, 1), 0)
|
||||
}
|
||||
}
|
||||
} else if event.phase.contains(.ended) {
|
||||
if scrollDirection == .horizontal {
|
||||
slider(value: Double(tmpPanValue), event: .touchUpInside)
|
||||
hideSeekToView()
|
||||
} else {
|
||||
if KSOptions.enableVolumeGestures {
|
||||
playerLayer?.player.playbackVolume = tmpPanValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override open var acceptsFirstResponder: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
override open func keyDown(with event: NSEvent) {
|
||||
if let specialKey = event.specialKey {
|
||||
if specialKey == .rightArrow {
|
||||
slider(value: Double(toolBar.timeSlider.value) + 0.01 * totalTime, event: .touchUpInside)
|
||||
} else if specialKey == .leftArrow {
|
||||
slider(value: Double(toolBar.timeSlider.value) - 0.01 * totalTime, event: .touchUpInside)
|
||||
}
|
||||
} else if let character = event.characters?.first {
|
||||
if character == " " {
|
||||
onButtonPressed(toolBar.playButton)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override open func draggingEntered(_: NSDraggingInfo) -> NSDragOperation {
|
||||
.copy
|
||||
}
|
||||
|
||||
override open func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
|
||||
if let url = sender.getUrl() {
|
||||
if url.isMovie || url.isAudio {
|
||||
set(resource: KSPlayerResource(url: url, options: KSOptions()))
|
||||
return true
|
||||
} else {
|
||||
srtControl.selectedSubtitleInfo = URLSubtitleInfo(url: url)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
class UIActivityIndicatorView: UIView {
|
||||
private let loadingView = NSView()
|
||||
private let progressLabel = UILabel()
|
||||
public var progress: Double = 0 {
|
||||
didSet {
|
||||
print("new progress: \(progress)")
|
||||
progressLabel.stringValue = "\(Int(progress * 100))%"
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame frameRect: CGRect) {
|
||||
super.init(frame: frameRect)
|
||||
wantsLayer = true
|
||||
backingLayer?.backgroundColor = UIColor(white: 0, alpha: 0.2).cgColor
|
||||
setupLoadingView()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func setupLoadingView() {
|
||||
loadingView.wantsLayer = true
|
||||
addSubview(loadingView)
|
||||
let imageView = NSImageView()
|
||||
imageView.image = KSOptions.image(named: "loading")
|
||||
loadingView.addSubview(imageView)
|
||||
imageView.imageScaling = .scaleAxesIndependently
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
loadingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
widthAnchor.constraint(equalToConstant: 110),
|
||||
heightAnchor.constraint(equalToConstant: 110),
|
||||
loadingView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
loadingView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
loadingView.widthAnchor.constraint(equalTo: widthAnchor),
|
||||
loadingView.heightAnchor.constraint(equalTo: heightAnchor),
|
||||
imageView.bottomAnchor.constraint(equalTo: loadingView.bottomAnchor),
|
||||
imageView.leadingAnchor.constraint(equalTo: loadingView.leadingAnchor),
|
||||
imageView.heightAnchor.constraint(equalTo: widthAnchor),
|
||||
imageView.widthAnchor.constraint(equalTo: heightAnchor),
|
||||
])
|
||||
progressLabel.alignment = .center
|
||||
progressLabel.font = NSFont.systemFont(ofSize: 18, weight: .medium)
|
||||
addSubview(progressLabel)
|
||||
progressLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
progressLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
progressLabel.topAnchor.constraint(equalTo: loadingView.bottomAnchor, constant: 20),
|
||||
progressLabel.widthAnchor.constraint(equalToConstant: 100),
|
||||
progressLabel.heightAnchor.constraint(equalToConstant: 22),
|
||||
])
|
||||
startAnimating()
|
||||
}
|
||||
}
|
||||
|
||||
extension UIActivityIndicatorView: LoadingIndector {
|
||||
func startAnimating() {
|
||||
loadingView.backingLayer?.position = CGPoint(x: loadingView.layer!.frame.midX, y: loadingView.layer!.frame.midY)
|
||||
loadingView.backingLayer?.anchorPoint = CGPoint(x: 0.5, y: 0.5)
|
||||
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
||||
rotationAnimation.duration = 1.0
|
||||
rotationAnimation.repeatCount = MAXFLOAT
|
||||
rotationAnimation.fromValue = 0.0
|
||||
rotationAnimation.toValue = Float.pi * -2
|
||||
loadingView.backingLayer?.add(rotationAnimation, forKey: "loading")
|
||||
}
|
||||
|
||||
func stopAnimating() {
|
||||
loadingView.backingLayer?.removeAnimation(forKey: "loading")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user