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>
226 lines
9.2 KiB
Swift
226 lines
9.2 KiB
Swift
//
|
|
// BrightnessVolume.swift
|
|
// KSPlayer
|
|
//
|
|
// Created by kintan on 2017/11/3.
|
|
//
|
|
#if canImport(UIKit)
|
|
import UIKit
|
|
|
|
@MainActor
|
|
open class BrightnessVolume {
|
|
private var brightnessObservation: NSKeyValueObservation?
|
|
public static let shared = BrightnessVolume()
|
|
public var progressView: BrightnessVolumeViewProtocol & UIView = ProgressView()
|
|
init() {
|
|
#if !os(tvOS) && !os(xrOS)
|
|
brightnessObservation = UIScreen.main.observe(\.brightness, options: .new) { [weak self] _, change in
|
|
guard KSOptions.enableBrightnessGestures else { return }
|
|
if let self, let value = change.newValue {
|
|
self.appearView()
|
|
self.progressView.setProgress(Float(value), type: 0)
|
|
}
|
|
}
|
|
#endif
|
|
let name = NSNotification.Name(rawValue: "AVSystemController_SystemVolumeDidChangeNotification")
|
|
NotificationCenter.default.addObserver(self, selector: #selector(volumeIsChanged(notification:)), name: name, object: nil)
|
|
progressView.alpha = 0.0
|
|
}
|
|
|
|
public func move(to view: UIView) {
|
|
progressView.move(to: view)
|
|
}
|
|
|
|
@objc private func volumeIsChanged(notification: NSNotification) {
|
|
guard KSOptions.enableVolumeGestures else { return }
|
|
if let changeReason = notification.userInfo?["AVSystemController_AudioVolumeChangeReasonNotificationParameter"] as? String, changeReason == "ExplicitVolumeChange" {
|
|
if let volume = notification.userInfo?["AVSystemController_AudioVolumeNotificationParameter"] as? CGFloat {
|
|
appearView()
|
|
progressView.setProgress(Float(volume), type: 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func appearView() {
|
|
if progressView.alpha == 0.0 {
|
|
progressView.alpha = 1.0
|
|
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) { [weak self] () in
|
|
self?.disAppearView()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func disAppearView() {
|
|
if progressView.alpha == 1.0 {
|
|
UIView.animate(withDuration: 0.8) { [weak self] () in
|
|
self?.progressView.alpha = 0.0
|
|
}
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
brightnessObservation?.invalidate()
|
|
}
|
|
}
|
|
|
|
public protocol BrightnessVolumeViewProtocol {
|
|
// type: 0 brightness type: 1 volume
|
|
func setProgress(_ progress: Float, type: UInt)
|
|
func move(to view: UIView)
|
|
}
|
|
|
|
private final class SystemView: UIVisualEffectView {
|
|
private let stackView = UIStackView()
|
|
private let imageView = UIImageView()
|
|
private let titleLabel = UILabel()
|
|
private lazy var brightnessImage = UIImage(systemName: "sun.max")
|
|
private lazy var volumeImage = UIImage(systemName: "speaker.wave.3.fill")
|
|
private convenience init() {
|
|
self.init(effect: UIBlurEffect(style: .extraLight))
|
|
clipsToBounds = true
|
|
cornerRadius = 10
|
|
imageView.image = brightnessImage
|
|
contentView.addSubview(imageView)
|
|
titleLabel.font = .systemFont(ofSize: 16)
|
|
titleLabel.textColor = UIColor(red: 0.25, green: 0.22, blue: 0.21, alpha: 1)
|
|
titleLabel.textAlignment = .center
|
|
titleLabel.text = "亮度"
|
|
contentView.addSubview(titleLabel)
|
|
let longView = UIView()
|
|
longView.backgroundColor = titleLabel.textColor
|
|
contentView.addSubview(longView)
|
|
stackView.alignment = .center
|
|
stackView.distribution = .fillEqually
|
|
stackView.axis = .horizontal
|
|
stackView.spacing = 1
|
|
longView.addSubview(stackView)
|
|
for _ in 0 ..< 16 {
|
|
let tipView = UIView()
|
|
tipView.backgroundColor = .white
|
|
stackView.addArrangedSubview(tipView)
|
|
tipView.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
tipView.heightAnchor.constraint(equalTo: stackView.heightAnchor),
|
|
])
|
|
}
|
|
translatesAutoresizingMaskIntoConstraints = false
|
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
|
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
longView.translatesAutoresizingMaskIntoConstraints = false
|
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
imageView.widthAnchor.constraint(equalToConstant: 79),
|
|
imageView.heightAnchor.constraint(equalToConstant: 76),
|
|
imageView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 5),
|
|
titleLabel.widthAnchor.constraint(equalTo: widthAnchor),
|
|
titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
titleLabel.heightAnchor.constraint(equalToConstant: 30),
|
|
longView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 13),
|
|
longView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -13),
|
|
longView.heightAnchor.constraint(equalToConstant: 7),
|
|
longView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -16),
|
|
stackView.leadingAnchor.constraint(equalTo: longView.leadingAnchor, constant: 1),
|
|
stackView.trailingAnchor.constraint(equalTo: longView.trailingAnchor, constant: -1),
|
|
stackView.topAnchor.constraint(equalTo: longView.topAnchor, constant: 1),
|
|
stackView.bottomAnchor.constraint(equalTo: longView.bottomAnchor, constant: -1),
|
|
])
|
|
}
|
|
}
|
|
|
|
extension SystemView: BrightnessVolumeViewProtocol {
|
|
public func setProgress(_ progress: Float, type: UInt) {
|
|
if type == 0 {
|
|
imageView.image = brightnessImage
|
|
titleLabel.text = NSLocalizedString("brightness", comment: "")
|
|
} else {
|
|
imageView.image = volumeImage
|
|
titleLabel.text = NSLocalizedString("volume", comment: "")
|
|
}
|
|
let level = Int(progress * Float(stackView.arrangedSubviews.count))
|
|
for i in 0 ..< stackView.arrangedSubviews.count {
|
|
let view = stackView.arrangedSubviews[i]
|
|
if i <= level, level > 0 {
|
|
view.alpha = 1
|
|
} else {
|
|
view.alpha = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
public func move(to view: UIView) {
|
|
if superview != view {
|
|
removeFromSuperview()
|
|
view.addSubview(self)
|
|
translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
|
centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
|
heightAnchor.constraint(equalToConstant: 155),
|
|
widthAnchor.constraint(equalToConstant: 155),
|
|
])
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class ProgressView: UIView {
|
|
private lazy var brightnessImage = UIImage(systemName: "sun.max")
|
|
private lazy var volumeImage = UIImage(systemName: "speaker.fill")
|
|
private lazy var brightnessOffImage = UIImage(systemName: "sun.min")
|
|
private lazy var volumeOffImage = UIImage(systemName: "speaker.slash.fill")
|
|
private let progressView = UIProgressView()
|
|
private let imageView = UIImageView()
|
|
|
|
override init(frame _: CGRect) {
|
|
super.init(frame: .zero)
|
|
addSubview(progressView)
|
|
addSubview(imageView)
|
|
progressView.progressTintColor = UIColor.white
|
|
progressView.trackTintColor = UIColor.white.withAlphaComponent(0.5)
|
|
progressView.translatesAutoresizingMaskIntoConstraints = false
|
|
progressView.centerRotate(byDegrees: -90)
|
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
progressView.widthAnchor.constraint(equalToConstant: 115),
|
|
progressView.heightAnchor.constraint(equalToConstant: 2),
|
|
progressView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
progressView.topAnchor.constraint(equalTo: topAnchor, constant: 57),
|
|
imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
imageView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
])
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder _: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|
|
|
|
extension ProgressView: BrightnessVolumeViewProtocol {
|
|
func setProgress(_ progress: Float, type: UInt) {
|
|
progressView.setProgress(progress, animated: false)
|
|
if progress == 0 {
|
|
imageView.image = type == 0 ? brightnessOffImage : volumeOffImage
|
|
} else {
|
|
imageView.image = type == 0 ? brightnessImage : volumeImage
|
|
}
|
|
}
|
|
|
|
func move(to view: UIView) {
|
|
if superview != view {
|
|
removeFromSuperview()
|
|
view.addSubview(self)
|
|
translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
trailingAnchor.constraint(equalTo: view.safeTrailingAnchor, constant: -10),
|
|
centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
|
heightAnchor.constraint(equalToConstant: 150),
|
|
widthAnchor.constraint(equalToConstant: 24),
|
|
])
|
|
}
|
|
}
|
|
}
|
|
#endif
|