diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift b/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift index 55ae983..6a4b6d4 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift @@ -9,174 +9,134 @@ import SwiftUI - struct MusicProgressSlider: View { - @Binding var value: T - let inRange: ClosedRange - - 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: View { + @Binding var value: T + @Binding var bufferValue: T // NEW + let inRange: ClosedRange + + 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 } } diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index 5530058..95f94f4 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -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.. 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() + } + } + } + } } diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index a0d0a49..7176dc8 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -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 */; diff --git a/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9784b35..5bbeff8 100644 --- a/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -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 }