mirror of
https://github.com/cranci1/Sora.git
synced 2026-03-11 17:45:37 +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
|
import SwiftUI
|
||||||
|
|
||||||
struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
|
struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
|
||||||
@Binding var value: T
|
@Binding var value: T
|
||||||
let inRange: ClosedRange<T>
|
@Binding var bufferValue: T // NEW
|
||||||
|
let inRange: ClosedRange<T>
|
||||||
let bufferValue: T
|
|
||||||
let activeFillColor: Color
|
let activeFillColor: Color
|
||||||
let fillColor: Color
|
let fillColor: Color
|
||||||
let bufferColor: Color
|
let emptyColor: Color
|
||||||
let emptyColor: Color
|
let height: CGFloat
|
||||||
let height: CGFloat
|
|
||||||
|
let onEditingChanged: (Bool) -> Void
|
||||||
let onEditingChanged: (Bool) -> Void
|
|
||||||
|
@State private var localRealProgress: T = 0
|
||||||
@State private var localRealProgress: T = 0
|
@State private var localTempProgress: T = 0
|
||||||
@State private var localTempProgress: T = 0
|
@GestureState private var isActive: Bool = false
|
||||||
@GestureState private var isActive: Bool = false
|
|
||||||
|
var body: some View {
|
||||||
var body: some View {
|
GeometryReader { bounds in
|
||||||
GeometryReader { bounds in
|
ZStack {
|
||||||
ZStack {
|
VStack {
|
||||||
VStack {
|
// Base track + buffer indicator + current progress
|
||||||
ZStack(alignment: .center) {
|
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 {
|
// Entire background track
|
||||||
let shouldShowHours = inRange.upperBound >= 3600
|
Capsule()
|
||||||
Text(value.asTimeString(style: .positional, showHours: shouldShowHours))
|
.fill(emptyColor)
|
||||||
Spacer(minLength: 0)
|
|
||||||
Text("-" + (inRange.upperBound - value).asTimeString(
|
// 1) The buffer fill portion (behind the actual progress)
|
||||||
style: .positional,
|
Capsule() // NEW
|
||||||
showHours: shouldShowHours
|
.fill(fillColor.opacity(0.3)) // or any "bufferColor"
|
||||||
))
|
.mask({
|
||||||
}
|
HStack {
|
||||||
.font(.system(size: 12))
|
Rectangle()
|
||||||
.foregroundColor(isActive ? fillColor : emptyColor)
|
.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,
|
// Time labels
|
||||||
alignment: .center
|
HStack {
|
||||||
)
|
let shouldShowHours = inRange.upperBound >= 3600
|
||||||
.animation(animation, value: isActive)
|
Text(value.asTimeString(style: .positional, showHours: shouldShowHours))
|
||||||
}
|
Spacer(minLength: 0)
|
||||||
.frame(
|
Text("-" + (inRange.upperBound - value)
|
||||||
width: bounds.size.width,
|
.asTimeString(style: .positional, showHours: shouldShowHours))
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
.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)
|
.frame(height: isActive ? height * 1.25 : height, alignment: .center)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var animation: Animation {
|
private var animation: Animation {
|
||||||
if isActive {
|
isActive
|
||||||
return .spring()
|
? .spring()
|
||||||
} else {
|
: .spring(response: 0.5, dampingFraction: 0.5, blendDuration: 0.6)
|
||||||
return .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 range = inRange.upperBound - inRange.lowerBound
|
||||||
let correctedStartValue = value - inRange.lowerBound
|
let pct = (clampedValue - inRange.lowerBound) / range
|
||||||
let percentage = correctedStartValue / range
|
return max(min(pct, 1), 0)
|
||||||
return percentage
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getPrgValue() -> T {
|
private func getPrgValue() -> T {
|
||||||
return ((localRealProgress + localTempProgress) * (inRange.upperBound - inRange.lowerBound)) + inRange.lowerBound}
|
((localRealProgress + localTempProgress) * (inRange.upperBound - inRange.lowerBound))
|
||||||
// MARK: - Helpers
|
+ inRange.lowerBound
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import MarqueeLabel
|
||||||
import AVKit
|
import AVKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
|
@ -55,6 +56,13 @@ class CustomMediaPlayerViewController: UIViewController {
|
||||||
var lastDuration: Double = 0.0
|
var lastDuration: Double = 0.0
|
||||||
var watchNextButtonAppearedAt: Double?
|
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 subtitleForegroundColor: String = "white"
|
||||||
var subtitleBackgroundEnabled: Bool = true
|
var subtitleBackgroundEnabled: Bool = true
|
||||||
var subtitleFontSize: Double = 20.0
|
var subtitleFontSize: Double = 20.0
|
||||||
|
|
@ -66,6 +74,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var marqueeLabel: MarqueeLabel!
|
||||||
var playerViewController: AVPlayerViewController!
|
var playerViewController: AVPlayerViewController!
|
||||||
var controlsContainerView: UIView!
|
var controlsContainerView: UIView!
|
||||||
var playPauseButton: UIImageView!
|
var playPauseButton: UIImageView!
|
||||||
|
|
@ -102,7 +111,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
||||||
|
|
||||||
private var playerItemKVOContext = 0
|
private var playerItemKVOContext = 0
|
||||||
private var loadedTimeRangesObservation: NSKeyValueObservation?
|
private var loadedTimeRangesObservation: NSKeyValueObservation?
|
||||||
|
private var playerTimeControlStatusObserver: NSKeyValueObservation?
|
||||||
|
|
||||||
init(module: ScrapingModule,
|
init(module: ScrapingModule,
|
||||||
urlString: String,
|
urlString: String,
|
||||||
|
|
@ -168,6 +177,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
||||||
setupSpeedButton()
|
setupSpeedButton()
|
||||||
setupQualityButton()
|
setupQualityButton()
|
||||||
setupMenuButton()
|
setupMenuButton()
|
||||||
|
setupMarqueeLabel()
|
||||||
setupSkip85Button()
|
setupSkip85Button()
|
||||||
setupWatchNextButton()
|
setupWatchNextButton()
|
||||||
addTimeObserver()
|
addTimeObserver()
|
||||||
|
|
@ -179,7 +189,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
||||||
self?.updateBufferValue()
|
self?.updateBufferValue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||||
self?.checkForHLSStream()
|
self?.checkForHLSStream()
|
||||||
}
|
}
|
||||||
|
|
@ -188,6 +198,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
||||||
holdForPause()
|
holdForPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
player.play()
|
player.play()
|
||||||
|
|
||||||
if let url = subtitlesURL, !url.isEmpty {
|
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) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
NotificationCenter.default.addObserver(self,
|
NotificationCenter.default.addObserver(self,
|
||||||
|
|
@ -371,19 +408,39 @@ class CustomMediaPlayerViewController: UIViewController {
|
||||||
forwardButton.translatesAutoresizingMaskIntoConstraints = false
|
forwardButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
let sliderView = MusicProgressSlider(
|
let sliderView = MusicProgressSlider(
|
||||||
value: Binding(get: { self.sliderViewModel.sliderValue },
|
value: Binding(
|
||||||
set: { self.sliderViewModel.sliderValue = $0 }),
|
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),
|
inRange: 0...(duration > 0 ? duration : 1.0),
|
||||||
bufferValue: self.sliderViewModel.bufferValue,
|
|
||||||
activeFillColor: .white,
|
activeFillColor: .white,
|
||||||
fillColor: .white.opacity(0.5),
|
fillColor: .white.opacity(0.5),
|
||||||
bufferColor: .white.opacity(0.2),
|
|
||||||
emptyColor: .white.opacity(0.3),
|
emptyColor: .white.opacity(0.3),
|
||||||
height: 30,
|
height: 30,
|
||||||
onEditingChanged: { editing in
|
onEditingChanged: { editing in
|
||||||
self.isSliderEditing = editing
|
if editing {
|
||||||
if !editing {
|
self.isSliderEditing = true
|
||||||
self.player.seek(to: CMTime(seconds: self.sliderViewModel.sliderValue, preferredTimescale: 600))
|
} 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),
|
emptyColor: .white.opacity(0.3),
|
||||||
width: 22,
|
width: 22,
|
||||||
onEditingChanged: { editing in
|
onEditingChanged: { editing in
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
let brightnessContainer = UIView()
|
let brightnessContainer = UIView()
|
||||||
|
|
@ -616,22 +673,90 @@ class CustomMediaPlayerViewController: UIViewController {
|
||||||
dismissButton.widthAnchor.constraint(equalToConstant: 40),
|
dismissButton.widthAnchor.constraint(equalToConstant: 40),
|
||||||
dismissButton.heightAnchor.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()
|
marqueeLabel.speed = .rate(30) // Adjust scrolling speed as needed
|
||||||
episodeLabel.text = "\(titleText) • Ep \(episodeNumber)"
|
marqueeLabel.fadeLength = 10.0 // Fading at the label’s edges
|
||||||
episodeLabel.textColor = .white
|
marqueeLabel.leadingBuffer = 1.0 // Left inset for scrolling
|
||||||
episodeLabel.font = UIFont.systemFont(ofSize: 15, weight: .medium)
|
marqueeLabel.trailingBuffer = 16.0 // Right inset for scrolling
|
||||||
episodeLabel.numberOfLines = 1
|
marqueeLabel.animationDelay = 2.5
|
||||||
episodeLabel.lineBreakMode = .byTruncatingTail
|
|
||||||
|
|
||||||
controlsContainerView.addSubview(episodeLabel)
|
// Set default lineBreakMode (will be updated later based on available width)
|
||||||
episodeLabel.translatesAutoresizingMaskIntoConstraints = false
|
marqueeLabel.lineBreakMode = .byTruncatingTail
|
||||||
|
marqueeLabel.textAlignment = .left
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
controlsContainerView.addSubview(marqueeLabel)
|
||||||
episodeLabel.centerYAnchor.constraint(equalTo: dismissButton.centerYAnchor),
|
marqueeLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
episodeLabel.leadingAnchor.constraint(equalTo: dismissButton.trailingAnchor, constant: 12),
|
|
||||||
episodeLabel.trailingAnchor.constraint(lessThanOrEqualTo: controlsContainerView.trailingAnchor, constant: -16)
|
// 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() {
|
func setupMenuButton() {
|
||||||
|
|
@ -789,6 +914,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
||||||
let currentItem = self.player.currentItem,
|
let currentItem = self.player.currentItem,
|
||||||
currentItem.duration.seconds.isFinite else { return }
|
currentItem.duration.seconds.isFinite else { return }
|
||||||
|
|
||||||
|
self.updateBufferValue()
|
||||||
let currentDuration = currentItem.duration.seconds
|
let currentDuration = currentItem.duration.seconds
|
||||||
if currentDuration.isNaN || currentDuration <= 0 { return }
|
if currentDuration.isNaN || currentDuration <= 0 { return }
|
||||||
|
|
||||||
|
|
@ -811,21 +937,24 @@ class CustomMediaPlayerViewController: UIViewController {
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.sliderHostingController?.rootView = MusicProgressSlider(
|
self.sliderHostingController?.rootView = MusicProgressSlider(
|
||||||
value: Binding(get: {
|
value: Binding(
|
||||||
max(0, min(self.sliderViewModel.sliderValue, self.duration))
|
get: { max(0, min(self.sliderViewModel.sliderValue, self.duration)) },
|
||||||
}, set: {
|
set: {
|
||||||
self.sliderViewModel.sliderValue = max(0, min($0, self.duration))
|
self.sliderViewModel.sliderValue = max(0, min($0, self.duration))
|
||||||
}),
|
}
|
||||||
inRange: 0...(self.duration > 0 ? self.duration : 1.0),
|
),
|
||||||
bufferValue: self.sliderViewModel.bufferValue,
|
bufferValue: Binding(get: { self.sliderViewModel.bufferValue },
|
||||||
|
set: { self.sliderViewModel.bufferValue = $0 }), inRange: 0...(self.duration > 0 ? self.duration : 1.0),
|
||||||
activeFillColor: .white,
|
activeFillColor: .white,
|
||||||
fillColor: .white.opacity(0.6),
|
fillColor: .white.opacity(0.6),
|
||||||
bufferColor: .white.opacity(0.36),
|
|
||||||
emptyColor: .white.opacity(0.3),
|
emptyColor: .white.opacity(0.3),
|
||||||
height: 30,
|
height: 30,
|
||||||
onEditingChanged: { editing in
|
onEditingChanged: { editing in
|
||||||
if !editing {
|
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.player.seek(to: targetTime) { [weak self] finished in
|
||||||
self?.updateBufferValue()
|
self?.updateBufferValue()
|
||||||
}
|
}
|
||||||
|
|
@ -835,9 +964,9 @@ class CustomMediaPlayerViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
let isNearEnd = (self.duration - self.currentTimeVal) <= (self.duration * 0.10)
|
let isNearEnd = (self.duration - self.currentTimeVal) <= (self.duration * 0.10)
|
||||||
&& self.currentTimeVal != self.duration
|
&& self.currentTimeVal != self.duration
|
||||||
&& self.showWatchNextButton
|
&& self.showWatchNextButton
|
||||||
&& self.duration != 0
|
&& self.duration != 0
|
||||||
|
|
||||||
if isNearEnd {
|
if isNearEnd {
|
||||||
if !self.isWatchNextVisible {
|
if !self.isWatchNextVisible {
|
||||||
|
|
@ -942,10 +1071,10 @@ class CustomMediaPlayerViewController: UIViewController {
|
||||||
let finalSkip = holdValue > 0 ? holdValue : 30
|
let finalSkip = holdValue > 0 ? holdValue : 30
|
||||||
currentTimeVal = max(currentTimeVal - finalSkip, 0)
|
currentTimeVal = max(currentTimeVal - finalSkip, 0)
|
||||||
player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) { [weak self] finished in
|
player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) { [weak self] finished in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.updateBufferValue()
|
self.updateBufferValue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func seekForwardLongPress(_ gesture: UILongPressGestureRecognizer) {
|
@objc func seekForwardLongPress(_ gesture: UILongPressGestureRecognizer) {
|
||||||
|
|
@ -954,10 +1083,10 @@ class CustomMediaPlayerViewController: UIViewController {
|
||||||
let finalSkip = holdValue > 0 ? holdValue : 30
|
let finalSkip = holdValue > 0 ? holdValue : 30
|
||||||
currentTimeVal = min(currentTimeVal + finalSkip, duration)
|
currentTimeVal = min(currentTimeVal + finalSkip, duration)
|
||||||
player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) { [weak self] finished in
|
player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) { [weak self] finished in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.updateBufferValue()
|
self.updateBufferValue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func seekBackward() {
|
@objc func seekBackward() {
|
||||||
|
|
@ -965,20 +1094,20 @@ class CustomMediaPlayerViewController: UIViewController {
|
||||||
let finalSkip = skipValue > 0 ? skipValue : 10
|
let finalSkip = skipValue > 0 ? skipValue : 10
|
||||||
currentTimeVal = max(currentTimeVal - finalSkip, 0)
|
currentTimeVal = max(currentTimeVal - finalSkip, 0)
|
||||||
player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) { [weak self] finished in
|
player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) { [weak self] finished in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.updateBufferValue()
|
self.updateBufferValue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func seekForward() {
|
@objc func seekForward() {
|
||||||
let skipValue = UserDefaults.standard.double(forKey: "skipIncrement")
|
let skipValue = UserDefaults.standard.double(forKey: "skipIncrement")
|
||||||
let finalSkip = skipValue > 0 ? skipValue : 10
|
let finalSkip = skipValue > 0 ? skipValue : 10
|
||||||
currentTimeVal = min(currentTimeVal + finalSkip, duration)
|
currentTimeVal = min(currentTimeVal + finalSkip, duration)
|
||||||
player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) { [weak self] finished in
|
player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) { [weak self] finished in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.updateBufferValue()
|
self.updateBufferValue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func handleDoubleTap(_ gesture: UITapGestureRecognizer) {
|
@objc func handleDoubleTap(_ gesture: UITapGestureRecognizer) {
|
||||||
let tapLocation = gesture.location(in: view)
|
let tapLocation = gesture.location(in: view)
|
||||||
|
|
@ -997,21 +1126,23 @@ class CustomMediaPlayerViewController: UIViewController {
|
||||||
|
|
||||||
@objc func togglePlayPause() {
|
@objc func togglePlayPause() {
|
||||||
if isPlaying {
|
if isPlaying {
|
||||||
|
player.pause()
|
||||||
|
isPlaying = false
|
||||||
|
playPauseButton.image = UIImage(systemName: "play.fill")
|
||||||
|
|
||||||
if !isControlsVisible {
|
if !isControlsVisible {
|
||||||
isControlsVisible = true
|
isControlsVisible = true
|
||||||
UIView.animate(withDuration: 0.5) {
|
UIView.animate(withDuration: 0.2) {
|
||||||
self.controlsContainerView.alpha = 1.0
|
self.controlsContainerView.alpha = 1.0
|
||||||
self.skip85Button.alpha = 0.8
|
self.skip85Button.alpha = 0.8
|
||||||
self.view.layoutIfNeeded()
|
self.view.layoutIfNeeded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
player.pause()
|
|
||||||
playPauseButton.image = UIImage(systemName: "play.fill")
|
|
||||||
} else {
|
} else {
|
||||||
player.play()
|
player.play()
|
||||||
|
isPlaying = true
|
||||||
playPauseButton.image = UIImage(systemName: "pause.fill")
|
playPauseButton.image = UIImage(systemName: "pause.fill")
|
||||||
}
|
}
|
||||||
isPlaying.toggle()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func dismissTapped() {
|
@objc func dismissTapped() {
|
||||||
|
|
@ -1032,7 +1163,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
||||||
|
|
||||||
@objc private func handleHoldForPause(_ gesture: UILongPressGestureRecognizer) {
|
@objc private func handleHoldForPause(_ gesture: UILongPressGestureRecognizer) {
|
||||||
guard isHoldPauseEnabled else { return }
|
guard isHoldPauseEnabled else { return }
|
||||||
|
|
||||||
if gesture.state == .began {
|
if gesture.state == .began {
|
||||||
togglePlayPause()
|
togglePlayPause()
|
||||||
}
|
}
|
||||||
|
|
@ -1088,7 +1219,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
||||||
if line.contains("#EXT-X-STREAM-INF"), index + 1 < lines.count {
|
if line.contains("#EXT-X-STREAM-INF"), index + 1 < lines.count {
|
||||||
if let resolutionRange = line.range(of: "RESOLUTION="),
|
if let resolutionRange = line.range(of: "RESOLUTION="),
|
||||||
let resolutionEndRange = line[resolutionRange.upperBound...].range(of: ",")
|
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])
|
let resolutionPart = String(line[resolutionRange.upperBound..<resolutionEndRange.lowerBound])
|
||||||
if let heightStr = resolutionPart.components(separatedBy: "x").last,
|
if let heightStr = resolutionPart.components(separatedBy: "x").last,
|
||||||
|
|
@ -1102,7 +1233,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
||||||
if let baseURL = self.baseM3U8URL {
|
if let baseURL = self.baseM3U8URL {
|
||||||
let baseURLString = baseURL.deletingLastPathComponent().absoluteString
|
let baseURLString = baseURL.deletingLastPathComponent().absoluteString
|
||||||
qualityURL = URL(string: nextLine, relativeTo: baseURL)?.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.isHidden = false
|
||||||
self.qualityButton.menu = self.qualitySelectionMenu()
|
self.qualityButton.menu = self.qualitySelectionMenu()
|
||||||
|
self.updateMarqueeConstraints()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
isHLSStream = false
|
isHLSStream = false
|
||||||
|
|
@ -1487,6 +1619,20 @@ class CustomMediaPlayerViewController: UIViewController {
|
||||||
player?.rate = lastPlayedSpeed > 0 ? lastPlayedSpeed : 1.0
|
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 */; };
|
13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */; };
|
||||||
13EA2BD62D32D97400C1EBD7 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD32D32D97400C1EBD7 /* Double+Extension.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 */; };
|
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 */; };
|
1E3F5EC82D9F16B7003F310F /* VerticalBrightnessSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E3F5EC72D9F16B7003F310F /* VerticalBrightnessSlider.swift */; };
|
||||||
1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */; };
|
1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */; };
|
||||||
1EAC7A322D888BC50083984D /* MusicProgressSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */; };
|
1EAC7A322D888BC50083984D /* MusicProgressSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */; };
|
||||||
|
|
@ -135,6 +136,7 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
1E048A722DA3262900D9DD3F /* MarqueeLabel in Frameworks */,
|
||||||
132E35232D959E410007800E /* Kingfisher in Frameworks */,
|
132E35232D959E410007800E /* Kingfisher in Frameworks */,
|
||||||
132E35202D959E1D0007800E /* FFmpeg-iOS-Lame in Frameworks */,
|
132E35202D959E1D0007800E /* FFmpeg-iOS-Lame in Frameworks */,
|
||||||
132E351D2D959DDB0007800E /* Drops in Frameworks */,
|
132E351D2D959DDB0007800E /* Drops in Frameworks */,
|
||||||
|
|
@ -469,6 +471,7 @@
|
||||||
132E351C2D959DDB0007800E /* Drops */,
|
132E351C2D959DDB0007800E /* Drops */,
|
||||||
132E351F2D959E1D0007800E /* FFmpeg-iOS-Lame */,
|
132E351F2D959E1D0007800E /* FFmpeg-iOS-Lame */,
|
||||||
132E35222D959E410007800E /* Kingfisher */,
|
132E35222D959E410007800E /* Kingfisher */,
|
||||||
|
1E048A712DA3262900D9DD3F /* MarqueeLabel */,
|
||||||
);
|
);
|
||||||
productName = Sora;
|
productName = Sora;
|
||||||
productReference = 133D7C6A2D2BE2500075467E /* Sulfur.app */;
|
productReference = 133D7C6A2D2BE2500075467E /* Sulfur.app */;
|
||||||
|
|
@ -501,6 +504,7 @@
|
||||||
132E351B2D959DDB0007800E /* XCRemoteSwiftPackageReference "Drops" */,
|
132E351B2D959DDB0007800E /* XCRemoteSwiftPackageReference "Drops" */,
|
||||||
132E351E2D959E1D0007800E /* XCRemoteSwiftPackageReference "FFmpeg-iOS-Lame" */,
|
132E351E2D959E1D0007800E /* XCRemoteSwiftPackageReference "FFmpeg-iOS-Lame" */,
|
||||||
132E35212D959E410007800E /* XCRemoteSwiftPackageReference "Kingfisher" */,
|
132E35212D959E410007800E /* XCRemoteSwiftPackageReference "Kingfisher" */,
|
||||||
|
1E048A702DA3262900D9DD3F /* XCRemoteSwiftPackageReference "MarqueeLabel" */,
|
||||||
);
|
);
|
||||||
productRefGroup = 133D7C6B2D2BE2500075467E /* Products */;
|
productRefGroup = 133D7C6B2D2BE2500075467E /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
|
|
@ -838,6 +842,14 @@
|
||||||
version = 7.9.1;
|
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 */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
|
@ -856,6 +868,11 @@
|
||||||
package = 132E35212D959E410007800E /* XCRemoteSwiftPackageReference "Kingfisher" */;
|
package = 132E35212D959E410007800E /* XCRemoteSwiftPackageReference "Kingfisher" */;
|
||||||
productName = Kingfisher;
|
productName = Kingfisher;
|
||||||
};
|
};
|
||||||
|
1E048A712DA3262900D9DD3F /* MarqueeLabel */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 1E048A702DA3262900D9DD3F /* XCRemoteSwiftPackageReference "MarqueeLabel" */;
|
||||||
|
productName = MarqueeLabel;
|
||||||
|
};
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = 133D7C622D2BE2500075467E /* Project object */;
|
rootObject = 133D7C622D2BE2500075467E /* Project object */;
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,51 @@
|
||||||
{
|
{
|
||||||
"object": {
|
"originHash" : "e772caa8d6a8793d24bf04e3d77695cd5ac695f3605d2b657e40115caedf8863",
|
||||||
"pins": [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"package": "Drops",
|
"identity" : "drops",
|
||||||
"repositoryURL": "https://github.com/omaralbeik/Drops.git",
|
"kind" : "remoteSourceControl",
|
||||||
"state": {
|
"location" : "https://github.com/omaralbeik/Drops.git",
|
||||||
"branch": "main",
|
"state" : {
|
||||||
"revision": "5824681795286c36bdc4a493081a63e64e2a064e",
|
"branch" : "main",
|
||||||
"version": null
|
"revision" : "5824681795286c36bdc4a493081a63e64e2a064e"
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
},
|
||||||
},
|
{
|
||||||
"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