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:
594
KSPlayer-main/Sources/KSPlayer/Core/AppKitExtend.swift
Normal file
594
KSPlayer-main/Sources/KSPlayer/Core/AppKitExtend.swift
Normal file
@@ -0,0 +1,594 @@
|
||||
//
|
||||
// AppKitExtend.swift
|
||||
// KSPlayer
|
||||
//
|
||||
// Created by kintan on 2018/3/9.
|
||||
//
|
||||
|
||||
/// 'NSWindow' is unavailable in Mac Catalyst
|
||||
#if !canImport(UIKit)
|
||||
import AppKit
|
||||
import CoreMedia
|
||||
import IOKit.pwr_mgt
|
||||
|
||||
public typealias UIApplicationDelegate = NSApplicationDelegate
|
||||
public typealias UIApplication = NSApplication
|
||||
public typealias UIWindow = NSWindow
|
||||
public typealias UIViewController = NSViewController
|
||||
public typealias UIColor = NSColor
|
||||
public typealias UIStackView = NSStackView
|
||||
public typealias UIPanGestureRecognizer = NSPanGestureRecognizer
|
||||
public typealias UIGestureRecognizer = NSGestureRecognizer
|
||||
public typealias UIGestureRecognizerDelegate = NSGestureRecognizerDelegate
|
||||
public typealias UIViewContentMode = ContentMode
|
||||
public typealias UIFont = NSFont
|
||||
public typealias UIFontDescriptor = NSFontDescriptor
|
||||
public typealias UIControl = NSControl
|
||||
public typealias UITextField = NSTextField
|
||||
public typealias UIImageView = NSImageView
|
||||
public typealias UITapGestureRecognizer = NSClickGestureRecognizer
|
||||
public typealias UXSlider = NSSlider
|
||||
public typealias UITableView = NSTableView
|
||||
public typealias UITableViewDelegate = NSTableViewDelegate
|
||||
public typealias UITableViewDataSource = NSTableViewDataSource
|
||||
public typealias UITouch = NSTouch
|
||||
public typealias UIEvent = NSEvent
|
||||
public typealias UIButton = KSButton
|
||||
public extension UIFontDescriptor.SymbolicTraits {
|
||||
static var traitItalic = italic
|
||||
static var traitBold = bold
|
||||
}
|
||||
|
||||
extension NSScreen {
|
||||
var scale: CGFloat {
|
||||
backingScaleFactor
|
||||
}
|
||||
}
|
||||
|
||||
public extension NSClickGestureRecognizer {
|
||||
var numberOfTapsRequired: Int {
|
||||
get {
|
||||
numberOfClicksRequired
|
||||
}
|
||||
set {
|
||||
numberOfClicksRequired = newValue
|
||||
}
|
||||
}
|
||||
|
||||
func require(toFail otherGestureRecognizer: NSClickGestureRecognizer) {
|
||||
buttonMask = otherGestureRecognizer.buttonMask << 1
|
||||
}
|
||||
}
|
||||
|
||||
public extension NSView {
|
||||
@objc internal var contentMode: UIViewContentMode {
|
||||
get {
|
||||
if let contentsGravity = backingLayer?.contentsGravity {
|
||||
switch contentsGravity {
|
||||
case .resize:
|
||||
return .scaleToFill
|
||||
case .resizeAspect:
|
||||
return .scaleAspectFit
|
||||
case .resizeAspectFill:
|
||||
return .scaleAspectFill
|
||||
default:
|
||||
return .scaleAspectFit
|
||||
}
|
||||
} else {
|
||||
return .scaleAspectFit
|
||||
}
|
||||
}
|
||||
set {
|
||||
switch newValue {
|
||||
case .scaleToFill:
|
||||
backingLayer?.contentsGravity = .resize
|
||||
case .scaleAspectFit:
|
||||
backingLayer?.contentsGravity = .resizeAspect
|
||||
case .scaleAspectFill:
|
||||
backingLayer?.contentsGravity = .resizeAspectFill
|
||||
case .center:
|
||||
backingLayer?.contentsGravity = .center
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var center: CGPoint {
|
||||
CGPoint(x: frame.midX, y: frame.midY)
|
||||
}
|
||||
|
||||
var alpha: CGFloat {
|
||||
get {
|
||||
alphaValue
|
||||
}
|
||||
set {
|
||||
alphaValue = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var clipsToBounds: Bool {
|
||||
get {
|
||||
if let layer {
|
||||
return layer.masksToBounds
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
set {
|
||||
backingLayer?.masksToBounds = newValue
|
||||
}
|
||||
}
|
||||
|
||||
class func animate(withDuration duration: TimeInterval, animations: @escaping () -> Void, completion: ((Bool) -> Void)? = nil) {
|
||||
CATransaction.begin()
|
||||
CATransaction.setAnimationDuration(duration)
|
||||
CATransaction.setCompletionBlock {
|
||||
completion?(true)
|
||||
}
|
||||
animations()
|
||||
CATransaction.commit()
|
||||
}
|
||||
|
||||
class func animate(withDuration duration: TimeInterval, animations: @escaping () -> Void) {
|
||||
animate(withDuration: duration, animations: animations, completion: nil)
|
||||
}
|
||||
|
||||
func layoutIfNeeded() {
|
||||
layer?.layoutIfNeeded()
|
||||
}
|
||||
|
||||
func centerRotate(byDegrees: Double) {
|
||||
layer?.position = center
|
||||
layer?.anchorPoint = CGPoint(x: 0.5, y: 0.5)
|
||||
layer?.setAffineTransform(CGAffineTransform(rotationAngle: CGFloat(Double.pi * byDegrees / 180.0)))
|
||||
}
|
||||
}
|
||||
|
||||
public extension NSImage {
|
||||
convenience init(cgImage: CGImage) {
|
||||
self.init(cgImage: cgImage, size: NSSize.zero)
|
||||
}
|
||||
|
||||
@available(macOS 11.0, *)
|
||||
convenience init?(systemName: String) {
|
||||
self.init(systemSymbolName: systemName, accessibilityDescription: nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension NSButton {
|
||||
var titleFont: UIFont? {
|
||||
get {
|
||||
font
|
||||
}
|
||||
set {
|
||||
font = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var tintColor: UIColor? {
|
||||
get {
|
||||
contentTintColor
|
||||
}
|
||||
set {
|
||||
contentTintColor = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var backgroundColor: UIColor? {
|
||||
get {
|
||||
if let layer, let cgColor = layer.backgroundColor {
|
||||
return UIColor(cgColor: cgColor)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
set {
|
||||
backingLayer?.backgroundColor = newValue?.cgColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NSImageView {
|
||||
var backgroundColor: UIColor? {
|
||||
get {
|
||||
if let layer, let cgColor = layer.backgroundColor {
|
||||
return UIColor(cgColor: cgColor)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
set {
|
||||
backingLayer?.backgroundColor = newValue?.cgColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension NSControl {
|
||||
var textAlignment: NSTextAlignment {
|
||||
get {
|
||||
alignment
|
||||
}
|
||||
set {
|
||||
alignment = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var text: String {
|
||||
get {
|
||||
stringValue
|
||||
}
|
||||
set {
|
||||
stringValue = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var attributedText: NSAttributedString? {
|
||||
get {
|
||||
attributedStringValue
|
||||
}
|
||||
set {
|
||||
attributedStringValue = newValue ?? NSAttributedString()
|
||||
}
|
||||
}
|
||||
|
||||
var numberOfLines: Int {
|
||||
get {
|
||||
usesSingleLineMode ? 1 : 0
|
||||
}
|
||||
set {
|
||||
usesSingleLineMode = newValue == 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension NSTextContainer {
|
||||
var numberOfLines: Int {
|
||||
get {
|
||||
maximumNumberOfLines
|
||||
}
|
||||
set {
|
||||
maximumNumberOfLines = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension NSResponder {
|
||||
var next: NSResponder? {
|
||||
nextResponder
|
||||
}
|
||||
}
|
||||
|
||||
public extension NSSlider {
|
||||
var minimumTrackTintColor: UIColor? {
|
||||
get {
|
||||
trackFillColor
|
||||
}
|
||||
set {
|
||||
trackFillColor = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var maximumTrackTintColor: UIColor? {
|
||||
get {
|
||||
nil
|
||||
}
|
||||
set {}
|
||||
}
|
||||
}
|
||||
|
||||
public extension NSStackView {
|
||||
var axis: NSUserInterfaceLayoutOrientation {
|
||||
get {
|
||||
orientation
|
||||
}
|
||||
set {
|
||||
orientation = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension NSGestureRecognizer {
|
||||
func addTarget(_ target: AnyObject, action: Selector) {
|
||||
self.target = target
|
||||
self.action = action
|
||||
}
|
||||
}
|
||||
|
||||
public extension UIApplication {
|
||||
private static var assertionID = IOPMAssertionID()
|
||||
static var isIdleTimerDisabled = false {
|
||||
didSet {
|
||||
if isIdleTimerDisabled != oldValue {
|
||||
if isIdleTimerDisabled {
|
||||
_ = IOPMAssertionCreateWithName(kIOPMAssertionTypeNoDisplaySleep as CFString,
|
||||
IOPMAssertionLevel(kIOPMAssertionLevelOn),
|
||||
"KSPlayer is playing video" as CFString,
|
||||
&assertionID)
|
||||
} else {
|
||||
_ = IOPMAssertionRelease(assertionID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isIdleTimerDisabled: Bool {
|
||||
get {
|
||||
UIApplication.isIdleTimerDisabled
|
||||
}
|
||||
set {
|
||||
UIApplication.isIdleTimerDisabled = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @available(*, unavailable, renamed: "UIView.ContentMode")
|
||||
@objc public enum ContentMode: Int {
|
||||
case scaleToFill
|
||||
|
||||
case scaleAspectFit // contents scaled to fit with fixed aspect. remainder is transparent
|
||||
|
||||
case scaleAspectFill // contents scaled to fill with fixed aspect. some portion of content may be clipped.
|
||||
|
||||
case redraw // redraw on bounds change (calls -setNeedsDisplay)
|
||||
|
||||
case center // contents remain same size. positioned adjusted.
|
||||
|
||||
case top
|
||||
|
||||
case bottom
|
||||
|
||||
case left
|
||||
|
||||
case right
|
||||
|
||||
case topLeft
|
||||
|
||||
case topRight
|
||||
|
||||
case bottomLeft
|
||||
|
||||
case bottomRight
|
||||
}
|
||||
|
||||
public extension UIControl {
|
||||
struct State: OptionSet {
|
||||
public var rawValue: UInt
|
||||
public init(rawValue: UInt) { self.rawValue = rawValue }
|
||||
public static var normal = State(rawValue: 1 << 0)
|
||||
public static var highlighted = State(rawValue: 1 << 1)
|
||||
public static var disabled = State(rawValue: 1 << 2)
|
||||
public static var selected = State(rawValue: 1 << 3)
|
||||
public static var focused = State(rawValue: 1 << 4)
|
||||
public static var application = State(rawValue: 1 << 5)
|
||||
public static var reserved = State(rawValue: 1 << 6)
|
||||
}
|
||||
}
|
||||
|
||||
extension UIControl.State: Hashable {}
|
||||
public class UILabel: NSTextField {
|
||||
override init(frame frameRect: CGRect) {
|
||||
super.init(frame: frameRect)
|
||||
alignment = .left
|
||||
isBordered = false
|
||||
isEditable = false
|
||||
isSelectable = false
|
||||
isBezeled = false
|
||||
drawsBackground = false
|
||||
focusRingType = .none
|
||||
textColor = NSColor.white
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
public class KSButton: NSButton {
|
||||
private var images = [UIControl.State: UIImage]()
|
||||
private var titles = [UIControl.State: String]()
|
||||
private var titleColors = [State: UIColor]()
|
||||
private var targetActions = [ControlEvents: (AnyObject?, Selector)]()
|
||||
|
||||
override public init(frame frameRect: CGRect) {
|
||||
super.init(frame: frameRect)
|
||||
isBordered = false
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public var isSelected: Bool = false {
|
||||
didSet {
|
||||
update(state: isSelected ? .selected : .normal)
|
||||
}
|
||||
}
|
||||
|
||||
override public var isEnabled: Bool {
|
||||
didSet {
|
||||
update(state: isEnabled ? .normal : .disabled)
|
||||
}
|
||||
}
|
||||
|
||||
open func setImage(_ image: UIImage?, for state: UIControl.State) {
|
||||
images[state] = image
|
||||
if state == .normal, isEnabled, !isSelected {
|
||||
self.image = image
|
||||
}
|
||||
}
|
||||
|
||||
open func setTitle(_ title: String, for state: UIControl.State) {
|
||||
titles[state] = title
|
||||
if state == .normal, isEnabled, !isSelected {
|
||||
self.title = title
|
||||
}
|
||||
}
|
||||
|
||||
open func setTitleColor(_ titleColor: UIColor?, for state: UIControl.State) {
|
||||
titleColors[state] = titleColor
|
||||
if state == .normal, isEnabled, !isSelected {
|
||||
// self.titleColor = titleColor
|
||||
}
|
||||
}
|
||||
|
||||
private func update(state: UIControl.State) {
|
||||
if let stateImage = images[state] {
|
||||
image = stateImage
|
||||
}
|
||||
if let stateTitle = titles[state] {
|
||||
title = stateTitle
|
||||
}
|
||||
}
|
||||
|
||||
open func addTarget(_ target: AnyObject?, action: Selector, for controlEvents: ControlEvents) {
|
||||
targetActions[controlEvents] = (target, action)
|
||||
}
|
||||
|
||||
open func removeTarget(_: AnyObject?, action _: Selector?, for controlEvents: ControlEvents) {
|
||||
targetActions.removeValue(forKey: controlEvents)
|
||||
}
|
||||
|
||||
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 public func mouseDown(with event: NSEvent) {
|
||||
super.mouseDown(with: event)
|
||||
if let (target, action) = targetActions[.touchUpInside] ?? targetActions[.primaryActionTriggered] {
|
||||
_ = target?.perform(action, with: self)
|
||||
}
|
||||
}
|
||||
|
||||
override public func mouseEntered(with event: NSEvent) {
|
||||
super.mouseEntered(with: event)
|
||||
if let (target, action) = targetActions[.mouseExited] {
|
||||
_ = target?.perform(action, with: self)
|
||||
}
|
||||
}
|
||||
|
||||
override public func mouseExited(with event: NSEvent) {
|
||||
super.mouseExited(with: event)
|
||||
if let (target, action) = targetActions[.mouseExited] {
|
||||
_ = target?.perform(action, with: self)
|
||||
}
|
||||
}
|
||||
|
||||
open func sendActions(for controlEvents: ControlEvents) {
|
||||
if let (target, action) = targetActions[controlEvents] {
|
||||
_ = target?.perform(action, with: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class KSSlider: NSSlider {
|
||||
weak var delegate: KSSliderDelegate?
|
||||
public var trackHeigt = CGFloat(2)
|
||||
public var isPlayable = false
|
||||
public var isUserInteractionEnabled: Bool = true
|
||||
var tintColor: UIColor?
|
||||
public convenience init() {
|
||||
self.init(frame: .zero)
|
||||
}
|
||||
|
||||
override public init(frame frameRect: CGRect) {
|
||||
super.init(frame: frameRect)
|
||||
target = self
|
||||
action = #selector(progressSliderTouchEnded(_:))
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func progressSliderTouchEnded(_ sender: KSSlider) {
|
||||
if isUserInteractionEnabled {
|
||||
delegate?.slider(value: Double(sender.floatValue), event: .touchUpInside)
|
||||
}
|
||||
}
|
||||
|
||||
open func setThumbImage(_: UIImage?, for _: State) {}
|
||||
|
||||
@IBInspectable var maximumValue: Float {
|
||||
get {
|
||||
Float(maxValue)
|
||||
}
|
||||
set {
|
||||
maxValue = Double(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
@IBInspectable var minimumValue: Float {
|
||||
get {
|
||||
Float(minValue)
|
||||
}
|
||||
set {
|
||||
minValue = Double(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
@IBInspectable var value: Float {
|
||||
get {
|
||||
floatValue
|
||||
}
|
||||
set {
|
||||
floatValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UIView {
|
||||
func image() -> UIImage? {
|
||||
guard let rep = bitmapImageRepForCachingDisplay(in: bounds) else {
|
||||
return nil
|
||||
}
|
||||
cacheDisplay(in: bounds, to: rep)
|
||||
let image = NSImage(size: bounds.size)
|
||||
image.addRepresentation(rep)
|
||||
return image
|
||||
}
|
||||
}
|
||||
|
||||
// todo
|
||||
open class UIAlertController: UIViewController {
|
||||
public enum Style: Int {
|
||||
case actionSheet
|
||||
case alert
|
||||
}
|
||||
|
||||
public convenience init(title _: String?, message _: String?, preferredStyle _: UIAlertController.Style) {
|
||||
self.init()
|
||||
}
|
||||
|
||||
var preferredAction: UIAlertAction?
|
||||
|
||||
open func addAction(_: UIAlertAction) {}
|
||||
}
|
||||
|
||||
open class UIAlertAction: NSObject {
|
||||
public enum Style: Int {
|
||||
case `default`
|
||||
case cancel
|
||||
case destructive
|
||||
}
|
||||
|
||||
public let title: String?
|
||||
public let style: UIAlertAction.Style
|
||||
public private(set) var isEnabled: Bool = false
|
||||
public init(title: String?, style: UIAlertAction.Style, handler _: ((UIAlertAction) -> Void)? = nil) {
|
||||
self.title = title
|
||||
self.style = style
|
||||
}
|
||||
}
|
||||
|
||||
public extension UIViewController {
|
||||
func present(_: UIViewController, animated _: Bool, completion _: (() -> Void)? = nil) {}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user