Files
simvision/KSPlayer-main/Sources/KSPlayer/Core/AppKitExtend.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

595 lines
15 KiB
Swift

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