mirror of
https://github.com/cranci1/Sora.git
synced 2026-01-11 20:10:24 +00:00
commits say it all (#66)
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:
parent
7fb6d2d92e
commit
1f3ea9c267
4 changed files with 383 additions and 252 deletions
|
|
@ -9,174 +9,134 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
|
||||
@Binding var value: T
|
||||
@Binding var bufferValue: T // NEW
|
||||
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 {
|
||||
// Base track + buffer indicator + current progress
|
||||
ZStack(alignment: .center) {
|
||||
|
||||
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)
|
||||
// Entire background track
|
||||
Capsule()
|
||||
.fill(emptyColor)
|
||||
|
||||
// 1) The buffer fill portion (behind the actual progress)
|
||||
Capsule() // NEW
|
||||
.fill(fillColor.opacity(0.3)) // or any "bufferColor"
|
||||
.mask({
|
||||
HStack {
|
||||
Rectangle()
|
||||
.frame(
|
||||
width: max(
|
||||
bounds.size.width * CGFloat(getPrgPercentage(bufferValue)),
|
||||
0
|
||||
),
|
||||
alignment: .leading
|
||||
)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
})
|
||||
|
||||
// 2) The actual playback progress
|
||||
Capsule()
|
||||
.fill(isActive ? activeFillColor : fillColor)
|
||||
.mask({
|
||||
HStack {
|
||||
Rectangle()
|
||||
.frame(
|
||||
width: max(
|
||||
bounds.size.width * CGFloat(localRealProgress + localTempProgress),
|
||||
0
|
||||
),
|
||||
alignment: .leading
|
||||
)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
.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)
|
||||
.updating($isActive) { _, state, _ in
|
||||
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)
|
||||
|
||||
// Time labels
|
||||
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)
|
||||
}
|
||||
.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)
|
||||
.updating($isActive) { _, state, _ in
|
||||
state = true
|
||||
}
|
||||
.onChanged { gesture in
|
||||
localTempProgress = T(gesture.translation.width / bounds.size.width)
|
||||
value = clampValue(getPrgValue())
|
||||
}
|
||||
.onEnded { _ in
|
||||
localRealProgress = getPrgPercentage(value)
|
||||
localTempProgress = 0
|
||||
}
|
||||
)
|
||||
.onChange(of: isActive) { newValue in
|
||||
value = clampValue(getPrgValue())
|
||||
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)
|
||||
}
|
||||
|
||||
private var animation: Animation {
|
||||
if isActive {
|
||||
return .spring()
|
||||
} else {
|
||||
return .spring(response: 0.5, dampingFraction: 0.5, blendDuration: 0.6)
|
||||
}
|
||||
isActive
|
||||
? .spring()
|
||||
: .spring(response: 0.5, dampingFraction: 0.5, blendDuration: 0.6)
|
||||
}
|
||||
|
||||
private func getPrgPercentage(_ value: T) -> T {
|
||||
private func clampValue(_ val: T) -> T {
|
||||
max(min(val, inRange.upperBound), inRange.lowerBound)
|
||||
}
|
||||
|
||||
private func getPrgPercentage(_ val: T) -> T {
|
||||
let clampedValue = clampValue(val)
|
||||
let range = inRange.upperBound - inRange.lowerBound
|
||||
let correctedStartValue = value - inRange.lowerBound
|
||||
let percentage = correctedStartValue / range
|
||||
return percentage
|
||||
let pct = (clampedValue - inRange.lowerBound) / range
|
||||
return max(min(pct, 1), 0)
|
||||
}
|
||||
|
||||
private func getPrgValue() -> T {
|
||||
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))
|
||||
((localRealProgress + localTempProgress) * (inRange.upperBound - inRange.lowerBound))
|
||||
+ inRange.lowerBound
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import MarqueeLabel
|
||||
import AVKit
|
||||
import SwiftUI
|
||||
import AVFoundation
|
||||
|
|
@ -55,6 +56,13 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
var lastDuration: Double = 0.0
|
||||
var watchNextButtonAppearedAt: Double?
|
||||
|
||||
// MARK: - Constraint Sets
|
||||
var portraitButtonVisibleConstraints: [NSLayoutConstraint] = []
|
||||
var portraitButtonHiddenConstraints: [NSLayoutConstraint] = []
|
||||
var landscapeButtonVisibleConstraints: [NSLayoutConstraint] = []
|
||||
var landscapeButtonHiddenConstraints: [NSLayoutConstraint] = []
|
||||
var currentMarqueeConstraints: [NSLayoutConstraint] = []
|
||||
|
||||
var subtitleForegroundColor: String = "white"
|
||||
var subtitleBackgroundEnabled: Bool = true
|
||||
var subtitleFontSize: Double = 20.0
|
||||
|
|
@ -66,6 +74,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
}
|
||||
}
|
||||
|
||||
var marqueeLabel: MarqueeLabel!
|
||||
var playerViewController: AVPlayerViewController!
|
||||
var controlsContainerView: UIView!
|
||||
var playPauseButton: UIImageView!
|
||||
|
|
@ -102,7 +111,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
|
||||
private var playerItemKVOContext = 0
|
||||
private var loadedTimeRangesObservation: NSKeyValueObservation?
|
||||
|
||||
private var playerTimeControlStatusObserver: NSKeyValueObservation?
|
||||
|
||||
init(module: ScrapingModule,
|
||||
urlString: String,
|
||||
|
|
@ -168,6 +177,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
setupSpeedButton()
|
||||
setupQualityButton()
|
||||
setupMenuButton()
|
||||
setupMarqueeLabel()
|
||||
setupSkip85Button()
|
||||
setupWatchNextButton()
|
||||
addTimeObserver()
|
||||
|
|
@ -179,7 +189,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
self?.updateBufferValue()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||
self?.checkForHLSStream()
|
||||
}
|
||||
|
|
@ -188,6 +198,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
holdForPause()
|
||||
}
|
||||
|
||||
|
||||
player.play()
|
||||
|
||||
if let url = subtitlesURL, !url.isEmpty {
|
||||
|
|
@ -203,6 +214,32 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
}
|
||||
}
|
||||
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
coordinator.animate(alongsideTransition: { _ in
|
||||
self.updateMarqueeConstraints()
|
||||
})
|
||||
}
|
||||
|
||||
/// In layoutSubviews, check if the text width is larger than the available space and update the label’s properties.
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
// Safely unwrap marqueeLabel
|
||||
guard let marqueeLabel = marqueeLabel else {
|
||||
return // or handle the error gracefully
|
||||
}
|
||||
|
||||
let availableWidth = marqueeLabel.frame.width
|
||||
let textWidth = marqueeLabel.intrinsicContentSize.width
|
||||
|
||||
if textWidth > availableWidth {
|
||||
marqueeLabel.lineBreakMode = .byTruncatingTail
|
||||
} else {
|
||||
marqueeLabel.lineBreakMode = .byClipping
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
NotificationCenter.default.addObserver(self,
|
||||
|
|
@ -371,19 +408,39 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
forwardButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let sliderView = MusicProgressSlider(
|
||||
value: Binding(get: { self.sliderViewModel.sliderValue },
|
||||
set: { self.sliderViewModel.sliderValue = $0 }),
|
||||
value: Binding(
|
||||
get: { self.sliderViewModel.sliderValue },
|
||||
set: { self.sliderViewModel.sliderValue = $0 }
|
||||
),
|
||||
bufferValue: Binding(
|
||||
get: { self.sliderViewModel.bufferValue }, // NEW
|
||||
set: { self.sliderViewModel.bufferValue = $0 } // NEW
|
||||
),
|
||||
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
|
||||
self.isSliderEditing = editing
|
||||
if !editing {
|
||||
self.player.seek(to: CMTime(seconds: self.sliderViewModel.sliderValue, preferredTimescale: 600))
|
||||
if editing {
|
||||
self.isSliderEditing = true
|
||||
} else {
|
||||
let wasPlaying = self.isPlaying
|
||||
let targetTime = CMTime(seconds: self.sliderViewModel.sliderValue,
|
||||
preferredTimescale: 600)
|
||||
self.player.seek(to: targetTime) { [weak self] finished in
|
||||
guard let self = self else { return }
|
||||
|
||||
let final = self.player.currentTime().seconds
|
||||
self.sliderViewModel.sliderValue = final
|
||||
self.currentTimeVal = final
|
||||
self.updateBufferValue()
|
||||
self.isSliderEditing = false
|
||||
|
||||
if wasPlaying {
|
||||
self.player.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -440,7 +497,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
emptyColor: .white.opacity(0.3),
|
||||
width: 22,
|
||||
onEditingChanged: { editing in
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
let brightnessContainer = UIView()
|
||||
|
|
@ -616,22 +673,90 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
dismissButton.widthAnchor.constraint(equalToConstant: 40),
|
||||
dismissButton.heightAnchor.constraint(equalToConstant: 40)
|
||||
])
|
||||
}
|
||||
|
||||
func setupMarqueeLabel() {
|
||||
// Create the MarqueeLabel and configure its scrolling behavior
|
||||
marqueeLabel = MarqueeLabel()
|
||||
marqueeLabel.text = "\(titleText) • Ep \(episodeNumber)"
|
||||
marqueeLabel.type = .continuous
|
||||
marqueeLabel.textColor = .white
|
||||
marqueeLabel.font = UIFont.systemFont(ofSize: 15, weight: .medium)
|
||||
|
||||
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
|
||||
marqueeLabel.speed = .rate(30) // Adjust scrolling speed as needed
|
||||
marqueeLabel.fadeLength = 10.0 // Fading at the label’s edges
|
||||
marqueeLabel.leadingBuffer = 1.0 // Left inset for scrolling
|
||||
marqueeLabel.trailingBuffer = 16.0 // Right inset for scrolling
|
||||
marqueeLabel.animationDelay = 2.5
|
||||
|
||||
controlsContainerView.addSubview(episodeLabel)
|
||||
episodeLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
// Set default lineBreakMode (will be updated later based on available width)
|
||||
marqueeLabel.lineBreakMode = .byTruncatingTail
|
||||
marqueeLabel.textAlignment = .left
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
episodeLabel.centerYAnchor.constraint(equalTo: dismissButton.centerYAnchor),
|
||||
episodeLabel.leadingAnchor.constraint(equalTo: dismissButton.trailingAnchor, constant: 12),
|
||||
episodeLabel.trailingAnchor.constraint(lessThanOrEqualTo: controlsContainerView.trailingAnchor, constant: -16)
|
||||
])
|
||||
controlsContainerView.addSubview(marqueeLabel)
|
||||
marqueeLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// Define four sets of constraints:
|
||||
// 1. Portrait mode with button visible
|
||||
portraitButtonVisibleConstraints = [
|
||||
marqueeLabel.leadingAnchor.constraint(equalTo: dismissButton.trailingAnchor, constant: 12),
|
||||
marqueeLabel.trailingAnchor.constraint(equalTo: menuButton.leadingAnchor, constant: -16),
|
||||
marqueeLabel.centerYAnchor.constraint(equalTo: dismissButton.centerYAnchor)
|
||||
]
|
||||
|
||||
// 2. Portrait mode with button hidden
|
||||
portraitButtonHiddenConstraints = [
|
||||
marqueeLabel.leadingAnchor.constraint(equalTo: dismissButton.trailingAnchor, constant: 12),
|
||||
marqueeLabel.trailingAnchor.constraint(equalTo: controlsContainerView.trailingAnchor, constant: -16),
|
||||
marqueeLabel.centerYAnchor.constraint(equalTo: dismissButton.centerYAnchor)
|
||||
]
|
||||
|
||||
// 3. Landscape mode with button visible (using smaller margins)
|
||||
landscapeButtonVisibleConstraints = [
|
||||
marqueeLabel.leadingAnchor.constraint(equalTo: dismissButton.trailingAnchor, constant: 8),
|
||||
marqueeLabel.trailingAnchor.constraint(equalTo: menuButton.leadingAnchor, constant: -8),
|
||||
marqueeLabel.centerYAnchor.constraint(equalTo: dismissButton.centerYAnchor)
|
||||
]
|
||||
|
||||
// 4. Landscape mode with button hidden
|
||||
landscapeButtonHiddenConstraints = [
|
||||
marqueeLabel.leadingAnchor.constraint(equalTo: dismissButton.trailingAnchor, constant: 8),
|
||||
marqueeLabel.trailingAnchor.constraint(equalTo: controlsContainerView.trailingAnchor, constant: -8),
|
||||
marqueeLabel.centerYAnchor.constraint(equalTo: dismissButton.centerYAnchor)
|
||||
]
|
||||
|
||||
// Activate an initial set based on the current orientation and menuButton state
|
||||
updateMarqueeConstraints()
|
||||
}
|
||||
|
||||
func updateMarqueeConstraints() {
|
||||
// First, remove any existing marquee constraints.
|
||||
NSLayoutConstraint.deactivate(currentMarqueeConstraints)
|
||||
|
||||
// Decide on spacing constants based on orientation.
|
||||
let isPortrait = UIDevice.current.orientation.isPortrait || view.bounds.height > view.bounds.width
|
||||
let leftSpacing: CGFloat = isPortrait ? 2 : 1
|
||||
let rightSpacing: CGFloat = isPortrait ? 16 : 8
|
||||
|
||||
// Determine which button to use for the trailing anchor.
|
||||
var trailingAnchor: NSLayoutXAxisAnchor = controlsContainerView.trailingAnchor // default fallback
|
||||
if let menu = menuButton, !menu.isHidden {
|
||||
trailingAnchor = menu.leadingAnchor
|
||||
} else if let quality = qualityButton, !quality.isHidden {
|
||||
trailingAnchor = quality.leadingAnchor
|
||||
} else if let speed = speedButton, !speed.isHidden {
|
||||
trailingAnchor = speed.leadingAnchor
|
||||
}
|
||||
|
||||
// Create new constraints for the marquee label.
|
||||
currentMarqueeConstraints = [
|
||||
marqueeLabel.leadingAnchor.constraint(equalTo: dismissButton.trailingAnchor, constant: leftSpacing),
|
||||
marqueeLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -rightSpacing),
|
||||
marqueeLabel.centerYAnchor.constraint(equalTo: dismissButton.centerYAnchor)
|
||||
]
|
||||
|
||||
NSLayoutConstraint.activate(currentMarqueeConstraints)
|
||||
view.layoutIfNeeded()
|
||||
}
|
||||
|
||||
func setupMenuButton() {
|
||||
|
|
@ -789,6 +914,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
let currentItem = self.player.currentItem,
|
||||
currentItem.duration.seconds.isFinite else { return }
|
||||
|
||||
self.updateBufferValue()
|
||||
let currentDuration = currentItem.duration.seconds
|
||||
if currentDuration.isNaN || currentDuration <= 0 { return }
|
||||
|
||||
|
|
@ -811,21 +937,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))
|
||||
}),
|
||||
inRange: 0...(self.duration > 0 ? self.duration : 1.0),
|
||||
bufferValue: self.sliderViewModel.bufferValue,
|
||||
value: Binding(
|
||||
get: { max(0, min(self.sliderViewModel.sliderValue, self.duration)) },
|
||||
set: {
|
||||
self.sliderViewModel.sliderValue = max(0, min($0, self.duration))
|
||||
}
|
||||
),
|
||||
bufferValue: Binding(get: { self.sliderViewModel.bufferValue },
|
||||
set: { self.sliderViewModel.bufferValue = $0 }), inRange: 0...(self.duration > 0 ? self.duration : 1.0),
|
||||
activeFillColor: .white,
|
||||
fillColor: .white.opacity(0.6),
|
||||
bufferColor: .white.opacity(0.36),
|
||||
emptyColor: .white.opacity(0.3),
|
||||
height: 30,
|
||||
onEditingChanged: { editing in
|
||||
if !editing {
|
||||
let targetTime = CMTime(seconds: self.sliderViewModel.sliderValue, preferredTimescale: 600)
|
||||
let targetTime = CMTime(
|
||||
seconds: self.sliderViewModel.sliderValue,
|
||||
preferredTimescale: 600
|
||||
)
|
||||
self.player.seek(to: targetTime) { [weak self] finished in
|
||||
self?.updateBufferValue()
|
||||
}
|
||||
|
|
@ -835,9 +964,9 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
}
|
||||
|
||||
let isNearEnd = (self.duration - self.currentTimeVal) <= (self.duration * 0.10)
|
||||
&& self.currentTimeVal != self.duration
|
||||
&& self.showWatchNextButton
|
||||
&& self.duration != 0
|
||||
&& self.currentTimeVal != self.duration
|
||||
&& self.showWatchNextButton
|
||||
&& self.duration != 0
|
||||
|
||||
if isNearEnd {
|
||||
if !self.isWatchNextVisible {
|
||||
|
|
@ -942,10 +1071,10 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
let finalSkip = holdValue > 0 ? holdValue : 30
|
||||
currentTimeVal = max(currentTimeVal - finalSkip, 0)
|
||||
player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) { [weak self] finished in
|
||||
guard let self = self else { return }
|
||||
self.updateBufferValue()
|
||||
}
|
||||
}
|
||||
guard let self = self else { return }
|
||||
self.updateBufferValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func seekForwardLongPress(_ gesture: UILongPressGestureRecognizer) {
|
||||
|
|
@ -954,10 +1083,10 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
let finalSkip = holdValue > 0 ? holdValue : 30
|
||||
currentTimeVal = min(currentTimeVal + finalSkip, duration)
|
||||
player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) { [weak self] finished in
|
||||
guard let self = self else { return }
|
||||
self.updateBufferValue()
|
||||
}
|
||||
}
|
||||
guard let self = self else { return }
|
||||
self.updateBufferValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func seekBackward() {
|
||||
|
|
@ -965,20 +1094,20 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
let finalSkip = skipValue > 0 ? skipValue : 10
|
||||
currentTimeVal = max(currentTimeVal - finalSkip, 0)
|
||||
player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) { [weak self] finished in
|
||||
guard let self = self else { return }
|
||||
self.updateBufferValue()
|
||||
}
|
||||
}
|
||||
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)) { [weak self] finished in
|
||||
guard let self = self else { return }
|
||||
self.updateBufferValue()
|
||||
}
|
||||
}
|
||||
guard let self = self else { return }
|
||||
self.updateBufferValue()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func handleDoubleTap(_ gesture: UITapGestureRecognizer) {
|
||||
let tapLocation = gesture.location(in: view)
|
||||
|
|
@ -997,21 +1126,23 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
|
||||
@objc func togglePlayPause() {
|
||||
if isPlaying {
|
||||
player.pause()
|
||||
isPlaying = false
|
||||
playPauseButton.image = UIImage(systemName: "play.fill")
|
||||
|
||||
if !isControlsVisible {
|
||||
isControlsVisible = true
|
||||
UIView.animate(withDuration: 0.5) {
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
self.controlsContainerView.alpha = 1.0
|
||||
self.skip85Button.alpha = 0.8
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
player.pause()
|
||||
playPauseButton.image = UIImage(systemName: "play.fill")
|
||||
} else {
|
||||
player.play()
|
||||
isPlaying = true
|
||||
playPauseButton.image = UIImage(systemName: "pause.fill")
|
||||
}
|
||||
isPlaying.toggle()
|
||||
}
|
||||
|
||||
@objc func dismissTapped() {
|
||||
|
|
@ -1032,7 +1163,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
|
||||
@objc private func handleHoldForPause(_ gesture: UILongPressGestureRecognizer) {
|
||||
guard isHoldPauseEnabled else { return }
|
||||
|
||||
|
||||
if gesture.state == .began {
|
||||
togglePlayPause()
|
||||
}
|
||||
|
|
@ -1088,7 +1219,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
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") {
|
||||
?? line[resolutionRange.upperBound...].range(of: "\n") {
|
||||
|
||||
let resolutionPart = String(line[resolutionRange.upperBound..<resolutionEndRange.lowerBound])
|
||||
if let heightStr = resolutionPart.components(separatedBy: "x").last,
|
||||
|
|
@ -1102,7 +1233,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
if let baseURL = self.baseM3U8URL {
|
||||
let baseURLString = baseURL.deletingLastPathComponent().absoluteString
|
||||
qualityURL = URL(string: nextLine, relativeTo: baseURL)?.absoluteString
|
||||
?? baseURLString + "/" + nextLine
|
||||
?? baseURLString + "/" + nextLine
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1222,6 +1353,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
|
||||
self.qualityButton.isHidden = false
|
||||
self.qualityButton.menu = self.qualitySelectionMenu()
|
||||
self.updateMarqueeConstraints()
|
||||
}
|
||||
} else {
|
||||
isHLSStream = false
|
||||
|
|
@ -1487,6 +1619,20 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
player?.rate = lastPlayedSpeed > 0 ? lastPlayedSpeed : 1.0
|
||||
}
|
||||
}
|
||||
|
||||
func setupTimeControlStatusObservation() {
|
||||
playerTimeControlStatusObserver = player.observe(\.timeControlStatus, options: [.new]) { [weak self] player, _ in
|
||||
guard let self = self else { return }
|
||||
if player.timeControlStatus == .paused,
|
||||
let reason = player.reasonForWaitingToPlay {
|
||||
// If we are paused for a “stall/minimize stalls” reason, forcibly resume:
|
||||
Logger.shared.log("Paused reason: \(reason)", type: "Error")
|
||||
if reason == .toMinimizeStalls || reason == .evaluatingBufferingRate {
|
||||
player.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@
|
|||
13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */; };
|
||||
13EA2BD62D32D97400C1EBD7 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */; };
|
||||
13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */; };
|
||||
1E048A722DA3262900D9DD3F /* MarqueeLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 1E048A712DA3262900D9DD3F /* MarqueeLabel */; };
|
||||
1E3F5EC82D9F16B7003F310F /* VerticalBrightnessSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E3F5EC72D9F16B7003F310F /* VerticalBrightnessSlider.swift */; };
|
||||
1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */; };
|
||||
1EAC7A322D888BC50083984D /* MusicProgressSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */; };
|
||||
|
|
@ -135,6 +136,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1E048A722DA3262900D9DD3F /* MarqueeLabel in Frameworks */,
|
||||
132E35232D959E410007800E /* Kingfisher in Frameworks */,
|
||||
132E35202D959E1D0007800E /* FFmpeg-iOS-Lame in Frameworks */,
|
||||
132E351D2D959DDB0007800E /* Drops in Frameworks */,
|
||||
|
|
@ -469,6 +471,7 @@
|
|||
132E351C2D959DDB0007800E /* Drops */,
|
||||
132E351F2D959E1D0007800E /* FFmpeg-iOS-Lame */,
|
||||
132E35222D959E410007800E /* Kingfisher */,
|
||||
1E048A712DA3262900D9DD3F /* MarqueeLabel */,
|
||||
);
|
||||
productName = Sora;
|
||||
productReference = 133D7C6A2D2BE2500075467E /* Sulfur.app */;
|
||||
|
|
@ -501,6 +504,7 @@
|
|||
132E351B2D959DDB0007800E /* XCRemoteSwiftPackageReference "Drops" */,
|
||||
132E351E2D959E1D0007800E /* XCRemoteSwiftPackageReference "FFmpeg-iOS-Lame" */,
|
||||
132E35212D959E410007800E /* XCRemoteSwiftPackageReference "Kingfisher" */,
|
||||
1E048A702DA3262900D9DD3F /* XCRemoteSwiftPackageReference "MarqueeLabel" */,
|
||||
);
|
||||
productRefGroup = 133D7C6B2D2BE2500075467E /* Products */;
|
||||
projectDirPath = "";
|
||||
|
|
@ -838,6 +842,14 @@
|
|||
version = 7.9.1;
|
||||
};
|
||||
};
|
||||
1E048A702DA3262900D9DD3F /* XCRemoteSwiftPackageReference "MarqueeLabel" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/cbpowell/MarqueeLabel";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 4.5.0;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
|
|
@ -856,6 +868,11 @@
|
|||
package = 132E35212D959E410007800E /* XCRemoteSwiftPackageReference "Kingfisher" */;
|
||||
productName = Kingfisher;
|
||||
};
|
||||
1E048A712DA3262900D9DD3F /* MarqueeLabel */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 1E048A702DA3262900D9DD3F /* XCRemoteSwiftPackageReference "MarqueeLabel" */;
|
||||
productName = MarqueeLabel;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 133D7C622D2BE2500075467E /* Project object */;
|
||||
|
|
|
|||
|
|
@ -1,43 +1,51 @@
|
|||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "Drops",
|
||||
"repositoryURL": "https://github.com/omaralbeik/Drops.git",
|
||||
"state": {
|
||||
"branch": "main",
|
||||
"revision": "5824681795286c36bdc4a493081a63e64e2a064e",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "FFmpeg-iOS-Lame",
|
||||
"repositoryURL": "https://github.com/kewlbear/FFmpeg-iOS-Lame",
|
||||
"state": {
|
||||
"branch": "main",
|
||||
"revision": "1808fa5a1263c5e216646cd8421fc7dcb70520cc",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "FFmpeg-iOS-Support",
|
||||
"repositoryURL": "https://github.com/kewlbear/FFmpeg-iOS-Support",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "be3bd9149ac53760e8725652eee99c405b2be47a",
|
||||
"version": "0.0.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Kingfisher",
|
||||
"repositoryURL": "https://github.com/onevcat/Kingfisher.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "b6f62758f21a8c03cd64f4009c037cfa580a256e",
|
||||
"version": "7.9.1"
|
||||
}
|
||||
"originHash" : "e772caa8d6a8793d24bf04e3d77695cd5ac695f3605d2b657e40115caedf8863",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "drops",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/omaralbeik/Drops.git",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "5824681795286c36bdc4a493081a63e64e2a064e"
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"identity" : "ffmpeg-ios-lame",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kewlbear/FFmpeg-iOS-Lame",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "1808fa5a1263c5e216646cd8421fc7dcb70520cc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "ffmpeg-ios-support",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kewlbear/FFmpeg-iOS-Support",
|
||||
"state" : {
|
||||
"revision" : "be3bd9149ac53760e8725652eee99c405b2be47a",
|
||||
"version" : "0.0.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "kingfisher",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/onevcat/Kingfisher.git",
|
||||
"state" : {
|
||||
"revision" : "b6f62758f21a8c03cd64f4009c037cfa580a256e",
|
||||
"version" : "7.9.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "marqueelabel",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/cbpowell/MarqueeLabel",
|
||||
"state" : {
|
||||
"revision" : "877e810534cda9afabb8143ae319b7c3341b121b",
|
||||
"version" : "4.5.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue