mirror of
https://github.com/cranci1/Sora.git
synced 2026-01-11 20:10:24 +00:00
ta da 💥 (#64)
Some checks are pending
Build and Release IPA / Build IPA (push) Waiting to run
Some checks are pending
Build and Release IPA / Build IPA (push) Waiting to run
This commit is contained in:
commit
2711cafc93
2 changed files with 312 additions and 160 deletions
|
|
@ -5,82 +5,129 @@
|
|||
// Created by Pratik on 08/01/23.
|
||||
//
|
||||
// Thanks to pratikg29 for this code inside his open source project "https://github.com/pratikg29/Custom-Slider-Control?ref=iosexample.com"
|
||||
// I did edit just a little bit the code for my liking
|
||||
//
|
||||
// I did edit some of the code for my liking (added a buffer indicator, etc.)
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
|
||||
@Binding var value: T
|
||||
let inRange: ClosedRange<T>
|
||||
let activeFillColor: Color
|
||||
let fillColor: Color
|
||||
let emptyColor: Color
|
||||
let height: CGFloat
|
||||
let onEditingChanged: (Bool) -> Void
|
||||
|
||||
@State private var localRealProgress: T = 0
|
||||
@State private var localTempProgress: T = 0
|
||||
@GestureState private var isActive: Bool = false
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { bounds in
|
||||
ZStack {
|
||||
VStack {
|
||||
ZStack(alignment: .center) {
|
||||
Capsule()
|
||||
.fill(emptyColor)
|
||||
Capsule()
|
||||
.fill(isActive ? activeFillColor : fillColor)
|
||||
.mask({
|
||||
HStack {
|
||||
Rectangle()
|
||||
.frame(width: max((bounds.size.width * CGFloat(localRealProgress + localTempProgress)).isFinite ? bounds.size.width * CGFloat(localRealProgress + localTempProgress) : 0, 0), alignment: .leading)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
})
|
||||
struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
|
||||
@Binding var value: T
|
||||
let inRange: ClosedRange<T>
|
||||
|
||||
let bufferValue: T
|
||||
let activeFillColor: Color
|
||||
let fillColor: Color
|
||||
let bufferColor: Color
|
||||
let emptyColor: Color
|
||||
let height: CGFloat
|
||||
|
||||
let onEditingChanged: (Bool) -> Void
|
||||
|
||||
@State private var localRealProgress: T = 0
|
||||
@State private var localTempProgress: T = 0
|
||||
@GestureState private var isActive: Bool = false
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { bounds in
|
||||
ZStack {
|
||||
VStack {
|
||||
ZStack(alignment: .center) {
|
||||
Capsule()
|
||||
.fill(emptyColor)
|
||||
|
||||
Capsule()
|
||||
.fill(bufferColor)
|
||||
.mask({
|
||||
HStack {
|
||||
Rectangle()
|
||||
.frame(
|
||||
width: max(
|
||||
(bounds.size.width
|
||||
* CGFloat(getPrgPercentage(bufferValue)))
|
||||
.isFinite
|
||||
? bounds.size.width
|
||||
* CGFloat(getPrgPercentage(bufferValue))
|
||||
: 0,
|
||||
0
|
||||
),
|
||||
alignment: .leading
|
||||
)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
})
|
||||
|
||||
Capsule()
|
||||
.fill(isActive ? activeFillColor : fillColor)
|
||||
.mask({
|
||||
HStack {
|
||||
Rectangle()
|
||||
.frame(
|
||||
width: max(
|
||||
(bounds.size.width
|
||||
* CGFloat(localRealProgress + localTempProgress))
|
||||
.isFinite
|
||||
? bounds.size.width
|
||||
* CGFloat(localRealProgress + localTempProgress)
|
||||
: 0,
|
||||
0
|
||||
),
|
||||
alignment: .leading
|
||||
)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
HStack {
|
||||
let shouldShowHours = inRange.upperBound >= 3600
|
||||
Text(value.asTimeString(style: .positional, showHours: shouldShowHours))
|
||||
Spacer(minLength: 0)
|
||||
Text("-" + (inRange.upperBound - value).asTimeString(
|
||||
style: .positional,
|
||||
showHours: shouldShowHours
|
||||
))
|
||||
}
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(isActive ? fillColor : emptyColor)
|
||||
}
|
||||
|
||||
HStack {
|
||||
// Determine if we should show hours based on the total duration.
|
||||
let shouldShowHours = inRange.upperBound >= 3600
|
||||
Text(value.asTimeString(style: .positional, showHours: shouldShowHours))
|
||||
Spacer(minLength: 0)
|
||||
Text("-" + (inRange.upperBound - value).asTimeString(style: .positional, showHours: shouldShowHours))
|
||||
}
|
||||
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(isActive ? fillColor : emptyColor)
|
||||
.frame(
|
||||
width: isActive ? bounds.size.width * 1.04 : bounds.size.width,
|
||||
alignment: .center
|
||||
)
|
||||
.animation(animation, value: isActive)
|
||||
}
|
||||
.frame(width: isActive ? bounds.size.width * 1.04 : bounds.size.width, alignment: .center)
|
||||
.animation(animation, value: isActive)
|
||||
}
|
||||
.frame(width: bounds.size.width, height: bounds.size.height, alignment: .center)
|
||||
.contentShape(Rectangle())
|
||||
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local)
|
||||
.frame(
|
||||
width: bounds.size.width,
|
||||
height: bounds.size.height,
|
||||
alignment: .center
|
||||
)
|
||||
.contentShape(Rectangle())
|
||||
.gesture(
|
||||
DragGesture(minimumDistance: 0, coordinateSpace: .local)
|
||||
.updating($isActive) { _, state, _ in
|
||||
state = true
|
||||
}
|
||||
state = true
|
||||
}
|
||||
.onChanged { gesture in
|
||||
localTempProgress = T(gesture.translation.width / bounds.size.width)
|
||||
value = max(min(getPrgValue(), inRange.upperBound), inRange.lowerBound)
|
||||
}.onEnded { _ in
|
||||
localRealProgress = max(min(localRealProgress + localTempProgress, 1), 0)
|
||||
localTempProgress = 0
|
||||
})
|
||||
.onChange(of: isActive) { newValue in
|
||||
value = max(min(getPrgValue(), inRange.upperBound), inRange.lowerBound)
|
||||
onEditingChanged(newValue)
|
||||
}
|
||||
.onAppear {
|
||||
localRealProgress = getPrgPercentage(value)
|
||||
}
|
||||
.onChange(of: value) { newValue in
|
||||
if !isActive {
|
||||
localRealProgress = getPrgPercentage(newValue)
|
||||
localTempProgress = T(gesture.translation.width / bounds.size.width)
|
||||
value = max(min(getPrgValue(), inRange.upperBound), inRange.lowerBound)
|
||||
}
|
||||
.onEnded { _ in
|
||||
localRealProgress = max(min(localRealProgress + localTempProgress, 1), 0)
|
||||
localTempProgress = 0
|
||||
}
|
||||
)
|
||||
.onChange(of: isActive) { newValue in
|
||||
value = max(min(getPrgValue(), inRange.upperBound), inRange.lowerBound)
|
||||
onEditingChanged(newValue)
|
||||
}
|
||||
.onAppear {
|
||||
localRealProgress = getPrgPercentage(value)
|
||||
}
|
||||
.onChange(of: value) { newValue in
|
||||
if !isActive {
|
||||
localRealProgress = getPrgPercentage(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: isActive ? height * 1.25 : height, alignment: .center)
|
||||
}
|
||||
|
||||
|
|
@ -100,6 +147,36 @@ struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
|
|||
}
|
||||
|
||||
private func getPrgValue() -> T {
|
||||
return ((localRealProgress + localTempProgress) * (inRange.upperBound - inRange.lowerBound)) + inRange.lowerBound
|
||||
return ((localRealProgress + localTempProgress) * (inRange.upperBound - inRange.lowerBound)) + inRange.lowerBound}
|
||||
// MARK: - Helpers
|
||||
|
||||
private func fraction(of val: T) -> T {
|
||||
let total = inRange.upperBound - inRange.lowerBound
|
||||
let normalized = val - inRange.lowerBound
|
||||
return (total > 0) ? (normalized / total) : 0
|
||||
}
|
||||
|
||||
private func clampedFraction(_ f: T) -> T {
|
||||
max(0, min(f, 1))
|
||||
}
|
||||
|
||||
private func getCurrentValue() -> T {
|
||||
let total = inRange.upperBound - inRange.lowerBound
|
||||
let frac = clampedFraction(localRealProgress + localTempProgress)
|
||||
return frac * total + inRange.lowerBound
|
||||
}
|
||||
|
||||
private func clampedValue(_ raw: T) -> T {
|
||||
max(inRange.lowerBound, min(raw, inRange.upperBound))
|
||||
}
|
||||
|
||||
private func playedWidth(boundsWidth: CGFloat) -> CGFloat {
|
||||
let frac = fraction(of: value)
|
||||
return max(0, min(boundsWidth * CGFloat(frac), boundsWidth))
|
||||
}
|
||||
|
||||
private func bufferWidth(boundsWidth: CGFloat) -> CGFloat {
|
||||
let frac = fraction(of: bufferValue)
|
||||
return max(0, min(boundsWidth * CGFloat(frac), boundsWidth))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,15 @@ import AVKit
|
|||
import SwiftUI
|
||||
import AVFoundation
|
||||
|
||||
// MARK: - SliderViewModel
|
||||
|
||||
class SliderViewModel: ObservableObject {
|
||||
@Published var sliderValue: Double = 0.0
|
||||
@Published var bufferValue: Double = 0.0
|
||||
}
|
||||
|
||||
// MARK: - CustomMediaPlayerViewController
|
||||
|
||||
class CustomMediaPlayerViewController: UIViewController {
|
||||
let module: ScrapingModule
|
||||
let streamURL: String
|
||||
|
|
@ -82,13 +87,16 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
var isControlsVisible = false
|
||||
|
||||
var subtitleBottomConstraint: NSLayoutConstraint?
|
||||
|
||||
var subtitleBottomPadding: CGFloat = 10.0 {
|
||||
didSet {
|
||||
updateSubtitleLabelConstraints()
|
||||
}
|
||||
}
|
||||
|
||||
private var playerItemKVOContext = 0
|
||||
private var loadedTimeRangesObservation: NSKeyValueObservation?
|
||||
|
||||
|
||||
init(module: ScrapingModule,
|
||||
urlString: String,
|
||||
fullUrl: String,
|
||||
|
|
@ -112,10 +120,12 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
guard let url = URL(string: urlString) else {
|
||||
fatalError("Invalid URL string")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer")
|
||||
request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Origin")
|
||||
request.addValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", forHTTPHeaderField: "User-Agent")
|
||||
request.addValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
|
||||
forHTTPHeaderField: "User-Agent")
|
||||
|
||||
let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": request.allHTTPHeaderFields ?? [:]])
|
||||
let playerItem = AVPlayerItem(asset: asset)
|
||||
|
|
@ -145,8 +155,8 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
addInvisibleControlOverlays()
|
||||
setupSubtitleLabel()
|
||||
setupDismissButton()
|
||||
setupQualityButton()
|
||||
setupSpeedButton()
|
||||
setupQualityButton()
|
||||
setupMenuButton()
|
||||
setupSkip85Button()
|
||||
setupWatchNextButton()
|
||||
|
|
@ -154,6 +164,12 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
startUpdateTimer()
|
||||
setupAudioSession()
|
||||
|
||||
if let item = player.currentItem {
|
||||
loadedTimeRangesObservation = item.observe(\.loadedTimeRanges, options: [.new, .initial]) { [weak self] (playerItem, change) in
|
||||
self?.updateBufferValue()
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||
self?.checkForHLSStream()
|
||||
}
|
||||
|
|
@ -175,25 +191,32 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidChange), name: .AVPlayerItemNewAccessLogEntry, object: nil)
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(playerItemDidChange),
|
||||
name: .AVPlayerItemNewAccessLogEntry,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemNewAccessLogEntry, object: nil)
|
||||
loadedTimeRangesObservation?.invalidate()
|
||||
loadedTimeRangesObservation = nil
|
||||
|
||||
if let playbackSpeed = player?.rate {
|
||||
UserDefaults.standard.set(playbackSpeed, forKey: "lastPlaybackSpeed")
|
||||
}
|
||||
player.pause()
|
||||
updateTimer?.invalidate()
|
||||
inactivityTimer?.invalidate()
|
||||
if let token = timeObserverToken {
|
||||
player.removeTimeObserver(token)
|
||||
timeObserverToken = nil
|
||||
}
|
||||
UserDefaults.standard.set(player.rate, forKey: "lastPlaybackSpeed")
|
||||
|
||||
updateTimer?.invalidate()
|
||||
inactivityTimer?.invalidate()
|
||||
|
||||
player.pause()
|
||||
|
||||
if let playbackSpeed = player?.rate {
|
||||
UserDefaults.standard.set(playbackSpeed, forKey: "lastPlaybackSpeed")
|
||||
}
|
||||
|
||||
if let currentItem = player.currentItem, currentItem.duration.seconds > 0 {
|
||||
let progress = currentTimeVal / currentItem.duration.seconds
|
||||
let item = ContinueWatchingItem(
|
||||
|
|
@ -211,9 +234,36 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
}
|
||||
}
|
||||
|
||||
override func observeValue(forKeyPath keyPath: String?,
|
||||
of object: Any?,
|
||||
change: [NSKeyValueChangeKey : Any]?,
|
||||
context: UnsafeMutableRawPointer?) {
|
||||
|
||||
guard context == &playerItemKVOContext else {
|
||||
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
||||
return
|
||||
}
|
||||
|
||||
if keyPath == "loadedTimeRanges" {
|
||||
updateBufferValue()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateBufferValue() {
|
||||
guard let item = player.currentItem else { return }
|
||||
|
||||
if let timeRange = item.loadedTimeRanges.first?.timeRangeValue {
|
||||
let buffered = CMTimeGetSeconds(timeRange.start) + CMTimeGetSeconds(timeRange.duration)
|
||||
DispatchQueue.main.async {
|
||||
self.sliderViewModel.bufferValue = buffered
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func playerItemDidChange() {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
if let self = self, self.qualityButton.isHidden && self.isHLSStream {
|
||||
guard let self = self else { return }
|
||||
if self.qualityButton.isHidden && self.isHLSStream {
|
||||
self.qualityButton.isHidden = false
|
||||
self.qualityButton.menu = self.qualitySelectionMenu()
|
||||
}
|
||||
|
|
@ -310,8 +360,10 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
value: Binding(get: { self.sliderViewModel.sliderValue },
|
||||
set: { self.sliderViewModel.sliderValue = $0 }),
|
||||
inRange: 0...(duration > 0 ? duration : 1.0),
|
||||
bufferValue: self.sliderViewModel.bufferValue,
|
||||
activeFillColor: .white,
|
||||
fillColor: .white.opacity(0.5),
|
||||
bufferColor: .white.opacity(0.2),
|
||||
emptyColor: .white.opacity(0.3),
|
||||
height: 30,
|
||||
onEditingChanged: { editing in
|
||||
|
|
@ -353,6 +405,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
])
|
||||
}
|
||||
|
||||
|
||||
func addInvisibleControlOverlays() {
|
||||
let playPauseOverlay = UIButton(type: .custom)
|
||||
playPauseOverlay.backgroundColor = .clear
|
||||
|
|
@ -365,30 +418,6 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
playPauseOverlay.widthAnchor.constraint(equalTo: playPauseButton.widthAnchor, constant: 20),
|
||||
playPauseOverlay.heightAnchor.constraint(equalTo: playPauseButton.heightAnchor, constant: 20)
|
||||
])
|
||||
|
||||
let backwardOverlay = UIButton(type: .custom)
|
||||
backwardOverlay.backgroundColor = .clear
|
||||
backwardOverlay.addTarget(self, action: #selector(seekBackward), for: .touchUpInside)
|
||||
view.addSubview(backwardOverlay)
|
||||
backwardOverlay.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
backwardOverlay.centerXAnchor.constraint(equalTo: backwardButton.centerXAnchor),
|
||||
backwardOverlay.centerYAnchor.constraint(equalTo: backwardButton.centerYAnchor),
|
||||
backwardOverlay.widthAnchor.constraint(equalTo: backwardButton.widthAnchor, constant: 20),
|
||||
backwardOverlay.heightAnchor.constraint(equalTo: backwardButton.heightAnchor, constant: 20)
|
||||
])
|
||||
|
||||
let forwardOverlay = UIButton(type: .custom)
|
||||
forwardOverlay.backgroundColor = .clear
|
||||
forwardOverlay.addTarget(self, action: #selector(seekForward), for: .touchUpInside)
|
||||
view.addSubview(forwardOverlay)
|
||||
forwardOverlay.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
forwardOverlay.centerXAnchor.constraint(equalTo: forwardButton.centerXAnchor),
|
||||
forwardOverlay.centerYAnchor.constraint(equalTo: forwardButton.centerYAnchor),
|
||||
forwardOverlay.widthAnchor.constraint(equalTo: forwardButton.widthAnchor, constant: 20),
|
||||
forwardOverlay.heightAnchor.constraint(equalTo: forwardButton.heightAnchor, constant: 20)
|
||||
])
|
||||
}
|
||||
|
||||
func setupSkipAndDismissGestures() {
|
||||
|
|
@ -516,32 +545,49 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
dismissButton.addTarget(self, action: #selector(dismissTapped), for: .touchUpInside)
|
||||
controlsContainerView.addSubview(dismissButton)
|
||||
dismissButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
dismissButton.leadingAnchor.constraint(equalTo: controlsContainerView.leadingAnchor, constant: 16),
|
||||
dismissButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
|
||||
dismissButton.widthAnchor.constraint(equalToConstant: 40),
|
||||
dismissButton.heightAnchor.constraint(equalToConstant: 40)
|
||||
])
|
||||
|
||||
let episodeLabel = UILabel()
|
||||
episodeLabel.text = "\(titleText) • Ep \(episodeNumber)"
|
||||
episodeLabel.textColor = .white
|
||||
episodeLabel.font = UIFont.systemFont(ofSize: 15, weight: .medium)
|
||||
episodeLabel.numberOfLines = 1
|
||||
episodeLabel.lineBreakMode = .byTruncatingTail
|
||||
|
||||
controlsContainerView.addSubview(episodeLabel)
|
||||
episodeLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
episodeLabel.centerYAnchor.constraint(equalTo: dismissButton.centerYAnchor),
|
||||
episodeLabel.leadingAnchor.constraint(equalTo: dismissButton.trailingAnchor, constant: 12),
|
||||
episodeLabel.trailingAnchor.constraint(lessThanOrEqualTo: controlsContainerView.trailingAnchor, constant: -16)
|
||||
])
|
||||
}
|
||||
|
||||
func setupMenuButton() {
|
||||
menuButton = UIButton(type: .system)
|
||||
menuButton.setImage(UIImage(systemName: "text.bubble"), for: .normal)
|
||||
menuButton.tintColor = .white
|
||||
|
||||
|
||||
if let subtitlesURL = subtitlesURL, !subtitlesURL.isEmpty {
|
||||
menuButton.showsMenuAsPrimaryAction = true
|
||||
menuButton.menu = buildOptionsMenu()
|
||||
} else {
|
||||
menuButton.isHidden = true
|
||||
}
|
||||
|
||||
|
||||
controlsContainerView.addSubview(menuButton)
|
||||
menuButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
menuButton.topAnchor.constraint(equalTo: controlsContainerView.topAnchor, constant: 20),
|
||||
menuButton.trailingAnchor.constraint(equalTo: speedButton.leadingAnchor, constant: -20),
|
||||
menuButton.trailingAnchor.constraint(equalTo: qualityButton.leadingAnchor, constant: -20),
|
||||
menuButton.widthAnchor.constraint(equalToConstant: 40),
|
||||
menuButton.heightAnchor.constraint(equalToConstant: 40)
|
||||
])
|
||||
|
|
@ -553,23 +599,26 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
speedButton.tintColor = .white
|
||||
speedButton.showsMenuAsPrimaryAction = true
|
||||
speedButton.menu = speedChangerMenu()
|
||||
|
||||
|
||||
controlsContainerView.addSubview(speedButton)
|
||||
speedButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
// Middle
|
||||
speedButton.topAnchor.constraint(equalTo: controlsContainerView.topAnchor, constant: 20),
|
||||
speedButton.trailingAnchor.constraint(equalTo: qualityButton.leadingAnchor, constant: -20),
|
||||
speedButton.trailingAnchor.constraint(equalTo: controlsContainerView.trailingAnchor, constant: -20),
|
||||
speedButton.widthAnchor.constraint(equalToConstant: 40),
|
||||
speedButton.heightAnchor.constraint(equalToConstant: 40)
|
||||
])
|
||||
}
|
||||
|
||||
func setupWatchNextButton() {
|
||||
let config = UIImage.SymbolConfiguration(pointSize: 14, weight: .regular)
|
||||
let image = UIImage(systemName: "forward.fill", withConfiguration: config)
|
||||
|
||||
watchNextButton = UIButton(type: .system)
|
||||
watchNextButton.setTitle("Play Next", for: .normal)
|
||||
watchNextButton.setImage(UIImage(systemName: "forward.fill"), for: .normal)
|
||||
watchNextButton.setTitle(" Play Next", for: .normal)
|
||||
watchNextButton.titleLabel?.font = UIFont.systemFont(ofSize: 14)
|
||||
watchNextButton.setImage(image, for: .normal)
|
||||
watchNextButton.tintColor = .black
|
||||
watchNextButton.backgroundColor = .white
|
||||
watchNextButton.layer.cornerRadius = 25
|
||||
|
|
@ -591,17 +640,21 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
watchNextButtonControlsConstraints = [
|
||||
watchNextButton.trailingAnchor.constraint(equalTo: sliderHostingController!.view.trailingAnchor),
|
||||
watchNextButton.bottomAnchor.constraint(equalTo: sliderHostingController!.view.topAnchor, constant: -5),
|
||||
watchNextButton.heightAnchor.constraint(equalToConstant: 50),
|
||||
watchNextButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 120)
|
||||
watchNextButton.heightAnchor.constraint(equalToConstant: 47),
|
||||
watchNextButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 97)
|
||||
]
|
||||
|
||||
NSLayoutConstraint.activate(watchNextButtonNormalConstraints)
|
||||
}
|
||||
|
||||
func setupSkip85Button() {
|
||||
let config = UIImage.SymbolConfiguration(pointSize: 14, weight: .regular)
|
||||
let image = UIImage(systemName: "goforward", withConfiguration: config)
|
||||
|
||||
skip85Button = UIButton(type: .system)
|
||||
skip85Button.setTitle("Skip 85s", for: .normal)
|
||||
skip85Button.setImage(UIImage(systemName: "goforward"), for: .normal)
|
||||
skip85Button.setTitle(" Skip 85s", for: .normal)
|
||||
skip85Button.titleLabel?.font = UIFont.systemFont(ofSize: 14)
|
||||
skip85Button.setImage(image, for: .normal)
|
||||
skip85Button.tintColor = .black
|
||||
skip85Button.backgroundColor = .white
|
||||
skip85Button.layer.cornerRadius = 25
|
||||
|
|
@ -614,9 +667,9 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
|
||||
NSLayoutConstraint.activate([
|
||||
skip85Button.leadingAnchor.constraint(equalTo: sliderHostingController!.view.leadingAnchor),
|
||||
skip85Button.bottomAnchor.constraint(equalTo: sliderHostingController!.view.topAnchor, constant: -3),
|
||||
skip85Button.heightAnchor.constraint(equalToConstant: 50),
|
||||
skip85Button.widthAnchor.constraint(greaterThanOrEqualToConstant: 120)
|
||||
skip85Button.bottomAnchor.constraint(equalTo: sliderHostingController!.view.topAnchor, constant: -5),
|
||||
skip85Button.heightAnchor.constraint(equalToConstant: 47),
|
||||
skip85Button.widthAnchor.constraint(greaterThanOrEqualToConstant: 97)
|
||||
])
|
||||
}
|
||||
|
||||
|
|
@ -627,18 +680,17 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
qualityButton.showsMenuAsPrimaryAction = true
|
||||
qualityButton.menu = qualitySelectionMenu()
|
||||
qualityButton.isHidden = true
|
||||
|
||||
|
||||
controlsContainerView.addSubview(qualityButton)
|
||||
qualityButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
qualityButton.topAnchor.constraint(equalTo: controlsContainerView.topAnchor, constant: 20),
|
||||
qualityButton.trailingAnchor.constraint(equalTo: controlsContainerView.trailingAnchor, constant: -20),
|
||||
qualityButton.trailingAnchor.constraint(equalTo: speedButton.leadingAnchor, constant: -20),
|
||||
qualityButton.widthAnchor.constraint(equalToConstant: 40),
|
||||
qualityButton.heightAnchor.constraint(equalToConstant: 40)
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
func updateSubtitleLabelAppearance() {
|
||||
subtitleLabel.font = UIFont.systemFont(ofSize: CGFloat(subtitleFontSize))
|
||||
|
|
@ -666,7 +718,9 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
|
||||
func addTimeObserver() {
|
||||
let interval = CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
||||
timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in
|
||||
timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval,
|
||||
queue: .main)
|
||||
{ [weak self] time in
|
||||
guard let self = self,
|
||||
let currentItem = self.player.currentItem,
|
||||
currentItem.duration.seconds.isFinite else { return }
|
||||
|
|
@ -693,18 +747,24 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
|
||||
DispatchQueue.main.async {
|
||||
self.sliderHostingController?.rootView = MusicProgressSlider(
|
||||
value: Binding(get: { max(0, min(self.sliderViewModel.sliderValue, self.duration)) },
|
||||
set: { self.sliderViewModel.sliderValue = max(0, min($0, self.duration)) }),
|
||||
value: Binding(get: {
|
||||
max(0, min(self.sliderViewModel.sliderValue, self.duration))
|
||||
}, set: {
|
||||
self.sliderViewModel.sliderValue = max(0, min($0, self.duration))
|
||||
}),
|
||||
inRange: 0...(self.duration > 0 ? self.duration : 1.0),
|
||||
bufferValue: self.sliderViewModel.bufferValue,
|
||||
activeFillColor: .white,
|
||||
fillColor: .white.opacity(0.5),
|
||||
fillColor: .white.opacity(0.6),
|
||||
bufferColor: .white.opacity(0.36),
|
||||
emptyColor: .white.opacity(0.3),
|
||||
height: 30,
|
||||
onEditingChanged: { editing in
|
||||
self.isSliderEditing = editing
|
||||
if !editing {
|
||||
let seekTime = CMTime(seconds: self.sliderViewModel.sliderValue, preferredTimescale: 600)
|
||||
self.player.seek(to: seekTime)
|
||||
let targetTime = CMTime(seconds: self.sliderViewModel.sliderValue, preferredTimescale: 600)
|
||||
self.player.seek(to: targetTime) { [weak self] finished in
|
||||
self?.updateBufferValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -758,8 +818,6 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
func repositionWatchNextButton() {
|
||||
self.isWatchNextRepositioned = true
|
||||
|
|
@ -774,7 +832,6 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
self.watchNextButtonTimer?.invalidate()
|
||||
self.watchNextButtonTimer = nil
|
||||
}
|
||||
|
||||
|
||||
func startUpdateTimer() {
|
||||
updateTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
|
||||
|
|
@ -820,8 +877,11 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
let holdValue = UserDefaults.standard.double(forKey: "skipIncrementHold")
|
||||
let finalSkip = holdValue > 0 ? holdValue : 30
|
||||
currentTimeVal = max(currentTimeVal - finalSkip, 0)
|
||||
player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600))
|
||||
}
|
||||
player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) { [weak self] finished in
|
||||
guard let self = self else { return }
|
||||
self.updateBufferValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func seekForwardLongPress(_ gesture: UILongPressGestureRecognizer) {
|
||||
|
|
@ -829,23 +889,32 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
let holdValue = UserDefaults.standard.double(forKey: "skipIncrementHold")
|
||||
let finalSkip = holdValue > 0 ? holdValue : 30
|
||||
currentTimeVal = min(currentTimeVal + finalSkip, duration)
|
||||
player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600))
|
||||
}
|
||||
player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) { [weak self] finished in
|
||||
guard let self = self else { return }
|
||||
self.updateBufferValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func seekBackward() {
|
||||
let skipValue = UserDefaults.standard.double(forKey: "skipIncrement")
|
||||
let finalSkip = skipValue > 0 ? skipValue : 10
|
||||
currentTimeVal = max(currentTimeVal - finalSkip, 0)
|
||||
player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600))
|
||||
}
|
||||
player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) { [weak self] finished in
|
||||
guard let self = self else { return }
|
||||
self.updateBufferValue()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func seekForward() {
|
||||
let skipValue = UserDefaults.standard.double(forKey: "skipIncrement")
|
||||
let finalSkip = skipValue > 0 ? skipValue : 10
|
||||
currentTimeVal = min(currentTimeVal + finalSkip, duration)
|
||||
player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600))
|
||||
}
|
||||
player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) { [weak self] finished in
|
||||
guard let self = self else { return }
|
||||
self.updateBufferValue()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func handleDoubleTap(_ gesture: UITapGestureRecognizer) {
|
||||
let tapLocation = gesture.location(in: view)
|
||||
|
|
@ -857,7 +926,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
showSkipFeedback(direction: "forward")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func handleSwipeDown(_ gesture: UISwipeGestureRecognizer) {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
|
@ -881,11 +950,6 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
isPlaying.toggle()
|
||||
}
|
||||
|
||||
@objc func sliderEditingEnded() {
|
||||
let newTime = sliderViewModel.sliderValue
|
||||
player.seek(to: CMTime(seconds: newTime, preferredTimescale: 600))
|
||||
}
|
||||
|
||||
@objc func dismissTapped() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
|
@ -919,10 +983,13 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
var request = URLRequest(url: url)
|
||||
request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer")
|
||||
request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Origin")
|
||||
request.addValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", forHTTPHeaderField: "User-Agent")
|
||||
request.addValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
|
||||
forHTTPHeaderField: "User-Agent")
|
||||
|
||||
URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
|
||||
guard let self = self, let data = data, let content = String(data: data, encoding: .utf8) else {
|
||||
guard let self = self,
|
||||
let data = data,
|
||||
let content = String(data: data, encoding: .utf8) else {
|
||||
print("Failed to load m3u8 file")
|
||||
DispatchQueue.main.async {
|
||||
self?.qualities = []
|
||||
|
|
@ -948,7 +1015,8 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
for (index, line) in lines.enumerated() {
|
||||
if line.contains("#EXT-X-STREAM-INF"), index + 1 < lines.count {
|
||||
if let resolutionRange = line.range(of: "RESOLUTION="),
|
||||
let resolutionEndRange = line[resolutionRange.upperBound...].range(of: ",") ?? line[resolutionRange.upperBound...].range(of: "\n") {
|
||||
let resolutionEndRange = line[resolutionRange.upperBound...].range(of: ",")
|
||||
?? line[resolutionRange.upperBound...].range(of: "\n") {
|
||||
|
||||
let resolutionPart = String(line[resolutionRange.upperBound..<resolutionEndRange.lowerBound])
|
||||
if let heightStr = resolutionPart.components(separatedBy: "x").last,
|
||||
|
|
@ -961,7 +1029,8 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
if !nextLine.hasPrefix("http") && nextLine.contains(".m3u8") {
|
||||
if let baseURL = self.baseM3U8URL {
|
||||
let baseURLString = baseURL.deletingLastPathComponent().absoluteString
|
||||
qualityURL = URL(string: nextLine, relativeTo: baseURL)?.absoluteString ?? baseURLString + "/" + nextLine
|
||||
qualityURL = URL(string: nextLine, relativeTo: baseURL)?.absoluteString
|
||||
?? baseURLString + "/" + nextLine
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -992,7 +1061,8 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
}
|
||||
|
||||
private func switchToQuality(urlString: String) {
|
||||
guard let url = URL(string: urlString), currentQualityURL?.absoluteString != urlString else { return }
|
||||
guard let url = URL(string: urlString),
|
||||
currentQualityURL?.absoluteString != urlString else { return }
|
||||
|
||||
let currentTime = player.currentTime()
|
||||
let wasPlaying = player.rate > 0
|
||||
|
|
@ -1000,13 +1070,13 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
var request = URLRequest(url: url)
|
||||
request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer")
|
||||
request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Origin")
|
||||
request.addValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", forHTTPHeaderField: "User-Agent")
|
||||
request.addValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
|
||||
forHTTPHeaderField: "User-Agent")
|
||||
|
||||
let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": request.allHTTPHeaderFields ?? [:]])
|
||||
let playerItem = AVPlayerItem(asset: asset)
|
||||
|
||||
player.replaceCurrentItem(with: playerItem)
|
||||
|
||||
player.seek(to: currentTime)
|
||||
if wasPlaying {
|
||||
player.play()
|
||||
|
|
@ -1018,7 +1088,10 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
qualityButton.menu = qualitySelectionMenu()
|
||||
|
||||
if let selectedQuality = qualities.first(where: { $0.1 == urlString })?.0 {
|
||||
DropManager.shared.showDrop(title: "Quality: \(selectedQuality)", subtitle: "", duration: 0.5, icon: UIImage(systemName: "eye"))
|
||||
DropManager.shared.showDrop(title: "Quality: \(selectedQuality)",
|
||||
subtitle: "",
|
||||
duration: 0.5,
|
||||
icon: UIImage(systemName: "eye"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1207,7 +1280,9 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
]
|
||||
let paddingMenu = UIMenu(title: "Bottom Padding", children: paddingActions)
|
||||
|
||||
let subtitleOptionsMenu = UIMenu(title: "Subtitle Options", children: [subtitlesToggleAction, colorMenu, fontSizeMenu, shadowMenu, backgroundMenu, paddingMenu])
|
||||
let subtitleOptionsMenu = UIMenu(title: "Subtitle Options", children: [
|
||||
subtitlesToggleAction, colorMenu, fontSizeMenu, shadowMenu, backgroundMenu, paddingMenu
|
||||
])
|
||||
|
||||
menuElements = [subtitleOptionsMenu]
|
||||
}
|
||||
|
|
@ -1285,7 +1360,6 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
let audioSession = AVAudioSession.sharedInstance()
|
||||
try audioSession.setCategory(.playback, mode: .moviePlayback, options: .mixWithOthers)
|
||||
try audioSession.setActive(true)
|
||||
|
||||
try audioSession.overrideOutputAudioPort(.speaker)
|
||||
} catch {
|
||||
Logger.shared.log("Failed to set up AVAudioSession: \(error)")
|
||||
|
|
@ -1330,6 +1404,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// yes? Like the plural of the famous american rapper ye? -IBHRAD
|
||||
// low taper fade the meme is massive -cranci
|
||||
// cranci still doesnt have a job -seiike
|
||||
|
|
|
|||
Loading…
Reference in a new issue