mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 13:33:42 +00:00
AVPlayer system controls on iOS
This commit is contained in:
parent
a4fdd50388
commit
5383cf0e90
21
Backports/ToolbarBackground+Backport.swift
Normal file
21
Backports/ToolbarBackground+Backport.swift
Normal file
@ -0,0 +1,21 @@
|
||||
import SwiftUI
|
||||
|
||||
extension Backport where Content: View {
|
||||
@ViewBuilder func toolbarBackground(_ color: Color) -> some View {
|
||||
if #available(iOS 16, *) {
|
||||
content
|
||||
.toolbarBackground(color, for: .navigationBar)
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func toolbarBackgroundVisibility(_ visible: Bool) -> some View {
|
||||
if #available(iOS 16, *) {
|
||||
content
|
||||
.toolbarBackground(visible ? .visible : .hidden, for: .navigationBar)
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
12
Backports/ToolbarColorScheme+Backport.swift
Normal file
12
Backports/ToolbarColorScheme+Backport.swift
Normal file
@ -0,0 +1,12 @@
|
||||
import SwiftUI
|
||||
|
||||
extension Backport where Content: View {
|
||||
@ViewBuilder func toolbarColorScheme(_ colorScheme: ColorScheme) -> some View {
|
||||
if #available(iOS 16, *) {
|
||||
content
|
||||
.toolbarColorScheme(colorScheme, for: .navigationBar)
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
11
Extensions/AVPlayerViewController+FullScreen.swift
Normal file
11
Extensions/AVPlayerViewController+FullScreen.swift
Normal file
@ -0,0 +1,11 @@
|
||||
import AVKit
|
||||
|
||||
extension AVPlayerViewController {
|
||||
func enterFullScreen(animated: Bool) {
|
||||
perform(NSSelectorFromString("enterFullScreenAnimated:completionHandler:"), with: animated, with: nil)
|
||||
}
|
||||
|
||||
func exitFullScreen(animated: Bool) {
|
||||
perform(NSSelectorFromString("exitFullScreenAnimated:completionHandler:"), with: animated, with: nil)
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import AVFoundation
|
||||
import AVKit
|
||||
import Defaults
|
||||
import Foundation
|
||||
import Logging
|
||||
@ -6,6 +6,7 @@ import MediaPlayer
|
||||
#if !os(macOS)
|
||||
import UIKit
|
||||
#endif
|
||||
import SwiftUI
|
||||
|
||||
final class AVPlayerBackend: PlayerBackend {
|
||||
static let assetKeysToLoad = ["tracks", "playable", "duration"]
|
||||
@ -84,6 +85,10 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
private(set) var playerLayer = AVPlayerLayer()
|
||||
#if os(tvOS)
|
||||
var controller: AppleAVPlayerViewController?
|
||||
#elseif os(iOS)
|
||||
var controller = AVPlayerViewController() { didSet {
|
||||
controller.player = avPlayer
|
||||
}}
|
||||
#endif
|
||||
var startPictureInPictureOnPlay = false
|
||||
var startPictureInPictureOnSwitch = false
|
||||
@ -108,6 +113,9 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
addPlayerTimeControlStatusObserver()
|
||||
|
||||
playerLayer.player = avPlayer
|
||||
#if os(iOS)
|
||||
controller.player = avPlayer
|
||||
#endif
|
||||
}
|
||||
|
||||
func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream? {
|
||||
@ -469,10 +477,6 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
|
||||
switch playerItem.status {
|
||||
case .readyToPlay:
|
||||
if self.model.playingInPictureInPicture {
|
||||
self.startPictureInPictureOnSwitch = false
|
||||
self.startPictureInPictureOnPlay = false
|
||||
}
|
||||
if self.model.activeBackend == .appleAVPlayer,
|
||||
self.isAutoplaying(playerItem)
|
||||
{
|
||||
@ -487,17 +491,21 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
self.model.play()
|
||||
}
|
||||
} else if self.startPictureInPictureOnPlay {
|
||||
self.startPictureInPictureOnPlay = false
|
||||
self.model.stream = self.stream
|
||||
self.model.streamSelection = self.stream
|
||||
|
||||
if self.model.activeBackend != .appleAVPlayer {
|
||||
self.startPictureInPictureOnSwitch = true
|
||||
let seconds = self.model.mpvBackend.currentTime?.seconds ?? 0
|
||||
self.seek(to: seconds, seekType: .backendSync) { _ in
|
||||
self.seek(to: seconds, seekType: .backendSync) { finished in
|
||||
guard finished else { return }
|
||||
DispatchQueue.main.async {
|
||||
self.model.pause()
|
||||
self.model.changeActiveBackend(from: .mpv, to: .appleAVPlayer, changingStream: false)
|
||||
|
||||
Delay.by(3) {
|
||||
self.startPictureInPictureOnPlay = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -688,7 +696,6 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
|
||||
func didChangeTo() {
|
||||
if startPictureInPictureOnSwitch {
|
||||
startPictureInPictureOnSwitch = false
|
||||
tryStartingPictureInPicture()
|
||||
} else if model.musicMode {
|
||||
startMusicMode()
|
||||
@ -697,6 +704,10 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
}
|
||||
}
|
||||
|
||||
var isStartingPiP: Bool {
|
||||
startPictureInPictureOnPlay || startPictureInPictureOnSwitch
|
||||
}
|
||||
|
||||
func tryStartingPictureInPicture() {
|
||||
guard let controller = model.pipController else { return }
|
||||
|
||||
@ -712,6 +723,32 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Delay.by(5) {
|
||||
self.startPictureInPictureOnSwitch = false
|
||||
}
|
||||
}
|
||||
|
||||
func setPlayerInLayer(_ playerIsPresented: Bool) {
|
||||
if playerIsPresented {
|
||||
bindPlayerToLayer()
|
||||
} else {
|
||||
removePlayerFromLayer()
|
||||
}
|
||||
}
|
||||
|
||||
func removePlayerFromLayer() {
|
||||
playerLayer.player = nil
|
||||
#if os(iOS)
|
||||
controller.player = nil
|
||||
#endif
|
||||
}
|
||||
|
||||
func bindPlayerToLayer() {
|
||||
playerLayer.player = avPlayer
|
||||
#if os(iOS)
|
||||
controller.player = avPlayer
|
||||
#endif
|
||||
}
|
||||
|
||||
func getTimeUpdates() {}
|
||||
|
@ -4,7 +4,7 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
final class PiPDelegate: NSObject, AVPictureInPictureControllerDelegate {
|
||||
var player: PlayerModel!
|
||||
var player: PlayerModel { .shared }
|
||||
|
||||
func pictureInPictureController(
|
||||
_: AVPictureInPictureController,
|
||||
@ -16,19 +16,17 @@ final class PiPDelegate: NSObject, AVPictureInPictureControllerDelegate {
|
||||
func pictureInPictureControllerWillStartPictureInPicture(_: AVPictureInPictureController) {}
|
||||
|
||||
func pictureInPictureControllerDidStartPictureInPicture(_: AVPictureInPictureController) {
|
||||
guard let player else { return }
|
||||
player.play()
|
||||
|
||||
player.playingInPictureInPicture = true
|
||||
player.avPlayerBackend.startPictureInPictureOnPlay = false
|
||||
player.avPlayerBackend.startPictureInPictureOnSwitch = false
|
||||
player.controls.objectWillChange.send()
|
||||
|
||||
if Defaults[.closePlayerOnOpeningPiP] { Delay.by(0.1) { player.hide() } }
|
||||
if Defaults[.closePlayerOnOpeningPiP] { Delay.by(0.1) { self.player.hide() } }
|
||||
}
|
||||
|
||||
func pictureInPictureControllerDidStopPictureInPicture(_: AVPictureInPictureController) {
|
||||
guard let player else { return }
|
||||
|
||||
player.playingInPictureInPicture = false
|
||||
player.controls.objectWillChange.send()
|
||||
}
|
||||
@ -39,6 +37,8 @@ final class PiPDelegate: NSObject, AVPictureInPictureControllerDelegate {
|
||||
_: AVPictureInPictureController,
|
||||
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
|
||||
) {
|
||||
let wasPlaying = player.isPlaying
|
||||
|
||||
var delay = 0.0
|
||||
#if os(iOS)
|
||||
if !player.presentingPlayer {
|
||||
@ -50,7 +50,7 @@ final class PiPDelegate: NSObject, AVPictureInPictureControllerDelegate {
|
||||
#endif
|
||||
|
||||
if !player.currentItem.isNil, !player.musicMode {
|
||||
player?.show()
|
||||
player.show()
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||
@ -58,6 +58,11 @@ final class PiPDelegate: NSObject, AVPictureInPictureControllerDelegate {
|
||||
self?.player.playingInPictureInPicture = false
|
||||
}
|
||||
|
||||
if wasPlaying {
|
||||
Delay.by(1) {
|
||||
self?.player.play()
|
||||
}
|
||||
}
|
||||
completionHandler(true)
|
||||
}
|
||||
}
|
||||
|
@ -49,7 +49,6 @@ final class PlayerModel: ObservableObject {
|
||||
|
||||
let logger = Logger(label: "stream.yattee.app")
|
||||
|
||||
var avPlayerView = AppleAVPlayerView()
|
||||
var playerItem: AVPlayerItem?
|
||||
|
||||
var mpvPlayerView = MPVPlayerView()
|
||||
@ -153,6 +152,9 @@ final class PlayerModel: ObservableObject {
|
||||
@Published var playingInPictureInPicture = false
|
||||
var pipController: AVPictureInPictureController?
|
||||
var pipDelegate = PiPDelegate()
|
||||
#if !os(macOS)
|
||||
var appleAVPlayerViewControllerDelegate = AppleAVPlayerViewControllerDelegate()
|
||||
#endif
|
||||
|
||||
var playerError: Error? { didSet {
|
||||
if let error = playerError {
|
||||
@ -164,6 +166,7 @@ final class PlayerModel: ObservableObject {
|
||||
@Default(.saveLastPlayed) var saveLastPlayed
|
||||
@Default(.lastPlayed) var lastPlayed
|
||||
@Default(.qualityProfiles) var qualityProfiles
|
||||
@Default(.avPlayerUsesSystemControls) var avPlayerUsesSystemControls
|
||||
@Default(.forceAVPlayerForLiveStreams) var forceAVPlayerForLiveStreams
|
||||
@Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer
|
||||
@Default(.closePiPOnNavigation) var closePiPOnNavigation
|
||||
@ -187,16 +190,17 @@ final class PlayerModel: ObservableObject {
|
||||
mpvBackend.client = mpvController.client
|
||||
#endif
|
||||
|
||||
Defaults[.activeBackend] = .mpv
|
||||
playbackMode = Defaults[.playbackMode]
|
||||
|
||||
guard pipController.isNil else { return }
|
||||
pipController = .init(playerLayer: avPlayerBackend.playerLayer)
|
||||
let pipDelegate = PiPDelegate()
|
||||
pipDelegate.player = self
|
||||
|
||||
self.pipDelegate = pipDelegate
|
||||
pipController = .init(playerLayer: avPlayerBackend.playerLayer)
|
||||
pipController?.delegate = pipDelegate
|
||||
#if os(iOS)
|
||||
if #available(iOS 14.2, *) {
|
||||
pipController?.canStartPictureInPictureAutomaticallyFromInline = true
|
||||
}
|
||||
#endif
|
||||
currentRate = playerRate
|
||||
}
|
||||
|
||||
@ -475,6 +479,12 @@ final class PlayerModel: ObservableObject {
|
||||
private func handlePresentationChange() {
|
||||
backend.setNeedsDrawing(presentingPlayer)
|
||||
|
||||
#if os(iOS)
|
||||
if presentingPlayer, activeBackend == .appleAVPlayer, avPlayerUsesSystemControls, Constants.isIPhone {
|
||||
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
||||
}
|
||||
#endif
|
||||
|
||||
controls.hide()
|
||||
|
||||
#if !os(macOS)
|
||||
@ -542,10 +552,15 @@ final class PlayerModel: ObservableObject {
|
||||
self.stream = stream
|
||||
streamSelection = stream
|
||||
|
||||
self.upgradeToStream(stream, force: true)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !backend.canPlay(stream) || (to == .mpv && !stream.hlsURL.isNil) {
|
||||
if !backend.canPlay(stream) ||
|
||||
(to == .mpv && stream.isHLS) ||
|
||||
(to == .appleAVPlayer && !stream.isHLS)
|
||||
{
|
||||
guard let preferredStream = streamByQualityProfile else {
|
||||
return
|
||||
}
|
||||
@ -631,8 +646,8 @@ final class PlayerModel: ObservableObject {
|
||||
if avPlayerBackend.video == video {
|
||||
if activeBackend != .appleAVPlayer {
|
||||
avPlayerBackend.startPictureInPictureOnSwitch = true
|
||||
changeActiveBackend(from: activeBackend, to: .appleAVPlayer)
|
||||
}
|
||||
changeActiveBackend(from: activeBackend, to: .appleAVPlayer)
|
||||
} else {
|
||||
avPlayerBackend.startPictureInPictureOnPlay = true
|
||||
playStream(stream, of: video, preservingTime: true, upgrading: true, withBackend: avPlayerBackend)
|
||||
@ -882,7 +897,7 @@ final class PlayerModel: ObservableObject {
|
||||
#else
|
||||
func handleEnterForeground() {
|
||||
setNeedsDrawing(presentingPlayer)
|
||||
avPlayerBackend.playerLayer.player = avPlayerBackend.avPlayer
|
||||
avPlayerBackend.bindPlayerToLayer()
|
||||
|
||||
guard closePiPAndOpenPlayerOnEnteringForeground, playingInPictureInPicture else {
|
||||
return
|
||||
@ -896,7 +911,7 @@ final class PlayerModel: ObservableObject {
|
||||
if Defaults[.pauseOnEnteringBackground], !playingInPictureInPicture, !musicMode {
|
||||
pause()
|
||||
} else if !playingInPictureInPicture {
|
||||
avPlayerBackend.playerLayer.player = nil
|
||||
avPlayerBackend.removePlayerFromLayer()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -919,6 +934,13 @@ final class PlayerModel: ObservableObject {
|
||||
#if os(tvOS)
|
||||
guard activeBackend == .mpv else { return }
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
if activeBackend == .appleAVPlayer, avPlayerUsesSystemControls {
|
||||
return
|
||||
}
|
||||
#endif
|
||||
|
||||
guard let video = currentItem?.video else {
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = .none
|
||||
return
|
||||
@ -986,13 +1008,23 @@ final class PlayerModel: ObservableObject {
|
||||
|
||||
#if os(iOS)
|
||||
if playingFullScreen {
|
||||
if activeBackend == .appleAVPlayer, avPlayerUsesSystemControls {
|
||||
avPlayerBackend.controller.enterFullScreen(animated: true)
|
||||
}
|
||||
guard rotateToLandscapeOnEnterFullScreen.isRotating else { return }
|
||||
if currentVideoIsLandscape {
|
||||
let delay = activeBackend == .appleAVPlayer && avPlayerUsesSystemControls ? 0.8 : 0
|
||||
// not sure why but first rotation call is ignore so doing rotate to same orientation first
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: OrientationTracker.shared.currentInterfaceOrientation)
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: rotateToLandscapeOnEnterFullScreen.interaceOrientation)
|
||||
Delay.by(delay) {
|
||||
let orientation = OrientationTracker.shared.currentDeviceOrientation.isLandscape ? OrientationTracker.shared.currentInterfaceOrientation : self.rotateToLandscapeOnEnterFullScreen.interaceOrientation
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: OrientationTracker.shared.currentInterfaceOrientation)
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: orientation)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if activeBackend == .appleAVPlayer, avPlayerUsesSystemControls {
|
||||
avPlayerBackend.controller.exitFullScreen(animated: true)
|
||||
}
|
||||
let rotationOrientation = rotateToPortraitOnExitFullScreen ? UIInterfaceOrientation.portrait : nil
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: rotationOrientation)
|
||||
}
|
||||
|
@ -176,6 +176,10 @@ class Stream: Equatable, Hashable, Identifiable {
|
||||
localURL != nil
|
||||
}
|
||||
|
||||
var isHLS: Bool {
|
||||
hlsURL != nil
|
||||
}
|
||||
|
||||
var quality: String {
|
||||
guard localURL.isNil else { return "Opened File" }
|
||||
return kind == .hls ? "adaptive (HLS)" : "\(resolution.name)\(kind == .stream ? " (\(kind.rawValue))" : "")"
|
||||
@ -229,8 +233,14 @@ class Stream: Equatable, Hashable, Identifiable {
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(videoAsset?.url)
|
||||
hasher.combine(audioAsset?.url)
|
||||
hasher.combine(hlsURL)
|
||||
if let url = videoAsset?.url {
|
||||
hasher.combine(url)
|
||||
}
|
||||
if let url = audioAsset?.url {
|
||||
hasher.combine(url)
|
||||
}
|
||||
if let url = hlsURL {
|
||||
hasher.combine(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -132,6 +132,7 @@ extension Defaults.Keys {
|
||||
|
||||
static let playerControlsLayout = Key<PlayerControlsLayout>("playerControlsLayout", default: playerControlsLayoutDefault)
|
||||
static let fullScreenPlayerControlsLayout = Key<PlayerControlsLayout>("fullScreenPlayerControlsLayout", default: fullScreenPlayerControlsLayoutDefault)
|
||||
static let avPlayerUsesSystemControls = Key<Bool>("avPlayerUsesSystemControls", default: true)
|
||||
static let horizontalPlayerGestureEnabled = Key<Bool>("horizontalPlayerGestureEnabled", default: true)
|
||||
static let seekGestureSpeed = Key<Double>("seekGestureSpeed", default: 0.5)
|
||||
static let seekGestureSensitivity = Key<Double>("seekGestureSensitivity", default: 30.0)
|
||||
|
@ -15,6 +15,8 @@ struct ContentView: View {
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
#endif
|
||||
|
||||
@Default(.avPlayerUsesSystemControls) private var avPlayerUsesSystemControls
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
#if os(iOS)
|
||||
@ -133,6 +135,7 @@ struct ContentView: View {
|
||||
)
|
||||
#endif
|
||||
.alert(isPresented: $navigation.presentingAlert) { navigation.alert }
|
||||
.statusBarHidden(player.playingFullScreen)
|
||||
}
|
||||
|
||||
var navigationStyle: NavigationStyle {
|
||||
@ -150,9 +153,11 @@ struct ContentView: View {
|
||||
playerView
|
||||
.transition(.asymmetric(insertion: .identity, removal: .opacity))
|
||||
.zIndex(3)
|
||||
} else if player.activeBackend == .appleAVPlayer {
|
||||
} else if player.activeBackend == .appleAVPlayer,
|
||||
avPlayerUsesSystemControls || player.avPlayerBackend.isStartingPiP
|
||||
{
|
||||
#if os(iOS)
|
||||
playerView.offset(y: UIScreen.main.bounds.height)
|
||||
AppleAVPlayerLayerView().offset(y: UIScreen.main.bounds.height)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
@ -2,15 +2,119 @@ import AVKit
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
#if !os(macOS)
|
||||
final class AppleAVPlayerViewControllerDelegate: NSObject, AVPlayerViewControllerDelegate {
|
||||
var player: PlayerModel { .shared }
|
||||
|
||||
func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_: AVPlayerViewController) -> Bool {
|
||||
false
|
||||
}
|
||||
|
||||
func playerViewController(_: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator) {
|
||||
Delay.by(0.5) { [weak self] in
|
||||
self?.player.playingFullScreen = true
|
||||
}
|
||||
}
|
||||
|
||||
func playerViewController(_: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
|
||||
let wasPlaying = player.isPlaying
|
||||
coordinator.animate(alongsideTransition: nil) { context in
|
||||
#if os(iOS)
|
||||
if wasPlaying {
|
||||
self.player.play()
|
||||
}
|
||||
#endif
|
||||
if !context.isCancelled {
|
||||
#if os(iOS)
|
||||
self.player.lockedOrientation = nil
|
||||
|
||||
if Defaults[.rotateToPortraitOnExitFullScreen] {
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
|
||||
}
|
||||
|
||||
if wasPlaying {
|
||||
self.player.play()
|
||||
}
|
||||
|
||||
self.player.playingFullScreen = false
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func playerViewControllerWillStartPictureInPicture(_: AVPlayerViewController) {}
|
||||
|
||||
func playerViewControllerWillStopPictureInPicture(_: AVPlayerViewController) {}
|
||||
|
||||
func playerViewControllerDidStartPictureInPicture(_: AVPlayerViewController) {
|
||||
player.playingInPictureInPicture = true
|
||||
player.avPlayerBackend.startPictureInPictureOnPlay = false
|
||||
player.avPlayerBackend.startPictureInPictureOnSwitch = false
|
||||
player.controls.objectWillChange.send()
|
||||
|
||||
if Defaults[.closePlayerOnOpeningPiP] { Delay.by(0.1) { self.player.hide() } }
|
||||
}
|
||||
|
||||
func playerViewControllerDidStopPictureInPicture(_: AVPlayerViewController) {
|
||||
player.playingInPictureInPicture = false
|
||||
player.controls.objectWillChange.send()
|
||||
}
|
||||
|
||||
func playerViewController(_: AVPlayerViewController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
|
||||
player.presentingPlayer = true
|
||||
withAnimation(.linear(duration: 0.3)) {
|
||||
self.player.playingInPictureInPicture = false
|
||||
Delay.by(0.5) {
|
||||
completionHandler(true)
|
||||
Delay.by(0.2) {
|
||||
self.player.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func playerViewController(_: AVPlayerViewController, restoreUserInterfaceForFullScreenExitWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
|
||||
withAnimation(nil) {
|
||||
player.presentingPlayer = true
|
||||
}
|
||||
|
||||
completionHandler(true)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
struct AppleAVPlayerView: UIViewRepresentable {
|
||||
struct AppleAVPlayerView: UIViewControllerRepresentable {
|
||||
@State private var controller = AVPlayerViewController()
|
||||
|
||||
func makeUIViewController(context _: Context) -> AVPlayerViewController {
|
||||
setupController()
|
||||
return controller
|
||||
}
|
||||
|
||||
func updateUIViewController(_: AVPlayerViewController, context _: Context) {
|
||||
setupController()
|
||||
}
|
||||
|
||||
func setupController() {
|
||||
controller.delegate = PlayerModel.shared.appleAVPlayerViewControllerDelegate
|
||||
controller.allowsPictureInPicturePlayback = true
|
||||
if #available(iOS 14.2, *) {
|
||||
controller.canStartPictureInPictureAutomaticallyFromInline = true
|
||||
}
|
||||
PlayerModel.shared.avPlayerBackend.controller = controller
|
||||
}
|
||||
}
|
||||
|
||||
struct AppleAVPlayerLayerView: UIViewRepresentable {
|
||||
func makeUIView(context _: Context) -> some UIView {
|
||||
PlayerLayerView()
|
||||
PlayerLayerView(frame: .zero)
|
||||
}
|
||||
|
||||
func updateUIView(_: UIViewType, context _: Context) {}
|
||||
}
|
||||
#else
|
||||
|
||||
#elseif os(tvOS)
|
||||
struct AppleAVPlayerView: UIViewControllerRepresentable {
|
||||
func makeUIViewController(context _: Context) -> AppleAVPlayerViewController {
|
||||
let controller = AppleAVPlayerViewController()
|
||||
@ -23,4 +127,27 @@ import SwiftUI
|
||||
PlayerModel.shared.rebuildTVMenu()
|
||||
}
|
||||
}
|
||||
#else
|
||||
struct AppleAVPlayerView: NSViewRepresentable {
|
||||
@State private var pictureInPictureDelegate = MacOSPiPDelegate()
|
||||
|
||||
func makeNSView(context _: Context) -> some NSView {
|
||||
let view = AVPlayerView()
|
||||
view.player = PlayerModel.shared.avPlayerBackend.avPlayer
|
||||
view.showsFullScreenToggleButton = true
|
||||
view.allowsPictureInPicturePlayback = true
|
||||
view.pictureInPictureDelegate = pictureInPictureDelegate
|
||||
return view
|
||||
}
|
||||
|
||||
func updateNSView(_: NSViewType, context _: Context) {}
|
||||
}
|
||||
|
||||
struct AppleAVPlayerLayerView: NSViewRepresentable {
|
||||
func makeNSView(context _: Context) -> some NSView {
|
||||
PlayerLayerView(frame: .zero)
|
||||
}
|
||||
|
||||
func updateNSView(_: NSViewType, context _: Context) {}
|
||||
}
|
||||
#endif
|
||||
|
@ -1,3 +1,4 @@
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct PlayerBackendView: View {
|
||||
@ -7,6 +8,8 @@ struct PlayerBackendView: View {
|
||||
@ObservedObject private var player = PlayerModel.shared
|
||||
@ObservedObject private var safeAreaModel = SafeAreaModel.shared
|
||||
|
||||
@Default(.avPlayerUsesSystemControls) private var avPlayerUsesSystemControls
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .top) {
|
||||
Group {
|
||||
@ -16,7 +19,20 @@ struct PlayerBackendView: View {
|
||||
case .mpv:
|
||||
player.mpvPlayerView
|
||||
case .appleAVPlayer:
|
||||
player.avPlayerView
|
||||
#if os(tvOS)
|
||||
AppleAVPlayerView()
|
||||
#else
|
||||
if avPlayerUsesSystemControls,
|
||||
!player.playingInPictureInPicture,
|
||||
!player.avPlayerBackend.isStartingPiP
|
||||
{
|
||||
AppleAVPlayerView()
|
||||
} else if !avPlayerUsesSystemControls ||
|
||||
player.avPlayerBackend.isStartingPiP
|
||||
{
|
||||
AppleAVPlayerLayerView()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.zIndex(0)
|
||||
@ -31,17 +47,16 @@ struct PlayerBackendView: View {
|
||||
.onChange(of: proxy.size) { _ in player.playerSize = proxy.size }
|
||||
.onChange(of: player.controls.presentingOverlays) { _ in player.playerSize = proxy.size }
|
||||
})
|
||||
#if os(iOS)
|
||||
.padding(.top, player.playingFullScreen && verticalSizeClass == .regular ? 20 : 0)
|
||||
#endif
|
||||
|
||||
#if !os(tvOS)
|
||||
PlayerGestures()
|
||||
PlayerControls()
|
||||
#if os(iOS)
|
||||
.padding(.top, controlsTopPadding)
|
||||
.padding(.bottom, controlsBottomPadding)
|
||||
#endif
|
||||
if player.activeBackend == .mpv || !avPlayerUsesSystemControls {
|
||||
PlayerGestures()
|
||||
PlayerControls()
|
||||
#if os(iOS)
|
||||
.padding(.top, controlsTopPadding)
|
||||
.padding(.bottom, controlsBottomPadding)
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
hiddenControlsButton
|
||||
#endif
|
||||
|
@ -3,7 +3,7 @@ import SwiftUI
|
||||
|
||||
extension VideoPlayerView {
|
||||
var playerDragGesture: some Gesture {
|
||||
DragGesture(minimumDistance: 0, coordinateSpace: .global)
|
||||
DragGesture(minimumDistance: 30, coordinateSpace: .global)
|
||||
#if os(iOS)
|
||||
.updating($dragGestureOffset) { value, state, _ in
|
||||
guard isVerticalDrag else { return }
|
||||
@ -36,7 +36,12 @@ extension VideoPlayerView {
|
||||
}
|
||||
#endif
|
||||
|
||||
if !isVerticalDrag, horizontalPlayerGestureEnabled, abs(horizontalDrag) > seekGestureSensitivity, !isHorizontalDrag {
|
||||
if !isVerticalDrag,
|
||||
horizontalPlayerGestureEnabled,
|
||||
abs(horizontalDrag) > seekGestureSensitivity,
|
||||
!isHorizontalDrag,
|
||||
player.activeBackend == .mpv || !avPlayerUsesSystemControls
|
||||
{
|
||||
isHorizontalDrag = true
|
||||
player.seek.onSeekGestureStart()
|
||||
viewDragOffset = 0
|
||||
@ -80,6 +85,16 @@ extension VideoPlayerView {
|
||||
player.seek.onSeekGestureEnd()
|
||||
}
|
||||
|
||||
if viewDragOffset > 60,
|
||||
player.playingFullScreen
|
||||
{
|
||||
#if os(iOS)
|
||||
player.lockedOrientation = nil
|
||||
#endif
|
||||
player.exitFullScreen(showControls: false)
|
||||
viewDragOffset = 0
|
||||
return
|
||||
}
|
||||
isVerticalDrag = false
|
||||
|
||||
guard player.presentingPlayer,
|
||||
|
@ -4,8 +4,8 @@ import SwiftUI
|
||||
struct VideoPlayerSizeModifier: ViewModifier {
|
||||
let geometry: GeometryProxy
|
||||
let aspectRatio: Double?
|
||||
let minimumHeightLeft: Double
|
||||
let fullScreen: Bool
|
||||
var detailsHiddenInFullScreen = true
|
||||
|
||||
#if os(iOS)
|
||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||
@ -14,26 +14,31 @@ struct VideoPlayerSizeModifier: ViewModifier {
|
||||
init(
|
||||
geometry: GeometryProxy,
|
||||
aspectRatio: Double? = nil,
|
||||
minimumHeightLeft: Double? = nil,
|
||||
fullScreen: Bool = false
|
||||
fullScreen: Bool = false,
|
||||
detailsHiddenInFullScreen: Bool = false
|
||||
) {
|
||||
self.geometry = geometry
|
||||
self.aspectRatio = aspectRatio ?? VideoPlayerView.defaultAspectRatio
|
||||
self.minimumHeightLeft = minimumHeightLeft ?? VideoPlayerView.defaultMinimumHeightLeft
|
||||
self.fullScreen = fullScreen
|
||||
self.detailsHiddenInFullScreen = detailsHiddenInFullScreen
|
||||
}
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.frame(width: geometry.size.width)
|
||||
.frame(maxWidth: geometry.size.width)
|
||||
.frame(maxHeight: maxHeight)
|
||||
|
||||
#if !os(macOS)
|
||||
.aspectRatio(fullScreen ? nil : usedAspectRatio, contentMode: usedAspectRatioContentMode)
|
||||
.aspectRatio(ratio, contentMode: usedAspectRatioContentMode)
|
||||
#endif
|
||||
}
|
||||
|
||||
var ratio: CGFloat? {
|
||||
fullScreen ? detailsHiddenInFullScreen ? nil : usedAspectRatio : usedAspectRatio
|
||||
}
|
||||
|
||||
var usedAspectRatio: Double {
|
||||
guard let aspectRatio, aspectRatio > 0, aspectRatio < VideoPlayerView.defaultAspectRatio else {
|
||||
guard let aspectRatio, aspectRatio > 0 else {
|
||||
return VideoPlayerView.defaultAspectRatio
|
||||
}
|
||||
|
||||
@ -50,15 +55,13 @@ struct VideoPlayerSizeModifier: ViewModifier {
|
||||
|
||||
var maxHeight: Double {
|
||||
guard !fullScreen else {
|
||||
return .infinity
|
||||
if detailsHiddenInFullScreen {
|
||||
return geometry.size.height
|
||||
} else {
|
||||
return geometry.size.width / usedAspectRatio
|
||||
}
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
let height = verticalSizeClass == .regular ? geometry.size.height - minimumHeightLeft : .infinity
|
||||
#else
|
||||
let height = geometry.size.height - minimumHeightLeft
|
||||
#endif
|
||||
|
||||
return [height, 0].max()!
|
||||
return max(geometry.size.height - VideoPlayerView.defaultMinimumHeightLeft, 0)
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +64,7 @@ struct VideoPlayerView: View {
|
||||
@Default(.playerSidebar) var playerSidebar
|
||||
@Default(.gestureBackwardSeekDuration) private var gestureBackwardSeekDuration
|
||||
@Default(.gestureForwardSeekDuration) private var gestureForwardSeekDuration
|
||||
@Default(.avPlayerUsesSystemControls) internal var avPlayerUsesSystemControls
|
||||
|
||||
@ObservedObject internal var controlsOverlayModel = ControlOverlaysModel.shared
|
||||
|
||||
@ -98,12 +99,12 @@ struct VideoPlayerView: View {
|
||||
return GeometryReader { geometry in
|
||||
HStack(spacing: 0) {
|
||||
content
|
||||
.ignoresSafeArea(.all, edges: .bottom)
|
||||
.frame(height: playerHeight.isNil ? nil : Double(playerHeight!))
|
||||
.onAppear {
|
||||
playerSize = geometry.size
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea(.all, edges: .bottom)
|
||||
.frame(height: playerHeight.isNil ? nil : Double(playerHeight!))
|
||||
.onChange(of: geometry.size) { _ in
|
||||
self.playerSize = geometry.size
|
||||
}
|
||||
@ -279,7 +280,8 @@ struct VideoPlayerView: View {
|
||||
VideoPlayerSizeModifier(
|
||||
geometry: geometry,
|
||||
aspectRatio: player.aspectRatio,
|
||||
fullScreen: fullScreenPlayer
|
||||
fullScreen: fullScreenPlayer,
|
||||
detailsHiddenInFullScreen: detailsHiddenInFullScreen
|
||||
)
|
||||
)
|
||||
.onHover { hovering in
|
||||
@ -303,15 +305,12 @@ struct VideoPlayerView: View {
|
||||
|
||||
.background(Color.black)
|
||||
|
||||
if !fullScreenPlayer {
|
||||
if !detailsHiddenInFullScreen {
|
||||
VideoDetails(
|
||||
video: player.videoForDisplay,
|
||||
fullScreen: $fullScreenDetails,
|
||||
sidebarQueue: $sidebarQueue
|
||||
)
|
||||
#if os(iOS)
|
||||
.ignoresSafeArea(.all, edges: .bottom)
|
||||
#endif
|
||||
.modifier(VideoDetailsPaddingModifier(
|
||||
playerSize: player.playerSize,
|
||||
fullScreen: fullScreenDetails
|
||||
@ -369,7 +368,7 @@ struct VideoPlayerView: View {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if !fullScreenPlayer {
|
||||
if !detailsHiddenInFullScreen {
|
||||
#if os(iOS)
|
||||
if sidebarQueue {
|
||||
List {
|
||||
@ -404,6 +403,12 @@ struct VideoPlayerView: View {
|
||||
}
|
||||
#if os(iOS)
|
||||
.statusBar(hidden: fullScreenPlayer)
|
||||
.backport
|
||||
.toolbarBackground(colorScheme == .light ? .white : .black)
|
||||
.backport
|
||||
.toolbarBackgroundVisibility(true)
|
||||
.backport
|
||||
.toolbarColorScheme(colorScheme)
|
||||
#endif
|
||||
#if os(macOS)
|
||||
.background(
|
||||
@ -414,6 +419,16 @@ struct VideoPlayerView: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
var detailsHiddenInFullScreen: Bool {
|
||||
guard fullScreenPlayer else { return false }
|
||||
|
||||
if player.activeBackend == .mpv {
|
||||
return true
|
||||
}
|
||||
|
||||
return !avPlayerUsesSystemControls || verticalSizeClass == .compact
|
||||
}
|
||||
|
||||
var fullScreenPlayer: Bool {
|
||||
#if os(iOS)
|
||||
player.playingFullScreen || verticalSizeClass == .compact
|
||||
|
@ -2,6 +2,8 @@ import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct PlayerControlsSettings: View {
|
||||
@Default(.avPlayerUsesSystemControls) private var avPlayerUsesSystemControls
|
||||
|
||||
@Default(.systemControlsCommands) private var systemControlsCommands
|
||||
@Default(.playerControlsLayout) private var playerControlsLayout
|
||||
@Default(.fullScreenPlayerControlsLayout) private var fullScreenPlayerControlsLayout
|
||||
@ -61,6 +63,9 @@ struct PlayerControlsSettings: View {
|
||||
@ViewBuilder var sections: some View {
|
||||
#if !os(tvOS)
|
||||
Section(header: SettingsHeader(text: "Controls".localized()), footer: controlsLayoutFooter) {
|
||||
#if !os(tvOS)
|
||||
avPlayerUsesSystemControlsToggle
|
||||
#endif
|
||||
horizontalPlayerGestureEnabledToggle
|
||||
SettingsHeader(text: "Seek gesture sensitivity".localized(), secondary: true)
|
||||
seekGestureSensitivityPicker
|
||||
@ -143,6 +148,10 @@ struct PlayerControlsSettings: View {
|
||||
Toggle("Seek with horizontal swipe on video", isOn: $horizontalPlayerGestureEnabled)
|
||||
}
|
||||
|
||||
private var avPlayerUsesSystemControlsToggle: some View {
|
||||
Toggle("Use system controls with AVPlayer", isOn: $avPlayerUsesSystemControls)
|
||||
}
|
||||
|
||||
private var seekGestureSensitivityPicker: some View {
|
||||
Picker("Seek gesture sensitivity", selection: $seekGestureSensitivity) {
|
||||
Text("Highest").tag(1.0)
|
||||
|
@ -723,6 +723,8 @@
|
||||
37B767DD2677C3CA0098BAA8 /* PlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */; };
|
||||
37B795902771DAE0001CF27B /* OpenURLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B7958F2771DAE0001CF27B /* OpenURLHandler.swift */; };
|
||||
37B795912771DAE0001CF27B /* OpenURLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B7958F2771DAE0001CF27B /* OpenURLHandler.swift */; };
|
||||
37B7CFE92A19603B001B0564 /* ToolbarBackground+Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B7CFE82A19603B001B0564 /* ToolbarBackground+Backport.swift */; };
|
||||
37B7CFEB2A1960EC001B0564 /* ToolbarColorScheme+Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B7CFEA2A1960EC001B0564 /* ToolbarColorScheme+Backport.swift */; };
|
||||
37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */; };
|
||||
37B81AFA26D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */; };
|
||||
37B81AFC26D2C9C900675966 /* VideoDetailsPaddingModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81AFB26D2C9C900675966 /* VideoDetailsPaddingModifier.swift */; };
|
||||
@ -885,6 +887,10 @@
|
||||
37D9BA0829507F69002586BD /* PlayerControlsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D9BA0529507F69002586BD /* PlayerControlsSettings.swift */; };
|
||||
37DCD3112A18E8150059A470 /* OrientationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DCD3102A18E8150059A470 /* OrientationModel.swift */; };
|
||||
37DCD3152A18F7630059A470 /* SafeAreaModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DCD3142A18F7630059A470 /* SafeAreaModel.swift */; };
|
||||
37DCD3172A191A180059A470 /* AVPlayerViewController+FullScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DCD3162A191A180059A470 /* AVPlayerViewController+FullScreen.swift */; };
|
||||
37DCD3182A191A180059A470 /* AVPlayerViewController+FullScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DCD3162A191A180059A470 /* AVPlayerViewController+FullScreen.swift */; };
|
||||
37DCD3192A191A180059A470 /* AVPlayerViewController+FullScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DCD3162A191A180059A470 /* AVPlayerViewController+FullScreen.swift */; };
|
||||
37DCD31A2A191A180059A470 /* AVPlayerViewController+FullScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DCD3162A191A180059A470 /* AVPlayerViewController+FullScreen.swift */; };
|
||||
37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; };
|
||||
37DD87C8271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; };
|
||||
37DD87C9271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; };
|
||||
@ -1379,6 +1385,8 @@
|
||||
37B4E804277D0AB4004BF56A /* Orientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Orientation.swift; sourceTree = "<group>"; };
|
||||
37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerModel.swift; sourceTree = "<group>"; };
|
||||
37B7958F2771DAE0001CF27B /* OpenURLHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenURLHandler.swift; sourceTree = "<group>"; };
|
||||
37B7CFE82A19603B001B0564 /* ToolbarBackground+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ToolbarBackground+Backport.swift"; sourceTree = "<group>"; };
|
||||
37B7CFEA2A1960EC001B0564 /* ToolbarColorScheme+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ToolbarColorScheme+Backport.swift"; sourceTree = "<group>"; };
|
||||
37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerSizeModifier.swift; sourceTree = "<group>"; };
|
||||
37B81AFB26D2C9C900675966 /* VideoDetailsPaddingModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetailsPaddingModifier.swift; sourceTree = "<group>"; };
|
||||
37B81AFE26D2CA3700675966 /* VideoDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetails.swift; sourceTree = "<group>"; };
|
||||
@ -1458,6 +1466,7 @@
|
||||
37D9BA0529507F69002586BD /* PlayerControlsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsSettings.swift; sourceTree = "<group>"; };
|
||||
37DCD3102A18E8150059A470 /* OrientationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrientationModel.swift; sourceTree = "<group>"; };
|
||||
37DCD3142A18F7630059A470 /* SafeAreaModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeAreaModel.swift; sourceTree = "<group>"; };
|
||||
37DCD3162A191A180059A470 /* AVPlayerViewController+FullScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AVPlayerViewController+FullScreen.swift"; sourceTree = "<group>"; };
|
||||
37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStreams.swift; sourceTree = "<group>"; };
|
||||
37DD9DA22785BBC900539416 /* NoCommentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoCommentsView.swift; sourceTree = "<group>"; };
|
||||
37DD9DAF2785D58D00539416 /* RefreshControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RefreshControl.swift; sourceTree = "<group>"; };
|
||||
@ -1897,6 +1906,8 @@
|
||||
37E80F3F287B472300561799 /* ScrollContentBackground+Backport.swift */,
|
||||
376E331128AD3B320070E30C /* ScrollDismissesKeyboard+Backport.swift */,
|
||||
3722AEBF274DAEB8005EA4D6 /* Tint+Backport.swift */,
|
||||
37B7CFE82A19603B001B0564 /* ToolbarBackground+Backport.swift */,
|
||||
37B7CFEA2A1960EC001B0564 /* ToolbarColorScheme+Backport.swift */,
|
||||
3727B74927872A920021C15E /* VisualEffectBlur-iOS.swift */,
|
||||
3727B74727872A500021C15E /* VisualEffectBlur-macOS.swift */,
|
||||
);
|
||||
@ -2228,6 +2239,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
379775922689365600DD52A8 /* Array+Next.swift */,
|
||||
37DCD3162A191A180059A470 /* AVPlayerViewController+FullScreen.swift */,
|
||||
376578842685429C00D4EA09 /* CaseIterable+Next.swift */,
|
||||
37C0697D2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift */,
|
||||
378AE942274EF00A006A4EE1 /* Color+Background.swift */,
|
||||
@ -3296,6 +3308,8 @@
|
||||
37BE0BCF26A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
|
||||
377692562946476F0055EC18 /* ChannelPlaylistsCacheModel.swift in Sources */,
|
||||
3769C02E2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */,
|
||||
37B7CFE92A19603B001B0564 /* ToolbarBackground+Backport.swift in Sources */,
|
||||
37DCD3172A191A180059A470 /* AVPlayerViewController+FullScreen.swift in Sources */,
|
||||
379EF9E029AA585F009FE6C6 /* HideShortsButtons.swift in Sources */,
|
||||
37F5E8B6291BE9D0006C15F5 /* URLBookmarkModel.swift in Sources */,
|
||||
37579D5D27864F5F00FD0B98 /* Help.swift in Sources */,
|
||||
@ -3305,6 +3319,7 @@
|
||||
375EC972289F2ABF00751258 /* MultiselectRow.swift in Sources */,
|
||||
37001563271B1F250049C794 /* AccountsModel.swift in Sources */,
|
||||
3795593627B08538007FF8F4 /* StreamControl.swift in Sources */,
|
||||
37B7CFEB2A1960EC001B0564 /* ToolbarColorScheme+Backport.swift in Sources */,
|
||||
37A2B346294723850050933E /* CacheModel.swift in Sources */,
|
||||
37CC3F50270D010D00608308 /* VideoBanner.swift in Sources */,
|
||||
378E50FB26FE8B9F00F49626 /* Instance.swift in Sources */,
|
||||
@ -3558,6 +3573,7 @@
|
||||
3711404026B206A6005B3555 /* SearchModel.swift in Sources */,
|
||||
37484C2A26FC83FF00287258 /* AccountForm.swift in Sources */,
|
||||
37BE0BD026A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
|
||||
37DCD3182A191A180059A470 /* AVPlayerViewController+FullScreen.swift in Sources */,
|
||||
373CFAEC26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */,
|
||||
37D2E0D528B67EFC00F64D52 /* Delay.swift in Sources */,
|
||||
37977584268922F600DD52A8 /* InvidiousAPI.swift in Sources */,
|
||||
@ -3615,6 +3631,7 @@
|
||||
3774124D27387D2300423605 /* PlaylistsModel.swift in Sources */,
|
||||
3774124B27387D2300423605 /* ThumbnailsModel.swift in Sources */,
|
||||
3774125427387D2300423605 /* Store.swift in Sources */,
|
||||
37DCD31A2A191A180059A470 /* AVPlayerViewController+FullScreen.swift in Sources */,
|
||||
3774125027387D2300423605 /* Video.swift in Sources */,
|
||||
37EF9A79275BEB8E0043B585 /* CommentView.swift in Sources */,
|
||||
3774125327387D2300423605 /* Country.swift in Sources */,
|
||||
@ -3813,6 +3830,7 @@
|
||||
372D85E0283842EE00FF3C7D /* PlayerLayerView.swift in Sources */,
|
||||
37CEE4C32677B697005A1EFE /* Stream.swift in Sources */,
|
||||
37F64FE626FE70A60081B69E /* RedrawOnModifier.swift in Sources */,
|
||||
37DCD3192A191A180059A470 /* AVPlayerViewController+FullScreen.swift in Sources */,
|
||||
37B2631C2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */,
|
||||
37484C2B26FC83FF00287258 /* AccountForm.swift in Sources */,
|
||||
37FB2860272225E800A57617 /* ContentItemView.swift in Sources */,
|
||||
|
Loading…
Reference in New Issue
Block a user