few player bug fixes (#104)

This commit is contained in:
Seiike 2025-04-24 15:11:50 +02:00 committed by GitHub
parent cefd996115
commit 219088f3d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 288 additions and 113 deletions

View file

@ -18,8 +18,8 @@ struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
let emptyColor: Color
let height: CGFloat
let onEditingChanged: (Bool) -> Void
let introSegments: [ClosedRange<T>] // Changed
let outroSegments: [ClosedRange<T>] // Changed
let introSegments: [ClosedRange<T>]
let outroSegments: [ClosedRange<T>]
let introColor: Color
let outroColor: Color
@ -57,10 +57,10 @@ struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
}
}
// Rest of the existing code...
Capsule()
.fill(emptyColor)
}
.clipShape(Capsule())
Capsule()
.fill(isActive ? activeFillColor : fillColor)

View file

@ -116,6 +116,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
}
}
private var wasPlayingBeforeSeek = false
private var malID: Int?
private var skipIntervals: (op: CMTimeRange?, ed: CMTimeRange?) = (nil, nil)
@ -123,6 +125,12 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
private var skipOutroButton: UIButton!
private let skipButtonBaseAlpha: CGFloat = 0.9
@Published var segments: [ClosedRange<Double>] = []
private var skipIntroLeading: NSLayoutConstraint!
private var skipOutroLeading: NSLayoutConstraint!
private var originalIntroLeading: CGFloat = 0
private var originalOutroLeading: CGFloat = 0
private var skipIntroDismissedInSession = false
private var skipOutroDismissedInSession = false
private var playerItemKVOContext = 0
private var loadedTimeRangesObservation: NSKeyValueObservation?
@ -214,7 +222,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
loadSubtitleSettings()
setupPlayerViewController()
setupControls()
setupSkipAndDismissGestures()
addInvisibleControlOverlays()
setupWatchNextButton()
setupSubtitleLabel()
@ -227,12 +234,16 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
setupMarqueeLabel()
setupSkip85Button()
setupSkipButtons()
setupSkipAndDismissGestures()
addTimeObserver()
startUpdateTimer()
setupAudioSession()
updateSkipButtonsVisibility()
view.bringSubviewToFront(subtitleLabel)
view.bringSubviewToFront(topSubtitleLabel)
AniListMutation().fetchMalID(animeId: aniListID) { [weak self] result in
switch result {
case .success(let mal):
@ -240,7 +251,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
self?.fetchSkipTimes(type: "op")
self?.fetchSkipTimes(type: "ed")
case .failure(let error):
Logger.shared.log("⚠️ Unable to fetch MAL ID: \(error)",type:"Error")
Logger.shared.log("Unable to fetch MAL ID: \(error)",type:"Error")
}
}
@ -522,29 +533,37 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
height: 33,
onEditingChanged: { editing in
if editing {
self.isSliderEditing = true
self.isSliderEditing = true
self.wasPlayingBeforeSeek = (self.player.timeControlStatus == .playing)
self.originalRate = self.player.rate
self.player.pause()
} 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.isSliderEditing = false
if wasPlaying {
self.player.play()
}
let target = CMTime(seconds: self.sliderViewModel.sliderValue,
preferredTimescale: 600)
self.player.seek(
to: target,
toleranceBefore: .zero,
toleranceAfter: .zero
) { [weak self] _ in
guard let self = self else { return }
let final = self.player.currentTime().seconds
self.sliderViewModel.sliderValue = final
self.currentTimeVal = final
self.isSliderEditing = false
if self.wasPlayingBeforeSeek {
self.player.playImmediately(atRate: self.originalRate)
}
}
}
},
introSegments: sliderViewModel.introSegments, // Added
outroSegments: sliderViewModel.outroSegments, // Added
introColor: segmentsColor, // Add your colors here
outroColor: segmentsColor // Or use settings.accentColor
},
introSegments: sliderViewModel.introSegments,
outroSegments: sliderViewModel.outroSegments,
introColor: segmentsColor,
outroColor: segmentsColor
)
sliderHostingController = UIHostingController(rootView: sliderView)
@ -614,7 +633,19 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
}
}
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
let panGesture = UIPanGestureRecognizer(target: self,
action: #selector(handlePanGesture(_:)))
// find the skip-swipe recognizers AFTER youve created them
if let introSwipe = skipIntroButton.gestureRecognizers?.first(
where: { $0 is UISwipeGestureRecognizer && ($0 as! UISwipeGestureRecognizer).direction == .left }
),
let outroSwipe = skipOutroButton.gestureRecognizers?.first(
where: { $0 is UISwipeGestureRecognizer && ($0 as! UISwipeGestureRecognizer).direction == .left }
) {
panGesture.require(toFail: introSwipe)
panGesture.require(toFail: outroSwipe)
}
view.addGestureRecognizer(panGesture)
}
@ -798,33 +829,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
controlsContainerView.addSubview(marqueeLabel)
marqueeLabel.translatesAutoresizingMaskIntoConstraints = false
// 1. Portrait mode with button visible
portraitButtonVisibleConstraints = [
marqueeLabel.leadingAnchor.constraint(equalTo: dismissButton.trailingAnchor, constant: 8),
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)
]
//Fully manages contsraints
updateMarqueeConstraints()
}
@ -855,7 +860,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
private func updateSkipButtonsVisibility() {
let t = currentTimeVal
let controlsShowing = isControlsVisible // true main UI is onscreen
let controlsShowing = isControlsVisible
func handle(_ button: UIButton, range: CMTimeRange?) {
guard let r = range else { button.isHidden = true; return }
@ -885,6 +890,17 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
handle(skipIntroButton, range: skipIntervals.op)
handle(skipOutroButton, range: skipIntervals.ed)
if skipIntroDismissedInSession {
skipIntroButton.isHidden = true
} else {
handle(skipIntroButton, range: skipIntervals.op)
}
if skipOutroDismissedInSession {
skipOutroButton.isHidden = true
} else {
handle(skipOutroButton, range: skipIntervals.ed)
}
}
private func updateSegments() {
@ -918,17 +934,38 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
emptyColor: .white.opacity(0.3),
height: 33,
onEditingChanged: { editing in
if !editing {
let targetTime = CMTime(
seconds: self.sliderViewModel.sliderValue,
preferredTimescale: 600
)
self.player.seek(to: targetTime)
if editing {
self.isSliderEditing = true
self.wasPlayingBeforeSeek = (self.player.timeControlStatus == .playing)
self.originalRate = self.player.rate
self.player.pause()
} else {
let target = CMTime(seconds: self.sliderViewModel.sliderValue,
preferredTimescale: 600)
self.player.seek(
to: target,
toleranceBefore: .zero,
toleranceAfter: .zero
) { [weak self] _ in
guard let self = self else { return }
let final = self.player.currentTime().seconds
self.sliderViewModel.sliderValue = final
self.currentTimeVal = final
self.isSliderEditing = false
if self.wasPlayingBeforeSeek {
self.player.playImmediately(atRate: self.originalRate)
}
}
}
},
},
introSegments: self.sliderViewModel.introSegments,
outroSegments: self.sliderViewModel.outroSegments,
introColor: segmentsColor, // Match your color choices
introColor: segmentsColor,
outroColor: segmentsColor
)
}
@ -961,16 +998,14 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
}.resume()
}
private func setupSkipButtons() {
func setupSkipButtons() {
// MARK: Skip Intro Button
skipIntroButton = UIButton(type: .system)
let introConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .bold)
let introImage = UIImage(systemName: "forward.frame", withConfiguration: introConfig)
skipIntroButton = UIButton(type: .system)
skipIntroButton.setImage(introImage, for: .normal)
skipIntroButton.setTitle(" Skip Intro", for: .normal)
skipIntroButton.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .bold)
// match skip85Button styling:
skipIntroButton.titleLabel?.font = .systemFont(ofSize: 14, weight: .bold)
skipIntroButton.backgroundColor = UIColor(red: 51/255, green: 51/255, blue: 51/255, alpha: 0.8)
skipIntroButton.tintColor = .white
skipIntroButton.setTitleColor(.white, for: .normal)
@ -982,28 +1017,30 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
skipIntroButton.layer.shadowOpacity = 0.6
skipIntroButton.layer.shadowRadius = 4
skipIntroButton.layer.masksToBounds = false
skipIntroButton.addTarget(self, action: #selector(skipIntro), for: .touchUpInside)
skipIntroButton.isUserInteractionEnabled = true
view.addSubview(skipIntroButton)
skipIntroButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
skipIntroButton.leadingAnchor.constraint(
equalTo: sliderHostingController!.view.leadingAnchor),
skipIntroButton.bottomAnchor.constraint(
equalTo: sliderHostingController!.view.topAnchor, constant: -5)
])
skipIntroButton.translatesAutoresizingMaskIntoConstraints = false
skipIntroLeading = skipIntroButton.leadingAnchor.constraint(
equalTo: sliderHostingController!.view.leadingAnchor
)
let skipIntroBottom = skipIntroButton.bottomAnchor.constraint(
equalTo: sliderHostingController!.view.topAnchor,
constant: -5
)
NSLayoutConstraint.activate([ skipIntroLeading, skipIntroBottom ])
skipIntroButton.addTarget(self, action: #selector(skipIntro), for: .touchUpInside)
skipIntroButton.addGestureRecognizer(UIPanGestureRecognizer(target: self,
action: #selector(handleIntroPan(_:))))
// MARK: Skip Outro Button
skipOutroButton = UIButton(type: .system)
let outroConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .bold)
let outroImage = UIImage(systemName: "forward.frame", withConfiguration: outroConfig)
skipOutroButton = UIButton(type: .system)
skipOutroButton.setImage(outroImage, for: .normal)
skipOutroButton.setTitle(" Skip Outro", for: .normal)
skipOutroButton.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .bold)
skipOutroButton.titleLabel?.font = .systemFont(ofSize: 14, weight: .bold)
// same styling as above
skipOutroButton.backgroundColor = skipIntroButton.backgroundColor
skipOutroButton.tintColor = skipIntroButton.tintColor
skipOutroButton.setTitleColor(.white, for: .normal)
@ -1015,18 +1052,26 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
skipOutroButton.layer.shadowOpacity = skipIntroButton.layer.shadowOpacity
skipOutroButton.layer.shadowRadius = skipIntroButton.layer.shadowRadius
skipOutroButton.layer.masksToBounds = false
skipOutroButton.addTarget(self, action: #selector(skipOutro), for: .touchUpInside)
skipOutroButton.isUserInteractionEnabled = true
view.addSubview(skipOutroButton)
skipOutroButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
skipOutroButton.leadingAnchor.constraint(
equalTo: sliderHostingController!.view.leadingAnchor),
skipOutroButton.bottomAnchor.constraint(
equalTo: sliderHostingController!.view.topAnchor, constant: -5)
])
skipOutroLeading = skipOutroButton.leadingAnchor.constraint(
equalTo: sliderHostingController!.view.leadingAnchor
)
let skipOutroBottom = skipOutroButton.bottomAnchor.constraint(
equalTo: sliderHostingController!.view.topAnchor,
constant: -5
)
NSLayoutConstraint.activate([ skipOutroLeading, skipOutroBottom ])
skipOutroButton.addTarget(self, action: #selector(skipOutro), for: .touchUpInside)
skipOutroButton.addGestureRecognizer(
UIPanGestureRecognizer(target: self, action: #selector(handleOutroPan(_:)))
)
// ensure Outro sits above Intro so its pan-gesture catches first
view.bringSubviewToFront(skipOutroButton)
}
@ -1046,20 +1091,14 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
dimButton.layer.masksToBounds = false
NSLayoutConstraint.activate([
dimButton.centerYAnchor.constraint(equalTo: dismissButton.centerYAnchor),
dimButton.topAnchor.constraint(equalTo: volumeSliderHostingView!.bottomAnchor, constant: 15),
dimButton.trailingAnchor.constraint(equalTo: volumeSliderHostingView!.trailingAnchor),
dimButton.widthAnchor.constraint(equalToConstant: 24),
dimButton.heightAnchor.constraint(equalToConstant: 24),
dimButton.heightAnchor.constraint(equalToConstant: 24)
])
dimButtonToSlider = dimButton.trailingAnchor.constraint(
equalTo: volumeSliderHostingView!.leadingAnchor,
constant: -8
)
dimButtonToRight = dimButton.trailingAnchor.constraint(
equalTo: controlsContainerView.trailingAnchor,
constant: -16
)
dimButtonToSlider = dimButton.trailingAnchor.constraint(equalTo: volumeSliderHostingView!.trailingAnchor)
dimButtonToRight = dimButton.trailingAnchor.constraint(equalTo: controlsContainerView.trailingAnchor, constant: -16)
dimButtonToSlider.isActive = true
}
@ -1069,7 +1108,9 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
let leftSpacing: CGFloat = 2
let rightSpacing: CGFloat = 6
let trailingAnchor: NSLayoutXAxisAnchor = dimButton.leadingAnchor
let trailingAnchor: NSLayoutXAxisAnchor = (volumeSliderHostingView?.isHidden == false)
? volumeSliderHostingView!.leadingAnchor
: view.safeAreaLayoutGuide.trailingAnchor
currentMarqueeConstraints = [
marqueeLabel.leadingAnchor.constraint(
@ -1126,6 +1167,12 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
speedButton.showsMenuAsPrimaryAction = true
speedButton.menu = speedChangerMenu()
speedButton.layer.shadowColor = UIColor.black.cgColor
speedButton.layer.shadowOffset = CGSize(width: 0, height: 2)
speedButton.layer.shadowOpacity = 0.6
speedButton.layer.shadowRadius = 4
speedButton.layer.masksToBounds = false
controlsContainerView.addSubview(speedButton)
speedButton.translatesAutoresizingMaskIntoConstraints = false
@ -1294,6 +1341,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
self.sliderViewModel.sliderValue = max(0, min(self.currentTimeVal, self.duration))
}
updateSkipButtonsVisibility()
UserDefaults.standard.set(self.currentTimeVal, forKey: "lastPlayedTime_\(self.fullUrl)")
UserDefaults.standard.set(self.duration, forKey: "totalTime_\(self.fullUrl)")
@ -1368,14 +1417,40 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
emptyColor: .white.opacity(0.3),
height: 33,
onEditingChanged: { editing in
if !editing {
let targetTime = CMTime(
seconds: self.sliderViewModel.sliderValue,
preferredTimescale: 600
)
self.player.seek(to: targetTime)
if editing {
// 1) start of scrub
self.isSliderEditing = true
// remember if we were playing, and at what rate
self.wasPlayingBeforeSeek = (self.player.timeControlStatus == .playing)
self.originalRate = self.player.rate
// pause the player immediately
self.player.pause()
} else {
// 2) end of scrub seek exactly to the new spot
let target = CMTime(seconds: self.sliderViewModel.sliderValue,
preferredTimescale: 600)
self.player.seek(
to: target,
toleranceBefore: .zero,
toleranceAfter: .zero
) { [weak self] _ in
guard let self = self else { return }
// update our UImodel
let final = self.player.currentTime().seconds
self.sliderViewModel.sliderValue = final
self.currentTimeVal = final
self.isSliderEditing = false
// 3) resume at exactly the same rate
if self.wasPlayingBeforeSeek {
self.player.playImmediately(atRate: self.originalRate)
}
}
}
},
},
introSegments: self.sliderViewModel.introSegments,
outroSegments: self.sliderViewModel.outroSegments,
introColor: segmentsColor, // Match your color choices
@ -1385,6 +1460,83 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
}
}
@objc private func handleIntroPan(_ g: UIPanGestureRecognizer) {
let translation = g.translation(in: view).x
switch g.state {
case .began:
originalIntroLeading = skipIntroLeading.constant
case .changed:
skipIntroLeading.constant = originalIntroLeading + min(0, translation)
view.layoutIfNeeded()
case .ended, .cancelled:
let threshold = -skipIntroButton.bounds.width * 0.4
if skipIntroLeading.constant < originalIntroLeading + threshold {
// animate all the way off to the left
let offscreen = -skipIntroButton.bounds.width - 100
UIView.animate(
withDuration: 0.25,
animations: {
self.skipIntroLeading.constant = offscreen
self.view.layoutIfNeeded()
}, completion: { _ in
self.skipIntroDismissedInSession = true
self.skipIntroButton.isHidden = true
})
} else {
// bounce back
UIView.animate(
withDuration: 0.3,
delay: 0,
usingSpringWithDamping: 0.6,
initialSpringVelocity: 1,
options: [],
animations: {
self.skipIntroLeading.constant = self.originalIntroLeading
self.view.layoutIfNeeded()
})
}
default: break
}
}
@objc private func handleOutroPan(_ g: UIPanGestureRecognizer) {
let translation = g.translation(in: view).x
switch g.state {
case .began:
originalOutroLeading = skipOutroLeading.constant
case .changed:
skipOutroLeading.constant = originalOutroLeading + min(0, translation)
view.layoutIfNeeded()
case .ended, .cancelled:
let threshold = -skipOutroButton.bounds.width * 0.4
if skipOutroLeading.constant < originalOutroLeading + threshold {
// animate off to the left
let offscreen = -skipOutroButton.bounds.width - 100
UIView.animate(
withDuration: 0.25,
animations: {
self.skipOutroLeading.constant = offscreen
self.view.layoutIfNeeded()
}, completion: { _ in
self.skipOutroDismissedInSession = true
self.skipOutroButton.isHidden = true
})
} else {
UIView.animate(
withDuration: 0.3,
delay: 0,
usingSpringWithDamping: 0.6,
initialSpringVelocity: 1,
options: [],
animations: {
self.skipOutroLeading.constant = self.originalOutroLeading
self.view.layoutIfNeeded()
})
}
default: break
}
}
@objc private func skipIntro() {
if let range = skipIntervals.op {
player.seek(to: range.end)
@ -1524,6 +1676,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
self.controlsContainerView.alpha = 1.0
self.skip85Button.alpha = 0.8
})
self.updateSkipButtonsVisibility()
}
}
} else {

View file

@ -17,6 +17,9 @@ struct LibraryView: View {
@Environment(\.verticalSizeClass) var verticalSizeClass
@State private var selectedBookmark: LibraryItem? = nil
@State private var isDetailActive: Bool = false
@State private var continueWatchingItems: [ContinueWatchingItem] = []
@State private var isLandscape: Bool = UIDevice.current.orientation.isLandscape
@ -98,7 +101,10 @@ struct LibraryView: View {
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 12), count: columnsCount), spacing: 12) {
ForEach(libraryManager.bookmarks) { item in
if let module = moduleManager.modules.first(where: { $0.id.uuidString == item.moduleId }) {
NavigationLink(destination: MediaInfoView(title: item.title, imageUrl: item.imageUrl, href: item.href, module: module)) {
Button(action: {
selectedBookmark = item
isDetailActive = true
}) {
VStack(alignment: .leading) {
ZStack {
KFImage(URL(string: item.imageUrl))
@ -141,6 +147,22 @@ struct LibraryView: View {
}
}
.padding(.horizontal, 20)
NavigationLink(
destination: Group {
if let bookmark = selectedBookmark,
let module = moduleManager.modules.first(where: { $0.id.uuidString == bookmark.moduleId }) {
MediaInfoView(title: bookmark.title,
imageUrl: bookmark.imageUrl,
href: bookmark.href,
module: module)
} else {
Text("No Data Available")
}
},
isActive: $isDetailActive
) {
EmptyView()
}
.onAppear {
updateOrientation()
}