mirror of
https://github.com/yattee/yattee.git
synced 2025-12-12 19:18:16 +00:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
450a4b42f7 | ||
|
|
db4b3115b1 | ||
|
|
fba465a22a | ||
|
|
d996069a20 | ||
|
|
d7a2564617 | ||
|
|
d5f8e35430 | ||
|
|
8f9de6d1be | ||
|
|
0fc6f7fdb7 | ||
|
|
d5f88a73f8 | ||
|
|
97af5a6e0c | ||
|
|
4c5ef920b4 | ||
|
|
a55683e6bf | ||
|
|
739ca007e8 | ||
|
|
7d7bd40a89 | ||
|
|
c6798be167 | ||
|
|
2b7ccc4b03 | ||
|
|
08ce572b9e | ||
|
|
1cc66fdc10 | ||
|
|
34a05433d5 | ||
|
|
5383cf0e90 | ||
|
|
a4fdd50388 | ||
|
|
f67b1d4feb | ||
|
|
b53b5eac56 | ||
|
|
6c5b8ef3ec | ||
|
|
226da4d2be | ||
|
|
49ffffae53 | ||
|
|
848a43ce7f | ||
|
|
6ca0e82feb | ||
|
|
f7f53c6417 | ||
|
|
55a0b2dee6 | ||
|
|
9f0700d2bf | ||
|
|
ab0c2e7b84 | ||
|
|
d603ef7431 | ||
|
|
281e0510cd | ||
|
|
837c9a3f75 | ||
|
|
bcec9d09ab | ||
|
|
82a09d1584 | ||
|
|
dae667fa8a | ||
|
|
0f802684f2 | ||
|
|
59b49c2e2f | ||
|
|
3a8d6aed76 | ||
|
|
2952e10359 | ||
|
|
59f84ec129 | ||
|
|
a76dae6656 | ||
|
|
a208ef9147 | ||
|
|
7a998d2d69 | ||
|
|
f3659904dc | ||
|
|
72246448f1 | ||
|
|
6617ad5fc6 | ||
|
|
65b3eb60d9 | ||
|
|
0174d2f8a0 | ||
|
|
8f340586a6 | ||
|
|
23e07baa7a |
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -23,7 +23,9 @@ jobs:
|
||||
testflight:
|
||||
strategy:
|
||||
matrix:
|
||||
lane: ['mac beta', 'ios beta', 'tvos beta']
|
||||
# disabled mac beta lane
|
||||
# lane: ['mac beta', 'ios beta', 'tvos beta']
|
||||
lane: ['ios beta', 'tvos beta']
|
||||
name: Releasing ${{ matrix.lane }} version to TestFlight
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
25
CHANGELOG.md
25
CHANGELOG.md
@@ -1,9 +1,16 @@
|
||||
## Build 146
|
||||
* Fixed playback settings sheet height
|
||||
* Localizations updates fixes
|
||||
* Other minor changes and fixes
|
||||
|
||||
## Previous Builds
|
||||
* Added button to scroll to top in comments and setting to toggle it
|
||||
* Switch seek duration +/- buttons in controls settings
|
||||
* Other minor changes and fixes
|
||||
## Build 150
|
||||
* Added support for AVPlayer native system controls on iOS and macOS
|
||||
- Use system features such as AirPlay, subtitles switching (Piped with HLS), text detection and copy and more
|
||||
- Added Controls setting: "Use system controls with AVPlayer"
|
||||
* Player rotates for landscape videos on entering full screen on iOS
|
||||
- Player > Orientation setting: "Rotate when entering fullscreen on landscape video"
|
||||
* Added Player > Playback setting: "Close video and player on end"
|
||||
* Added reporting for opening stream in OSD for AVPlayer
|
||||
* Fixed issue with opening channels and playlists links
|
||||
* Fixed issues where controls/player layout could break (e.g., when going to background and back)
|
||||
* Fixed issue where stream picker would show duplicate entries
|
||||
* Fixed issue where search suggestions would show unnecessary bottom padding
|
||||
* Fixed landscape channel sheet layout in player
|
||||
* Fixed reported crashes
|
||||
* Localization updates and fixes
|
||||
* Other minor fixes and improvements
|
||||
|
||||
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"]
|
||||
@@ -37,9 +38,7 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
!avPlayer.currentItem.isNil
|
||||
}
|
||||
|
||||
var isLoadingVideo: Bool {
|
||||
model.currentItem == nil || model.time == nil || !model.time!.isValid
|
||||
}
|
||||
var isLoadingVideo = false
|
||||
|
||||
var isPlaying: Bool {
|
||||
avPlayer.timeControlStatus == .playing
|
||||
@@ -84,6 +83,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 +111,9 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
addPlayerTimeControlStatusObserver()
|
||||
|
||||
playerLayer.player = avPlayer
|
||||
#if os(iOS)
|
||||
controller.player = avPlayer
|
||||
#endif
|
||||
}
|
||||
|
||||
func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream? {
|
||||
@@ -130,6 +136,8 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
preservingTime: Bool,
|
||||
upgrading _: Bool
|
||||
) {
|
||||
isLoadingVideo = true
|
||||
|
||||
if let url = stream.singleAssetURL {
|
||||
model.logger.info("playing stream with one asset\(stream.kind == .hls ? " (HLS)" : ""): \(url)")
|
||||
|
||||
@@ -467,12 +475,10 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
return
|
||||
}
|
||||
|
||||
self.isLoadingVideo = false
|
||||
|
||||
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 +493,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 +698,6 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
|
||||
func didChangeTo() {
|
||||
if startPictureInPictureOnSwitch {
|
||||
startPictureInPictureOnSwitch = false
|
||||
tryStartingPictureInPicture()
|
||||
} else if model.musicMode {
|
||||
startMusicMode()
|
||||
@@ -697,6 +706,10 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
}
|
||||
}
|
||||
|
||||
var isStartingPiP: Bool {
|
||||
startPictureInPictureOnPlay || startPictureInPictureOnSwitch
|
||||
}
|
||||
|
||||
func tryStartingPictureInPicture() {
|
||||
guard let controller = model.pipController else { return }
|
||||
|
||||
@@ -712,6 +725,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() {}
|
||||
|
||||
@@ -318,14 +318,14 @@ final class MPVClient: ObservableObject {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
let model = self.backend.model
|
||||
let aspectRatio = self.aspectRatio > 0 && self.aspectRatio < VideoPlayerView.defaultAspectRatio ? self.aspectRatio : VideoPlayerView.defaultAspectRatio
|
||||
let height = [model.playerSize.height, model.playerSize.width / aspectRatio].min()!
|
||||
var insets = 0.0
|
||||
#if os(iOS)
|
||||
insets = OrientationTracker.shared.currentInterfaceOrientation.isPortrait ? SafeAreaModel.shared.safeArea.bottom : 0
|
||||
#endif
|
||||
let offsetY = max(0, model.playingFullScreen ? ((model.playerSize.height / 2.0) - ((height + insets) / 2)) : 0)
|
||||
UIView.animate(withDuration: 0.2, animations: {
|
||||
let aspectRatio = self.aspectRatio > 0 && self.aspectRatio < VideoPlayerView.defaultAspectRatio ? self.aspectRatio : VideoPlayerView.defaultAspectRatio
|
||||
let height = [model.playerSize.height, model.playerSize.width / aspectRatio].min()!
|
||||
var insets = 0.0
|
||||
#if os(iOS)
|
||||
insets = OrientationTracker.shared.currentInterfaceOrientation.isPortrait ? SafeArea.insets.bottom : 0
|
||||
#endif
|
||||
let offsetY = model.playingFullScreen ? ((model.playerSize.height / 2.0) - ((height + insets) / 2)) : 0
|
||||
self.glView?.frame = CGRect(x: 0, y: offsetY, width: roundedWidth, height: height)
|
||||
}) { completion in
|
||||
if completion {
|
||||
|
||||
@@ -110,13 +110,15 @@ extension PlayerBackend {
|
||||
model.prepareCurrentItemForHistory(finished: true)
|
||||
|
||||
if model.queue.isEmpty {
|
||||
#if os(tvOS)
|
||||
if model.activeBackend == .appleAVPlayer {
|
||||
model.avPlayerBackend.controller?.dismiss(animated: false)
|
||||
}
|
||||
#endif
|
||||
model.resetQueue()
|
||||
model.hide()
|
||||
if Defaults[.closeVideoOnEOF] {
|
||||
#if os(tvOS)
|
||||
if model.activeBackend == .appleAVPlayer {
|
||||
model.avPlayerBackend.controller?.dismiss(animated: false)
|
||||
}
|
||||
#endif
|
||||
model.resetQueue()
|
||||
model.hide()
|
||||
}
|
||||
} else {
|
||||
model.advanceToNextItem()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -129,6 +128,7 @@ final class PlayerModel: ObservableObject {
|
||||
#if os(iOS)
|
||||
@Published var lockedOrientation: UIInterfaceOrientationMask?
|
||||
@Default(.rotateToPortraitOnExitFullScreen) private var rotateToPortraitOnExitFullScreen
|
||||
@Default(.rotateToLandscapeOnEnterFullScreen) private var rotateToLandscapeOnEnterFullScreen
|
||||
#endif
|
||||
|
||||
var accounts: AccountsModel { .shared }
|
||||
@@ -152,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 {
|
||||
@@ -163,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
|
||||
@@ -186,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
|
||||
}
|
||||
|
||||
@@ -474,7 +479,14 @@ 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()
|
||||
controls.hideOverlays()
|
||||
|
||||
#if !os(macOS)
|
||||
UIApplication.shared.isIdleTimerDisabled = presentingPlayer
|
||||
@@ -491,6 +503,18 @@ final class PlayerModel: ObservableObject {
|
||||
self?.pause()
|
||||
}
|
||||
}
|
||||
|
||||
if !presentingPlayer {
|
||||
#if os(iOS)
|
||||
if Defaults[.lockPortraitWhenBrowsing] {
|
||||
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
||||
} else {
|
||||
Orientation.lockOrientation(.allButUpsideDown)
|
||||
}
|
||||
|
||||
OrientationModel.shared.stopOrientationUpdates()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
func changeActiveBackend(from: PlayerBackendType, to: PlayerBackendType, changingStream: Bool = true) {
|
||||
@@ -541,10 +565,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
|
||||
}
|
||||
@@ -630,8 +659,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)
|
||||
@@ -881,7 +910,7 @@ final class PlayerModel: ObservableObject {
|
||||
#else
|
||||
func handleEnterForeground() {
|
||||
setNeedsDrawing(presentingPlayer)
|
||||
avPlayerBackend.playerLayer.player = avPlayerBackend.avPlayer
|
||||
avPlayerBackend.bindPlayerToLayer()
|
||||
|
||||
guard closePiPAndOpenPlayerOnEnteringForeground, playingInPictureInPicture else {
|
||||
return
|
||||
@@ -895,7 +924,7 @@ final class PlayerModel: ObservableObject {
|
||||
if Defaults[.pauseOnEnteringBackground], !playingInPictureInPicture, !musicMode {
|
||||
pause()
|
||||
} else if !playingInPictureInPicture {
|
||||
avPlayerBackend.playerLayer.player = nil
|
||||
avPlayerBackend.removePlayerFromLayer()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -918,6 +947,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
|
||||
@@ -981,24 +1017,34 @@ final class PlayerModel: ObservableObject {
|
||||
Windows.player.toggleFullScreen()
|
||||
#endif
|
||||
|
||||
playingFullScreen = !isFullScreen
|
||||
|
||||
#if os(iOS)
|
||||
if !playingFullScreen {
|
||||
playingFullScreen = true
|
||||
Orientation.lockOrientation(.allButUpsideDown)
|
||||
if playingFullScreen {
|
||||
if activeBackend == .appleAVPlayer, avPlayerUsesSystemControls {
|
||||
avPlayerBackend.controller.enterFullScreen(animated: true)
|
||||
return
|
||||
}
|
||||
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
|
||||
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)
|
||||
avPlayerBackend.controller.dismiss(animated: true)
|
||||
return
|
||||
}
|
||||
let rotationOrientation = rotateToPortraitOnExitFullScreen ? UIInterfaceOrientation.portrait : nil
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: rotationOrientation)
|
||||
// TODO: rework to move view before rotating
|
||||
if SafeArea.insets.left > 0 {
|
||||
Delay.by(0.15) {
|
||||
self.playingFullScreen = false
|
||||
}
|
||||
} else {
|
||||
self.playingFullScreen = false
|
||||
}
|
||||
}
|
||||
#else
|
||||
playingFullScreen = !isFullScreen
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1036,6 +1082,12 @@ final class PlayerModel: ObservableObject {
|
||||
#endif
|
||||
}
|
||||
|
||||
var currentVideoIsLandscape: Bool {
|
||||
guard currentVideo != nil else { return false }
|
||||
|
||||
return aspectRatio > 1
|
||||
}
|
||||
|
||||
var formattedSize: String {
|
||||
guard let videoWidth = backend?.videoWidth, let videoHeight = backend?.videoHeight else { return "unknown" }
|
||||
return "\(String(format: "%.2f", videoWidth))\u{d7}\(String(format: "%.2f", videoHeight))"
|
||||
|
||||
@@ -88,7 +88,7 @@ extension PlayerModel {
|
||||
guard let playerInstance = self.playerInstance else { return }
|
||||
let streamsInstance = video.streams.compactMap(\.instance).first
|
||||
|
||||
if video.streams.isEmpty || streamsInstance != playerInstance {
|
||||
if video.streams.isEmpty || streamsInstance.isNil || streamsInstance!.apiURLString != playerInstance.apiURLString {
|
||||
self.loadAvailableStreams(video) { [weak self] _ in
|
||||
self?.videoBeingOpened = nil
|
||||
}
|
||||
|
||||
@@ -32,6 +32,17 @@ extension PlayerModel {
|
||||
}
|
||||
}
|
||||
|
||||
var playerItemEndTimeWithSegments: CMTime? {
|
||||
if let duration = playerItemDuration,
|
||||
let segment = sponsorBlock.segments.last,
|
||||
segment.endTime.seconds >= duration.seconds - 3
|
||||
{
|
||||
return segment.endTime
|
||||
}
|
||||
|
||||
return playerItemDuration
|
||||
}
|
||||
|
||||
private func skip(_ segment: Segment, at time: CMTime) {
|
||||
if let duration = playerItemDuration, segment.endTime.seconds >= duration.seconds - 3 {
|
||||
logger.error("segment end time is: \(segment.end) when player item duration is: \(duration.seconds)")
|
||||
|
||||
@@ -41,7 +41,7 @@ extension PlayerModel {
|
||||
self.logger.info("ignoring loaded streams from \(instance.description) as current video has changed")
|
||||
return
|
||||
}
|
||||
self.availableStreams += self.streamsWithInstance(instance: instance, streams: video.streams)
|
||||
self.availableStreams = self.streamsWithInstance(instance: instance, streams: video.streams)
|
||||
} else {
|
||||
self.logger.critical("no streams available from \(instance.description)")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ final class ShareViewController: SLComposeServiceViewController {
|
||||
self.open(url: url)
|
||||
}
|
||||
|
||||
self.extensionContext!.completeRequest(returningItems: nil, completionHandler: nil)
|
||||
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,6 @@ struct ChannelPlaylistView: View {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if os(macOS)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
if showCloseButton {
|
||||
@@ -97,7 +96,9 @@ struct ChannelPlaylistView: View {
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
#if os(macOS)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: playlistButtonsPlacement) {
|
||||
HStack {
|
||||
ListingStyleButtons(listingStyle: $channelPlaylistListingStyle)
|
||||
|
||||
@@ -66,7 +66,7 @@ struct ChannelVideosView: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
#endif
|
||||
|
||||
VerticalCells(items: contentItems) {
|
||||
VerticalCells(items: contentItems, edgesIgnoringSafeArea: .init()) {
|
||||
if let description = presentedChannel?.description, !description.isEmpty {
|
||||
Button {
|
||||
withAnimation(.spring()) {
|
||||
|
||||
@@ -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)
|
||||
@@ -157,6 +158,7 @@ extension Defaults.Keys {
|
||||
#if !os(macOS)
|
||||
static let pauseOnEnteringBackground = Key<Bool>("pauseOnEnteringBackground", default: true)
|
||||
#endif
|
||||
static let closeVideoOnEOF = Key<Bool>("closeVideoOnEOF", default: false)
|
||||
static let closePiPOnNavigation = Key<Bool>("closePiPOnNavigation", default: false)
|
||||
static let closePiPOnOpeningPlayer = Key<Bool>("closePiPOnOpeningPlayer", default: false)
|
||||
#if !os(macOS)
|
||||
@@ -189,6 +191,10 @@ extension Defaults.Keys {
|
||||
static let honorSystemOrientationLock = Key<Bool>("honorSystemOrientationLock", default: true)
|
||||
static let enterFullscreenInLandscape = Key<Bool>("enterFullscreenInLandscape", default: UIDevice.current.userInterfaceIdiom == .phone)
|
||||
static let rotateToPortraitOnExitFullScreen = Key<Bool>("rotateToPortraitOnExitFullScreen", default: UIDevice.current.userInterfaceIdiom == .phone)
|
||||
static let rotateToLandscapeOnEnterFullScreen = Key<FullScreenRotationSetting>(
|
||||
"rotateToLandscapeOnEnterFullScreen",
|
||||
default: UIDevice.current.userInterfaceIdiom == .phone ? .landscapeRight : .disabled
|
||||
)
|
||||
#endif
|
||||
|
||||
static let showMPVPlaybackStats = Key<Bool>("showMPVPlaybackStats", default: false)
|
||||
@@ -416,3 +422,26 @@ enum PlayerTapGestureAction: String, CaseIterable, Defaults.Serializable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum FullScreenRotationSetting: String, CaseIterable, Defaults.Serializable {
|
||||
case disabled
|
||||
case landscapeLeft
|
||||
case landscapeRight
|
||||
|
||||
#if os(iOS)
|
||||
var interaceOrientation: UIInterfaceOrientation {
|
||||
switch self {
|
||||
case .landscapeLeft:
|
||||
return .landscapeLeft
|
||||
case .landscapeRight:
|
||||
return .landscapeRight
|
||||
default:
|
||||
return .portrait
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
var isRotating: Bool {
|
||||
self != .disabled
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,10 +44,10 @@ struct QueueView: View {
|
||||
|
||||
var label: String {
|
||||
if items.count < 2 {
|
||||
return "Next in Queue"
|
||||
return "Next in Queue".localized()
|
||||
}
|
||||
|
||||
return "Next in Queue (\(items.count))"
|
||||
return "Next in Queue".localized() + " (\(items.count))"
|
||||
}
|
||||
|
||||
var limitedItems: [ContentItem] {
|
||||
|
||||
@@ -15,22 +15,36 @@ struct ContentView: View {
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
#endif
|
||||
|
||||
@Default(.avPlayerUsesSystemControls) private var avPlayerUsesSystemControls
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
#if os(iOS)
|
||||
if Constants.isIPhone {
|
||||
AppTabNavigation()
|
||||
} else {
|
||||
if horizontalSizeClass == .compact {
|
||||
AppTabNavigation()
|
||||
} else {
|
||||
AppSidebarNavigation()
|
||||
GeometryReader { proxy in
|
||||
Group {
|
||||
#if os(iOS)
|
||||
Group {
|
||||
if Constants.isIPhone {
|
||||
AppTabNavigation()
|
||||
} else {
|
||||
if horizontalSizeClass == .compact {
|
||||
AppTabNavigation()
|
||||
} else {
|
||||
AppSidebarNavigation()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#elseif os(macOS)
|
||||
AppSidebarNavigation()
|
||||
#elseif os(tvOS)
|
||||
TVNavigationView()
|
||||
#elseif os(macOS)
|
||||
AppSidebarNavigation()
|
||||
#elseif os(tvOS)
|
||||
TVNavigationView()
|
||||
#endif
|
||||
}
|
||||
#if !os(macOS)
|
||||
.onAppear {
|
||||
SafeAreaModel.shared.safeArea = proxy.safeAreaInsets
|
||||
}
|
||||
.onChange(of: proxy.safeAreaInsets) { newValue in
|
||||
SafeAreaModel.shared.safeArea = newValue
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#if os(iOS)
|
||||
@@ -88,7 +102,7 @@ struct ContentView: View {
|
||||
}
|
||||
.onOpenURL { url in
|
||||
URLBookmarkModel.shared.saveBookmark(url)
|
||||
OpenURLHandler.shared.handle(url)
|
||||
OpenURLHandler(navigationStyle: navigationStyle).handle(url)
|
||||
}
|
||||
.background(
|
||||
EmptyView().sheet(isPresented: $navigation.presentingAddToPlaylist) {
|
||||
@@ -123,6 +137,9 @@ struct ContentView: View {
|
||||
)
|
||||
#endif
|
||||
.alert(isPresented: $navigation.presentingAlert) { navigation.alert }
|
||||
#if os(iOS)
|
||||
.statusBarHidden(player.playingFullScreen)
|
||||
#endif
|
||||
}
|
||||
|
||||
var navigationStyle: NavigationStyle {
|
||||
@@ -140,9 +157,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import Siesta
|
||||
|
||||
struct OpenURLHandler {
|
||||
static var firstHandle = true
|
||||
static var shared = Self()
|
||||
static let yatteeProtocol = "yattee://"
|
||||
|
||||
var accounts: AccountsModel { .shared }
|
||||
@@ -12,7 +11,7 @@ struct OpenURLHandler {
|
||||
var recents: RecentsModel { .shared }
|
||||
var player: PlayerModel { .shared }
|
||||
var search: SearchModel { .shared }
|
||||
var navigationStyle = NavigationStyle.sidebar
|
||||
var navigationStyle: NavigationStyle
|
||||
|
||||
func handle(_ url: URL) {
|
||||
if accounts.current.isNil {
|
||||
|
||||
@@ -2,15 +2,133 @@ import AVKit
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
#if !os(macOS)
|
||||
final class AppleAVPlayerViewControllerDelegate: NSObject, AVPlayerViewControllerDelegate {
|
||||
#if os(iOS)
|
||||
@Default(.rotateToLandscapeOnEnterFullScreen) private var rotateToLandscapeOnEnterFullScreen
|
||||
@Default(.avPlayerUsesSystemControls) private var avPlayerUsesSystemControls
|
||||
#endif
|
||||
|
||||
var player: PlayerModel { .shared }
|
||||
|
||||
func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_: AVPlayerViewController) -> Bool {
|
||||
false
|
||||
}
|
||||
|
||||
func playerViewController(_: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator) {
|
||||
#if os(iOS)
|
||||
guard rotateToLandscapeOnEnterFullScreen.isRotating else { return }
|
||||
if PlayerModel.shared.currentVideoIsLandscape {
|
||||
let delay = PlayerModel.shared.activeBackend == .appleAVPlayer && avPlayerUsesSystemControls ? 0.8 : 0
|
||||
// not sure why but first rotation call is ignore so doing rotate to same orientation first
|
||||
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)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
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(frame: .zero)
|
||||
}
|
||||
|
||||
func updateUIView(_: UIViewType, context _: Context) {}
|
||||
}
|
||||
#else
|
||||
|
||||
#elseif os(tvOS)
|
||||
struct AppleAVPlayerView: UIViewControllerRepresentable {
|
||||
func makeUIViewController(context _: Context) -> AppleAVPlayerViewController {
|
||||
let controller = AppleAVPlayerViewController()
|
||||
@@ -23,4 +141,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
|
||||
|
||||
@@ -24,7 +24,7 @@ struct OpeningStream: View {
|
||||
|
||||
if let selection = player.streamSelection {
|
||||
if selection.isLocal {
|
||||
return "Opening file..."
|
||||
return "Opening file...".localized()
|
||||
} else {
|
||||
return String(format: "Opening %@ stream...".localized(), selection.shortQuality)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ struct PlayerControls: View {
|
||||
|
||||
#if os(iOS)
|
||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||
@ObservedObject private var safeAreaModel = SafeAreaModel.shared
|
||||
#elseif os(tvOS)
|
||||
enum Field: Hashable {
|
||||
case seekOSD
|
||||
@@ -41,6 +42,8 @@ struct PlayerControls: View {
|
||||
@Default(.playerControlsPlaybackModeEnabled) private var playerControlsPlaybackModeEnabled
|
||||
@Default(.playerControlsMusicModeEnabled) private var playerControlsMusicModeEnabled
|
||||
|
||||
@Default(.avPlayerUsesSystemControls) private var avPlayerUsesSystemControls
|
||||
|
||||
private let controlsOverlayModel = ControlOverlaysModel.shared
|
||||
private var navigation = NavigationModel.shared
|
||||
|
||||
@@ -48,22 +51,28 @@ struct PlayerControls: View {
|
||||
player.playingFullScreen ? fullScreenPlayerControlsLayout : regularPlayerControlsLayout
|
||||
}
|
||||
|
||||
var showControls: Bool {
|
||||
player.activeBackend == .mpv || !avPlayerUsesSystemControls
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
Seek()
|
||||
.zIndex(4)
|
||||
.transition(.opacity)
|
||||
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||||
#if os(tvOS)
|
||||
.focused($focusedField, equals: .seekOSD)
|
||||
.onChange(of: player.seek.lastSeekTime) { _ in
|
||||
if !model.presentingControls {
|
||||
focusedField = .seekOSD
|
||||
if showControls {
|
||||
Seek()
|
||||
.zIndex(4)
|
||||
.transition(.opacity)
|
||||
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||||
#if os(tvOS)
|
||||
.focused($focusedField, equals: .seekOSD)
|
||||
.onChange(of: player.seek.lastSeekTime) { _ in
|
||||
if !model.presentingControls {
|
||||
focusedField = .seekOSD
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
.offset(y: 2)
|
||||
#endif
|
||||
#else
|
||||
.offset(y: 2)
|
||||
#endif
|
||||
}
|
||||
|
||||
VStack {
|
||||
ZStack {
|
||||
@@ -77,106 +86,108 @@ struct PlayerControls: View {
|
||||
}
|
||||
.offset(y: playerControlsLayout.osdVerticalOffset + 5)
|
||||
|
||||
Section {
|
||||
#if !os(tvOS)
|
||||
HStack {
|
||||
seekBackwardButton
|
||||
Spacer()
|
||||
togglePlayButton
|
||||
Spacer()
|
||||
seekForwardButton
|
||||
}
|
||||
.font(.system(size: playerControlsLayout.bigButtonFontSize))
|
||||
#endif
|
||||
|
||||
ZStack(alignment: .bottom) {
|
||||
VStack(spacing: 4) {
|
||||
#if !os(tvOS)
|
||||
buttonsBar
|
||||
|
||||
HStack {
|
||||
if !player.currentVideo.isNil, player.playingFullScreen {
|
||||
Button {
|
||||
withAnimation(Self.animation) {
|
||||
model.presentingDetailsOverlay = true
|
||||
}
|
||||
} label: {
|
||||
ControlsBar(fullScreen: $model.presentingDetailsOverlay, expansionState: .constant(.full), presentingControls: false, detailsTogglePlayer: false, detailsToggleFullScreen: false)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
.frame(maxWidth: 300, alignment: .leading)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
#endif
|
||||
|
||||
Spacer()
|
||||
|
||||
if playerControlsLayout.displaysTitleLine {
|
||||
VStack(alignment: .leading) {
|
||||
Text(player.videoForDisplay?.displayTitle ?? "Not Playing")
|
||||
.shadow(radius: 10)
|
||||
.font(.system(size: playerControlsLayout.titleLineFontSize).bold())
|
||||
.lineLimit(1)
|
||||
|
||||
Text(player.currentVideo?.displayAuthor ?? "")
|
||||
.fontWeight(.semibold)
|
||||
.shadow(radius: 10)
|
||||
.foregroundColor(.init(white: 0.8))
|
||||
.font(.system(size: playerControlsLayout.authorLineFontSize))
|
||||
.lineLimit(1)
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.offset(y: -40)
|
||||
}
|
||||
|
||||
timeline
|
||||
.padding(.bottom, 2)
|
||||
}
|
||||
.zIndex(1)
|
||||
.padding(.top, 2)
|
||||
.transition(.opacity)
|
||||
|
||||
HStack(spacing: playerControlsLayout.buttonsSpacing) {
|
||||
#if os(tvOS)
|
||||
togglePlayButton
|
||||
if showControls {
|
||||
Section {
|
||||
#if !os(tvOS)
|
||||
HStack {
|
||||
seekBackwardButton
|
||||
Spacer()
|
||||
togglePlayButton
|
||||
Spacer()
|
||||
seekForwardButton
|
||||
#endif
|
||||
if playerControlsRestartEnabled {
|
||||
restartVideoButton
|
||||
}
|
||||
if playerControlsAdvanceToNextEnabled {
|
||||
advanceToNextItemButton
|
||||
}
|
||||
Spacer()
|
||||
#if os(tvOS)
|
||||
if playerControlsSettingsEnabled {
|
||||
settingsButton
|
||||
.font(.system(size: playerControlsLayout.bigButtonFontSize))
|
||||
#endif
|
||||
|
||||
ZStack(alignment: .bottom) {
|
||||
VStack(spacing: 4) {
|
||||
#if !os(tvOS)
|
||||
buttonsBar
|
||||
|
||||
HStack {
|
||||
if !player.currentVideo.isNil, player.playingFullScreen {
|
||||
Button {
|
||||
withAnimation(Self.animation) {
|
||||
model.presentingDetailsOverlay = true
|
||||
}
|
||||
} label: {
|
||||
ControlsBar(fullScreen: $model.presentingDetailsOverlay, expansionState: .constant(.full), presentingControls: false, detailsTogglePlayer: false, detailsToggleFullScreen: false)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
.frame(maxWidth: 300, alignment: .leading)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
#endif
|
||||
|
||||
Spacer()
|
||||
|
||||
if playerControlsLayout.displaysTitleLine {
|
||||
VStack(alignment: .leading) {
|
||||
Text(player.videoForDisplay?.displayTitle ?? "Not Playing")
|
||||
.shadow(radius: 10)
|
||||
.font(.system(size: playerControlsLayout.titleLineFontSize).bold())
|
||||
.lineLimit(1)
|
||||
|
||||
Text(player.currentVideo?.displayAuthor ?? "")
|
||||
.fontWeight(.semibold)
|
||||
.shadow(radius: 10)
|
||||
.foregroundColor(.init(white: 0.8))
|
||||
.font(.system(size: playerControlsLayout.authorLineFontSize))
|
||||
.lineLimit(1)
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.offset(y: -40)
|
||||
}
|
||||
#endif
|
||||
if playerControlsPlaybackModeEnabled {
|
||||
playbackModeButton
|
||||
|
||||
timeline
|
||||
.padding(.bottom, 2)
|
||||
}
|
||||
.zIndex(1)
|
||||
.padding(.top, 2)
|
||||
.transition(.opacity)
|
||||
|
||||
HStack(spacing: playerControlsLayout.buttonsSpacing) {
|
||||
#if os(tvOS)
|
||||
togglePlayButton
|
||||
seekBackwardButton
|
||||
seekForwardButton
|
||||
#endif
|
||||
if playerControlsRestartEnabled {
|
||||
restartVideoButton
|
||||
}
|
||||
if playerControlsAdvanceToNextEnabled {
|
||||
advanceToNextItemButton
|
||||
}
|
||||
Spacer()
|
||||
#if os(tvOS)
|
||||
if playerControlsSettingsEnabled {
|
||||
settingsButton
|
||||
}
|
||||
#endif
|
||||
if playerControlsPlaybackModeEnabled {
|
||||
playbackModeButton
|
||||
}
|
||||
#if os(tvOS)
|
||||
closeVideoButton
|
||||
#else
|
||||
if playerControlsMusicModeEnabled {
|
||||
musicModeButton
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.zIndex(0)
|
||||
#if os(tvOS)
|
||||
closeVideoButton
|
||||
.offset(y: -playerControlsLayout.timelineHeight - 30)
|
||||
#else
|
||||
if playerControlsMusicModeEnabled {
|
||||
musicModeButton
|
||||
}
|
||||
.offset(y: -playerControlsLayout.timelineHeight - 5)
|
||||
#endif
|
||||
}
|
||||
.zIndex(0)
|
||||
#if os(tvOS)
|
||||
.offset(y: -playerControlsLayout.timelineHeight - 30)
|
||||
#else
|
||||
.offset(y: -playerControlsLayout.timelineHeight - 5)
|
||||
#endif
|
||||
}
|
||||
.opacity(model.presentingControls && !player.availableStreams.isEmpty ? 1 : 0)
|
||||
}
|
||||
.opacity(model.presentingControls && !player.availableStreams.isEmpty ? 1 : 0)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -229,7 +240,7 @@ struct PlayerControls: View {
|
||||
guard player.playerSize.height.isFinite else { return 200 }
|
||||
var inset = 0.0
|
||||
#if os(iOS)
|
||||
inset = SafeArea.insets.bottom
|
||||
inset = safeAreaModel.safeArea.bottom
|
||||
#endif
|
||||
return [player.playerSize.height - inset, 500].min()!
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import SwiftUI
|
||||
struct TVControls: UIViewRepresentable {
|
||||
var model: PlayerControlsModel!
|
||||
var player: PlayerModel { .shared }
|
||||
var safeArea: SafeAreaModel { .shared }
|
||||
var thumbnails: ThumbnailsModel { .shared }
|
||||
|
||||
@State private var direction = ""
|
||||
@@ -32,10 +33,10 @@ struct TVControls: UIViewRepresentable {
|
||||
|
||||
let controls = UIHostingController(rootView: PlayerControls())
|
||||
controls.view.frame = .init(
|
||||
origin: .init(x: SafeArea.insets.left, y: SafeArea.insets.top),
|
||||
origin: .init(x: safeArea.safeArea.leading, y: safeArea.safeArea.top),
|
||||
size: .init(
|
||||
width: UIScreen.main.bounds.width - SafeArea.horizontalInsets,
|
||||
height: UIScreen.main.bounds.height - SafeArea.verticalInset
|
||||
width: UIScreen.main.bounds.width - safeArea.horizontalInsets,
|
||||
height: UIScreen.main.bounds.height - safeArea.verticalInsets
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct PlayerBackendView: View {
|
||||
#if os(iOS)
|
||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||
@ObservedObject private var safeAreaModel = SafeAreaModel.shared
|
||||
#endif
|
||||
@ObservedObject private var player = PlayerModel.shared
|
||||
|
||||
@Default(.avPlayerUsesSystemControls) private var avPlayerUsesSystemControls
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .top) {
|
||||
Group {
|
||||
@@ -15,13 +19,23 @@ 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)
|
||||
|
||||
ControlsGradientView()
|
||||
.zIndex(1)
|
||||
}
|
||||
}
|
||||
.overlay(GeometryReader { proxy in
|
||||
@@ -30,12 +44,11 @@ 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()
|
||||
if player.activeBackend == .mpv || !avPlayerUsesSystemControls {
|
||||
PlayerGestures()
|
||||
}
|
||||
PlayerControls()
|
||||
#if os(iOS)
|
||||
.padding(.top, controlsTopPadding)
|
||||
@@ -55,19 +68,17 @@ struct PlayerBackendView: View {
|
||||
guard player.playingFullScreen else { return 0 }
|
||||
|
||||
if UIDevice.current.userInterfaceIdiom != .pad {
|
||||
return verticalSizeClass == .compact ? SafeArea.insets.top : 0
|
||||
return verticalSizeClass == .compact ? safeAreaModel.safeArea.top : 0
|
||||
} else {
|
||||
return SafeArea.insets.top.isZero ? SafeArea.insets.bottom : SafeArea.insets.top
|
||||
return safeAreaModel.safeArea.top.isZero ? safeAreaModel.safeArea.bottom : safeAreaModel.safeArea.top
|
||||
}
|
||||
}
|
||||
|
||||
var controlsBottomPadding: Double {
|
||||
guard player.playingFullScreen else { return 0 }
|
||||
|
||||
if UIDevice.current.userInterfaceIdiom != .pad {
|
||||
return player.playingFullScreen && verticalSizeClass == .compact ? SafeArea.insets.bottom : 0
|
||||
return player.playingFullScreen || verticalSizeClass == .compact ? safeAreaModel.safeArea.bottom : 0
|
||||
} else {
|
||||
return player.playingFullScreen ? SafeArea.insets.bottom : 0
|
||||
return player.playingFullScreen ? safeAreaModel.safeArea.bottom : 0
|
||||
}
|
||||
}
|
||||
#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 }
|
||||
@@ -17,7 +17,8 @@ extension VideoPlayerView {
|
||||
}
|
||||
.onChanged { value in
|
||||
guard player.presentingPlayer,
|
||||
!controlsOverlayModel.presenting else { return }
|
||||
!controlsOverlayModel.presenting,
|
||||
dragGestureState else { return }
|
||||
|
||||
if player.controls.presentingControls, !player.musicMode {
|
||||
player.controls.presentingControls = false
|
||||
@@ -36,7 +37,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 +86,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,
|
||||
|
||||
@@ -5,42 +5,50 @@ struct PlayerGestures: View {
|
||||
private var player = PlayerModel.shared
|
||||
@ObservedObject private var model = PlayerControlsModel.shared
|
||||
|
||||
@Default(.avPlayerUsesSystemControls) private var avPlayerUsesSystemControls
|
||||
|
||||
var showGestures: Bool {
|
||||
player.activeBackend == .mpv || !avPlayerUsesSystemControls
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
gestureRectangle
|
||||
.tapRecognizer(
|
||||
tapSensitivity: 0.2,
|
||||
doubleTapAction: {
|
||||
model.presentingControls = false
|
||||
let interval = TimeInterval(Defaults[.gestureBackwardSeekDuration]) ?? 10
|
||||
player.backend.seek(relative: .secondsInDefaultTimescale(-interval), seekType: .userInteracted)
|
||||
},
|
||||
anyTapAction: {
|
||||
singleTapAction()
|
||||
model.update()
|
||||
}
|
||||
)
|
||||
if showGestures {
|
||||
gestureRectangle
|
||||
.tapRecognizer(
|
||||
tapSensitivity: 0.2,
|
||||
doubleTapAction: {
|
||||
model.presentingControls = false
|
||||
let interval = TimeInterval(Defaults[.gestureBackwardSeekDuration]) ?? 10
|
||||
player.backend.seek(relative: .secondsInDefaultTimescale(-interval), seekType: .userInteracted)
|
||||
},
|
||||
anyTapAction: {
|
||||
singleTapAction()
|
||||
model.update()
|
||||
}
|
||||
)
|
||||
|
||||
gestureRectangle
|
||||
.tapRecognizer(
|
||||
tapSensitivity: 0.2,
|
||||
doubleTapAction: {
|
||||
model.presentingControls = false
|
||||
player.backend.togglePlay()
|
||||
},
|
||||
anyTapAction: singleTapAction
|
||||
)
|
||||
gestureRectangle
|
||||
.tapRecognizer(
|
||||
tapSensitivity: 0.2,
|
||||
doubleTapAction: {
|
||||
model.presentingControls = false
|
||||
player.backend.togglePlay()
|
||||
},
|
||||
anyTapAction: singleTapAction
|
||||
)
|
||||
|
||||
gestureRectangle
|
||||
.tapRecognizer(
|
||||
tapSensitivity: 0.2,
|
||||
doubleTapAction: {
|
||||
model.presentingControls = false
|
||||
let interval = TimeInterval(Defaults[.gestureForwardSeekDuration]) ?? 10
|
||||
player.backend.seek(relative: .secondsInDefaultTimescale(interval), seekType: .userInteracted)
|
||||
},
|
||||
anyTapAction: singleTapAction
|
||||
)
|
||||
gestureRectangle
|
||||
.tapRecognizer(
|
||||
tapSensitivity: 0.2,
|
||||
doubleTapAction: {
|
||||
model.presentingControls = false
|
||||
let interval = TimeInterval(Defaults[.gestureForwardSeekDuration]) ?? 10
|
||||
player.backend.seek(relative: .secondsInDefaultTimescale(interval), seekType: .userInteracted)
|
||||
},
|
||||
anyTapAction: singleTapAction
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ struct InspectorView: View {
|
||||
|
||||
videoDetailRow("Format", value: player.mpvBackend.videoFormat)
|
||||
videoDetailRow("Codec", value: player.mpvBackend.videoCodec)
|
||||
videoDetailRow("Hardware Decoder", value: player.mpvBackend.hwDecoder)
|
||||
videoDetailRow("Hardware decoder", value: player.mpvBackend.hwDecoder)
|
||||
videoDetailRow("Driver", value: player.mpvBackend.currentVo)
|
||||
videoDetailRow("Size", value: player.formattedSize)
|
||||
videoDetailRow("FPS", value: player.mpvBackend.formattedOutputFps)
|
||||
|
||||
@@ -91,7 +91,7 @@ struct VideoActions: View {
|
||||
case .share:
|
||||
return video?.isShareable ?? false
|
||||
case .addToPlaylist:
|
||||
return !(video?.isLocal ?? true)
|
||||
return !(video?.isLocal ?? true) && accounts.signedIn
|
||||
case .subscribe:
|
||||
return !(video?.isLocal ?? true) && accounts.signedIn && accounts.app.supportsSubscriptions
|
||||
case .settings:
|
||||
@@ -206,6 +206,7 @@ struct VideoActions: View {
|
||||
.foregroundColor(active ? Color("AppRedColor") : .secondary)
|
||||
.font(.caption2)
|
||||
.allowsTightening(true)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, playerActionsButtonLabelStyle.text ? 6 : 12)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,10 +37,8 @@ struct VideoPlayerView: View {
|
||||
|
||||
#if os(iOS)
|
||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||
|
||||
@State internal var orientation = UIInterfaceOrientation.portrait
|
||||
@State internal var lastOrientation: UIInterfaceOrientation?
|
||||
@State internal var orientationDebouncer = Debouncer(.milliseconds(300))
|
||||
@ObservedObject private var safeAreaModel = SafeAreaModel.shared
|
||||
private var orientationModel = OrientationModel.shared
|
||||
#elseif os(macOS)
|
||||
var hoverThrottle = Throttle(interval: 0.5)
|
||||
var mouseLocation: CGPoint { NSEvent.mouseLocation }
|
||||
@@ -52,7 +50,6 @@ struct VideoPlayerView: View {
|
||||
@State internal var isHorizontalDrag = false
|
||||
@State internal var isVerticalDrag = false
|
||||
@State internal var viewDragOffset = Self.hiddenOffset
|
||||
@State internal var orientationObserver: Any?
|
||||
#endif
|
||||
|
||||
@ObservedObject internal var player = PlayerModel.shared
|
||||
@@ -67,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
|
||||
|
||||
@@ -89,14 +87,6 @@ struct VideoPlayerView: View {
|
||||
.onChange(of: playerSidebar) { _ in
|
||||
updateSidebarQueue()
|
||||
}
|
||||
#if os(macOS)
|
||||
.background(
|
||||
EmptyView().sheet(isPresented: $navigation.presentingChannelSheet) {
|
||||
ChannelVideosView(channel: navigation.channelPresentedInSheet, showCloseButton: true, inNavigationView: false)
|
||||
.frame(minWidth: 1000, minHeight: 700)
|
||||
}
|
||||
)
|
||||
#endif
|
||||
}
|
||||
|
||||
var videoPlayer: some View {
|
||||
@@ -113,69 +103,52 @@ struct VideoPlayerView: View {
|
||||
playerSize = geometry.size
|
||||
}
|
||||
}
|
||||
#if os(iOS)
|
||||
.padding(.bottom, fullScreenPlayer ? 0.0001 : geometry.safeAreaInsets.bottom)
|
||||
#endif
|
||||
.onChange(of: geometry.size) { _ in
|
||||
self.playerSize = geometry.size
|
||||
}
|
||||
.onChange(of: fullScreenDetails) { value in
|
||||
player.backend.setNeedsDrawing(!value)
|
||||
}
|
||||
#if os(iOS)
|
||||
.frame(width: playerWidth.isNil ? nil : Double(playerWidth!), height: playerHeight.isNil ? nil : Double(playerHeight!))
|
||||
.ignoresSafeArea(.all, edges: .bottom)
|
||||
.onChange(of: player.presentingPlayer) { newValue in
|
||||
if newValue {
|
||||
#if os(iOS)
|
||||
.frame(height: playerHeight.isNil ? nil : Double(playerHeight!))
|
||||
#endif
|
||||
.onChange(of: geometry.size) { _ in
|
||||
self.playerSize = geometry.size
|
||||
}
|
||||
.onChange(of: fullScreenDetails) { value in
|
||||
player.backend.setNeedsDrawing(!value)
|
||||
}
|
||||
#if os(iOS)
|
||||
.onChange(of: player.presentingPlayer) { newValue in
|
||||
if newValue {
|
||||
viewDragOffset = 0
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
#if os(macOS)
|
||||
if player.videoForDisplay.isNil {
|
||||
player.hide()
|
||||
}
|
||||
#endif
|
||||
viewDragOffset = 0
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
#if os(macOS)
|
||||
if player.videoForDisplay.isNil {
|
||||
player.hide()
|
||||
}
|
||||
#endif
|
||||
viewDragOffset = 0
|
||||
|
||||
Delay.by(0.2) {
|
||||
configureOrientationUpdatesBasedOnAccelerometer()
|
||||
Delay.by(0.2) {
|
||||
orientationModel.configureOrientationUpdatesBasedOnAccelerometer()
|
||||
|
||||
if let orientationMask = player.lockedOrientation {
|
||||
Orientation.lockOrientation(
|
||||
orientationMask,
|
||||
andRotateTo: orientationMask == .landscapeLeft ? .landscapeLeft : orientationMask == .landscapeRight ? .landscapeRight : .portrait
|
||||
)
|
||||
} else {
|
||||
Orientation.lockOrientation(.allButUpsideDown)
|
||||
if let orientationMask = player.lockedOrientation {
|
||||
Orientation.lockOrientation(
|
||||
orientationMask,
|
||||
andRotateTo: orientationMask == .landscapeLeft ? .landscapeLeft : orientationMask == .landscapeRight ? .landscapeRight : .portrait
|
||||
)
|
||||
} else {
|
||||
Orientation.lockOrientation(.allButUpsideDown)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
if Defaults[.lockPortraitWhenBrowsing] {
|
||||
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
||||
} else {
|
||||
Orientation.lockOrientation(.allButUpsideDown)
|
||||
.onAnimationCompleted(for: viewDragOffset) {
|
||||
guard !dragGestureState else { return }
|
||||
if viewDragOffset == 0 {
|
||||
player.onPresentPlayer.forEach { $0() }
|
||||
player.onPresentPlayer = []
|
||||
} else if viewDragOffset == Self.hiddenOffset {
|
||||
player.hide(animate: false)
|
||||
}
|
||||
}
|
||||
stopOrientationUpdates()
|
||||
player.controls.hideOverlays()
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
|
||||
guard player.lockedOrientation.isNil else {
|
||||
return
|
||||
}
|
||||
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: OrientationTracker.shared.currentInterfaceOrientation)
|
||||
}
|
||||
.onAnimationCompleted(for: viewDragOffset) {
|
||||
guard !dragGestureState else { return }
|
||||
if viewDragOffset == 0 {
|
||||
player.onPresentPlayer.forEach { $0() }
|
||||
player.onPresentPlayer = []
|
||||
} else if viewDragOffset == Self.hiddenOffset {
|
||||
player.hide(animate: false)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#if os(iOS)
|
||||
@@ -189,7 +162,7 @@ struct VideoPlayerView: View {
|
||||
.persistentSystemOverlays(!fullScreenPlayer)
|
||||
#endif
|
||||
#if os(macOS)
|
||||
.frame(minWidth: 1000, minHeight: 700)
|
||||
.frame(minWidth: 1100, minHeight: 700)
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -263,16 +236,21 @@ struct VideoPlayerView: View {
|
||||
}
|
||||
|
||||
var playerOffset: Double {
|
||||
dragGestureState && !isHorizontalDrag ? dragGestureOffset.height : viewDragOffset
|
||||
dragGestureState && !isHorizontalDrag ? dragGestureOffset.height : dragOffset
|
||||
}
|
||||
|
||||
var playerWidth: Double? {
|
||||
fullScreenPlayer ? (UIScreen.main.bounds.size.width - SafeArea.insets.left - SafeArea.insets.right) : nil
|
||||
var dragOffset: Double {
|
||||
if viewDragOffset.isZero || viewDragOffset == Self.hiddenOffset {
|
||||
return viewDragOffset
|
||||
}
|
||||
|
||||
return player.presentingPlayer ? 0 : Self.hiddenOffset
|
||||
}
|
||||
|
||||
var playerHeight: Double? {
|
||||
let lockedPortrait = player.lockedOrientation?.contains(.portrait) ?? false
|
||||
return fullScreenPlayer ? UIScreen.main.bounds.size.height - (OrientationTracker.shared.currentInterfaceOrientation.isPortrait || lockedPortrait ? (SafeArea.insets.top + SafeArea.insets.bottom) : 0) : nil
|
||||
let isPortrait = OrientationTracker.shared.currentInterfaceOrientation.isPortrait || lockedPortrait
|
||||
return fullScreenPlayer ? UIScreen.main.bounds.size.height - (isPortrait ? safeAreaModel.safeArea.top + safeAreaModel.safeArea.bottom : 0) : nil
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -290,22 +268,21 @@ struct VideoPlayerView: View {
|
||||
.ignoresSafeArea()
|
||||
#else
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
player.playerBackendView
|
||||
}
|
||||
.modifier(
|
||||
VideoPlayerSizeModifier(
|
||||
geometry: geometry,
|
||||
aspectRatio: player.aspectRatio,
|
||||
fullScreen: fullScreenPlayer
|
||||
player.playerBackendView
|
||||
|
||||
.modifier(
|
||||
VideoPlayerSizeModifier(
|
||||
geometry: geometry,
|
||||
aspectRatio: player.aspectRatio,
|
||||
fullScreen: fullScreenPlayer,
|
||||
detailsHiddenInFullScreen: detailsHiddenInFullScreen
|
||||
)
|
||||
)
|
||||
)
|
||||
.frame(maxWidth: fullScreenPlayer ? .infinity : nil, maxHeight: fullScreenPlayer ? .infinity : nil)
|
||||
.onHover { hovering in
|
||||
hoveringPlayer = hovering
|
||||
hovering ? player.controls.show() : player.controls.hide()
|
||||
}
|
||||
.gesture(player.controls.presentingOverlays ? nil : playerDragGesture)
|
||||
.onHover { hovering in
|
||||
hoveringPlayer = hovering
|
||||
hovering ? player.controls.show() : player.controls.hide()
|
||||
}
|
||||
.gesture(player.controls.presentingOverlays ? nil : playerDragGesture)
|
||||
#if os(macOS)
|
||||
.onAppear(perform: {
|
||||
NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) {
|
||||
@@ -322,15 +299,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
|
||||
@@ -346,6 +320,9 @@ struct VideoPlayerView: View {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#if os(iOS)
|
||||
.background(BackgroundBlackView().edgesIgnoringSafeArea(.all))
|
||||
#endif
|
||||
.background(((colorScheme == .dark || fullScreenPlayer) ? Color.black : Color.white).edgesIgnoringSafeArea(.all))
|
||||
#if os(macOS)
|
||||
.frame(minWidth: 650)
|
||||
@@ -387,7 +364,7 @@ struct VideoPlayerView: View {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if !fullScreenPlayer {
|
||||
if !detailsHiddenInFullScreen {
|
||||
#if os(iOS)
|
||||
if sidebarQueue {
|
||||
List {
|
||||
@@ -422,6 +399,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(
|
||||
@@ -432,6 +415,20 @@ struct VideoPlayerView: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
var detailsHiddenInFullScreen: Bool {
|
||||
guard fullScreenPlayer else { return false }
|
||||
|
||||
if player.activeBackend == .mpv {
|
||||
return true
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
return !avPlayerUsesSystemControls || verticalSizeClass == .compact
|
||||
#else
|
||||
return !avPlayerUsesSystemControls
|
||||
#endif
|
||||
}
|
||||
|
||||
var fullScreenPlayer: Bool {
|
||||
#if os(iOS)
|
||||
player.playingFullScreen || verticalSizeClass == .compact
|
||||
@@ -497,3 +494,18 @@ struct VideoPlayerView_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
struct BackgroundBlackView: UIViewRepresentable {
|
||||
func makeUIView(context _: Context) -> UIView {
|
||||
let view = UIView()
|
||||
DispatchQueue.main.async {
|
||||
view.superview?.superview?.backgroundColor = .black
|
||||
view.superview?.superview?.layer.removeAllAnimations()
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_: UIView, context _: Context) {}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
struct SafeArea {
|
||||
static var insets: UIEdgeInsets {
|
||||
let keyWindow = scene?.windows.first { $0.isKeyWindow }
|
||||
|
||||
return keyWindow?.safeAreaInsets ?? .init()
|
||||
}
|
||||
|
||||
static var verticalInset: Double {
|
||||
insets.top + insets.bottom
|
||||
}
|
||||
|
||||
static var horizontalInsets: Double {
|
||||
insets.left + insets.right
|
||||
}
|
||||
|
||||
static var scene: UIWindowScene? {
|
||||
UIApplication.shared.connectedScenes
|
||||
.filter { $0.activationState == .foregroundActive }
|
||||
.compactMap { $0 as? UIWindowScene }
|
||||
.first
|
||||
}
|
||||
}
|
||||
@@ -59,9 +59,6 @@ struct SearchSuggestions: View {
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#if os(iOS)
|
||||
.padding(.bottom, 90)
|
||||
#endif
|
||||
#if os(macOS)
|
||||
.buttonStyle(.link)
|
||||
#endif
|
||||
|
||||
@@ -94,8 +94,12 @@ struct AccountForm: View {
|
||||
}
|
||||
|
||||
@ViewBuilder var validationStatus: some View {
|
||||
if !username.isEmpty && !password.isEmpty {
|
||||
Section {
|
||||
Section {
|
||||
if username.isEmpty || password.isEmpty {
|
||||
Text("Enter account credentials to connect...")
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
AccountValidationStatus(
|
||||
app: .constant(instance.app),
|
||||
isValid: $isValid,
|
||||
|
||||
@@ -99,8 +99,12 @@ struct InstanceForm: View {
|
||||
}
|
||||
|
||||
@ViewBuilder var validationStatus: some View {
|
||||
if !url.isEmpty {
|
||||
Section {
|
||||
Section {
|
||||
if url.isEmpty {
|
||||
Text("Enter location address to connect...")
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
AccountValidationStatus(
|
||||
app: $app,
|
||||
isValid: $isValid,
|
||||
|
||||
@@ -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
|
||||
@@ -73,7 +78,7 @@ struct PlayerControlsSettings: View {
|
||||
}
|
||||
#endif
|
||||
|
||||
Section(header: SettingsHeader(text: "Seeking"), footer: seekingGestureSection) {
|
||||
Section(header: SettingsHeader(text: "Seeking".localized()), footer: seekingGestureSection) {
|
||||
systemControlsCommandsPicker
|
||||
|
||||
seekingSection
|
||||
@@ -103,13 +108,13 @@ struct PlayerControlsSettings: View {
|
||||
}
|
||||
|
||||
var controlsButtonsSection: some View {
|
||||
Section(header: SettingsHeader(text: "Controls Buttons")) {
|
||||
Section(header: SettingsHeader(text: "Controls Buttons".localized())) {
|
||||
controlButtonToggles
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder var actionsButtonsSection: some View {
|
||||
Section(header: SettingsHeader(text: "Actions Buttons")) {
|
||||
Section(header: SettingsHeader(text: "Actions Buttons".localized())) {
|
||||
actionButtonToggles
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -206,7 +215,7 @@ struct PlayerControlsSettings: View {
|
||||
|
||||
private func seekingDurationSetting(_ name: String, _ value: Binding<String>) -> some View {
|
||||
HStack {
|
||||
Text(name)
|
||||
Text(name.localized())
|
||||
.frame(minWidth: 140, alignment: .leading)
|
||||
Spacer()
|
||||
|
||||
|
||||
@@ -13,10 +13,12 @@ struct PlayerSettings: View {
|
||||
#endif
|
||||
@Default(.expandVideoDescription) private var expandVideoDescription
|
||||
@Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer
|
||||
@Default(.closeVideoOnEOF) private var closeVideoOnEOF
|
||||
#if os(iOS)
|
||||
@Default(.honorSystemOrientationLock) private var honorSystemOrientationLock
|
||||
@Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape
|
||||
@Default(.rotateToPortraitOnExitFullScreen) private var rotateToPortraitOnExitFullScreen
|
||||
@Default(.rotateToLandscapeOnEnterFullScreen) private var rotateToLandscapeOnEnterFullScreen
|
||||
#endif
|
||||
@Default(.closePiPOnNavigation) private var closePiPOnNavigation
|
||||
@Default(.closePiPOnOpeningPlayer) private var closePiPOnOpeningPlayer
|
||||
@@ -65,6 +67,7 @@ struct PlayerSettings: View {
|
||||
sourcePicker
|
||||
}
|
||||
pauseOnHidingPlayerToggle
|
||||
closeVideoOnEOFToggle
|
||||
#if !os(macOS)
|
||||
pauseOnEnteringBackgroundToogle
|
||||
#endif
|
||||
@@ -118,8 +121,9 @@ struct PlayerSettings: View {
|
||||
if idiom == .pad {
|
||||
enterFullscreenInLandscapeToggle
|
||||
}
|
||||
rotateToPortraitOnExitFullScreenToggle
|
||||
honorSystemOrientationLockToggle
|
||||
rotateToPortraitOnExitFullScreenToggle
|
||||
rotateToLandscapeOnEnterFullScreenPicker
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -190,6 +194,10 @@ struct PlayerSettings: View {
|
||||
Toggle("Pause when player is closed", isOn: $pauseOnHidingPlayer)
|
||||
}
|
||||
|
||||
private var closeVideoOnEOFToggle: some View {
|
||||
Toggle("Close video and player on end", isOn: $closeVideoOnEOF)
|
||||
}
|
||||
|
||||
#if !os(macOS)
|
||||
private var pauseOnEnteringBackgroundToogle: some View {
|
||||
Toggle("Pause when entering background", isOn: $pauseOnEnteringBackground)
|
||||
@@ -209,6 +217,15 @@ struct PlayerSettings: View {
|
||||
private var rotateToPortraitOnExitFullScreenToggle: some View {
|
||||
Toggle("Rotate to portrait when exiting fullscreen", isOn: $rotateToPortraitOnExitFullScreen)
|
||||
}
|
||||
|
||||
private var rotateToLandscapeOnEnterFullScreenPicker: some View {
|
||||
Picker("Rotate when entering fullscreen on landscape video", selection: $rotateToLandscapeOnEnterFullScreen) {
|
||||
Text("Landscape left").tag(FullScreenRotationSetting.landscapeRight)
|
||||
Text("Landscape right").tag(FullScreenRotationSetting.landscapeLeft)
|
||||
Text("No rotation").tag(FullScreenRotationSetting.disabled)
|
||||
}
|
||||
.modifier(SettingsPickerModifier())
|
||||
}
|
||||
#endif
|
||||
|
||||
private var closePiPOnNavigationToggle: some View {
|
||||
|
||||
@@ -245,7 +245,7 @@ struct SettingsView: View {
|
||||
case .browsing:
|
||||
return 880
|
||||
case .player:
|
||||
return 450
|
||||
return 480
|
||||
case .controls:
|
||||
return 920
|
||||
case .quality:
|
||||
|
||||
@@ -12,11 +12,13 @@ struct VerticalCells<Header: View>: View {
|
||||
|
||||
var items = [ContentItem]()
|
||||
var allowEmpty = false
|
||||
var edgesIgnoringSafeArea = Edge.Set.horizontal
|
||||
|
||||
let header: Header?
|
||||
init(items: [ContentItem], allowEmpty: Bool = false, @ViewBuilder header: @escaping () -> Header? = { nil }) {
|
||||
init(items: [ContentItem], allowEmpty: Bool = false, edgesIgnoringSafeArea: Edge.Set = .horizontal, @ViewBuilder header: @escaping () -> Header? = { nil }) {
|
||||
self.items = items
|
||||
self.allowEmpty = allowEmpty
|
||||
self.edgesIgnoringSafeArea = edgesIgnoringSafeArea
|
||||
self.header = header()
|
||||
}
|
||||
|
||||
@@ -40,7 +42,7 @@ struct VerticalCells<Header: View>: View {
|
||||
#endif
|
||||
}
|
||||
.animation(nil)
|
||||
.edgesIgnoringSafeArea(.horizontal)
|
||||
.edgesIgnoringSafeArea(edgesIgnoringSafeArea)
|
||||
#if os(macOS)
|
||||
.background(Color.secondaryBackground)
|
||||
.frame(minWidth: 360)
|
||||
|
||||
@@ -563,3 +563,10 @@
|
||||
"Fullscreen" = "Vollbildmodus";
|
||||
"Lock" = "Sperre";
|
||||
"Description" = "Beschreibung";
|
||||
"Seek" = "Suchen";
|
||||
"Enter account credentials to connect..." = "Geben Sie die Kontozugangsdaten ein, um eine Verbindung herzustellen...";
|
||||
"Show scroll to top button in comments" = "Schaltfläche Nach oben scrollen in Kommentaren anzeigen";
|
||||
"Enter location address to connect..." = "Geben Sie die Internetadresse ein, um eine Verbindung herzustellen...";
|
||||
"Opened File" = "Geöffnete Datei";
|
||||
"File Extension" = "Dateierweiterung";
|
||||
"Opening file..." = "Datei öffnen...";
|
||||
|
||||
@@ -561,3 +561,19 @@
|
||||
"Loop one" = "Loop one";
|
||||
"Autoplay next" = "Autoplay next";
|
||||
"Stream" = "Stream";
|
||||
"Show scroll to top button in comments" = "Show scroll to top button in comments";
|
||||
"Enter account credentials to connect..." = "Enter account credentials to connect...";
|
||||
"Enter location address to connect..." = "Enter location address to connect...";
|
||||
"Seek" = "Seek";
|
||||
"Opened File" = "Opened File";
|
||||
"File Extension" = "File Extension";
|
||||
"Opening file..." = "Opening file...";
|
||||
"Public account" = "Public account";
|
||||
"Your Accounts" = "Your Accounts";
|
||||
"Browse without account" = "Browse without account";
|
||||
"Close video and player on end" = "Close video and player on end";
|
||||
"Use system controls with AVPlayer" = "Use system controls with AVPlayer";
|
||||
"Rotate when entering fullscreen on landscape video" = "Rotate when entering fullscreen on landscape video";
|
||||
"Landscape left" = "Landscape left";
|
||||
"Landscape right" = "Landscape right";
|
||||
"No rotation" = "No rotation";
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
/* Trending category, section containing all kinds of videos */
|
||||
"All" = "Tout";
|
||||
"Are you sure you want to clear history of watched videos?" = "Êtes-vous sûr de vouloir effacer l'historique des vidéos regardées ?";
|
||||
"Are you sure you want to clear history of watched videos?" = "Êtes-vous sûr de vouloir effacer l'historique des vidéos visionnées ?";
|
||||
"Are you sure you want to delete playlist?" = "Êtes-vous sûr de vouloir supprimer la liste de lecture ?";
|
||||
"Are you sure you want to restore default quality profiles?" = "Êtes-vous sûr de vouloir restaurer les profils de qualité par défaut ?";
|
||||
"Are you sure you want to unsubscribe from %@?" = "Êtes-vous sûr de vouloir vous désabonner de %@ ?";
|
||||
@@ -38,7 +38,7 @@
|
||||
"Close Video" = "Fermer la vidéo";
|
||||
"Disabled" = "Désactivé";
|
||||
"Donations" = "Dons";
|
||||
"Fullscreen size" = "Taille du plein écran";
|
||||
"Fullscreen size" = "Taille en plein écran";
|
||||
"Gaming" = "Jeux vidéo";
|
||||
"Help" = "Aide";
|
||||
"Hide sidebar" = "Masquer la barre latérale";
|
||||
@@ -50,17 +50,17 @@
|
||||
/* Video date filter in search */
|
||||
"Hour" = "Heure";
|
||||
"I am lost" = "Je suis perdu";
|
||||
"I found a bug /" = "J'ai trouvé une erreur";
|
||||
"I found a bug /" = "J'ai trouvé une erreur /";
|
||||
"I have a feature request" = "J'ai une demande de fonctionnalité";
|
||||
"I like this app!" = "J'aime bien cette appli !";
|
||||
"I like this app!" = "J'aime cette application !";
|
||||
"I want to ask a question" = "Je veux poser une question";
|
||||
"Info" = "Infos";
|
||||
"Info" = "Informations";
|
||||
|
||||
/* Selected video has just finished playing */
|
||||
"Just watched" = "Regardée à l'instant";
|
||||
"Just watched" = "Visionnée à l'instant";
|
||||
|
||||
/* Player controls layout size */
|
||||
"Large" = "Grand";
|
||||
"Large" = "Grands";
|
||||
"LIVE" = "DIRECT";
|
||||
"Loading..." = "Chargement…";
|
||||
"Locations" = "Instances";
|
||||
@@ -74,7 +74,7 @@
|
||||
"No description" = "Aucune description";
|
||||
"No Playlists" = "Aucune liste de lecture";
|
||||
"No results" = "Aucun résultat";
|
||||
"Normal" = "Normal";
|
||||
"Normal" = "Normale";
|
||||
"Not available" = "Indisponible";
|
||||
"Not Playing" = "Pas en lecture";
|
||||
"Nothing" = "Rien";
|
||||
@@ -87,10 +87,10 @@
|
||||
"Player" = "Lecteur";
|
||||
"Playlist" = "Liste de lecture";
|
||||
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "La liste de lecture « %@ » sera supprimée.\nIl n'est pas possible de revenir en arrière.";
|
||||
"Popular" = "Tendances";
|
||||
"Popular" = "Populaire";
|
||||
"Preferred Formats" = "Formats préférés";
|
||||
"Profiles" = "Profils";
|
||||
"Promoting a product or service that is directly related to the creator themselves. This usually includes merchandise or promotion of monetized platforms." = "La promotion d'un produit ou d'un service qui est directement lié au créateur lui-même. Il s'agit généralement de marchandises ou de la promotion de plateformes monétisées.";
|
||||
"Promoting a product or service that is directly related to the creator themselves. This usually includes merchandise or promotion of monetized platforms." = "Promotion d'un produit ou d'un service directement lié au créateur lui-même. Il s'agit généralement de marchandises ou de la promotion de plateformes monétisées.";
|
||||
"Quality" = "Qualité";
|
||||
"Rate" = "Vitesse";
|
||||
"Search history is empty" = "L'historique de recherche est vide";
|
||||
@@ -111,13 +111,13 @@
|
||||
"Current Location" = "Instance actuelle";
|
||||
"Private" = "Privée";
|
||||
"Playback queue is empty" = "La file d'attente de lecture est vide";
|
||||
"Playing Next" = "Lecture suivante";
|
||||
"Playing Next" = "Lire la suivante";
|
||||
"Visibility" = "Visibilité";
|
||||
"Favorites" = "Favoris";
|
||||
"Clear History" = "Effacer l'historique";
|
||||
"Edit" = "Modifier";
|
||||
"Low quality" = "Qualité basse";
|
||||
"Mark watched videos with" = "Marquer les vidéos regardées avec";
|
||||
"Mark watched videos with" = "Marquer les vidéos visionnées avec";
|
||||
"Button" = "Bouton";
|
||||
"Category" = "Catégorie";
|
||||
"Chapters" = "Chapitres";
|
||||
@@ -131,7 +131,7 @@
|
||||
"Continue from %@" = "Continuer depuis %@";
|
||||
"Close" = "Fermer";
|
||||
"Clear Search History" = "Effacer l'historique de recherche";
|
||||
"Clear Search History..." = "Effacer l'historique des recherches…";
|
||||
"Clear Search History..." = "Effacer l'historique de recherche…";
|
||||
"Clear the queue" = "Effacer la file d'attente";
|
||||
"Connection failed" = "Échec de la connexion";
|
||||
"Comments" = "Commentaires";
|
||||
@@ -154,7 +154,7 @@
|
||||
"Lowest" = "La plus basse";
|
||||
"Filter" = "Filtre";
|
||||
"Find Other" = "En trouver un autre";
|
||||
"Mark as watched" = "Marquer comme regardée";
|
||||
"Mark as watched" = "Marquer comme visionnée";
|
||||
"More info can be found in:" = "Vous trouverez plus d'informations dans :";
|
||||
"Duration" = "Durée";
|
||||
"Edit Playlist" = "Modifier la liste de lecture";
|
||||
@@ -168,13 +168,13 @@
|
||||
|
||||
/* Subscriptions title */
|
||||
"Subscriptions" = "Abonnements";
|
||||
"Mark video as watched after playing" = "Marquer la vidéo comme regardée après la lecture";
|
||||
"Mark video as watched after playing" = "Marquer la vidéo comme visionnée après la lecture";
|
||||
|
||||
/* Player controls layout size */
|
||||
"Medium" = "Moyenne";
|
||||
"Medium" = "Moyens";
|
||||
"Music" = "Musique";
|
||||
"Next" = "Suivant";
|
||||
"Pause when entering background" = "Pause lors de l'entrée en arrière-plan";
|
||||
"Pause when entering background" = "Pause lors du passage en arrière-plan";
|
||||
"Play All" = "Tout lire";
|
||||
"Quality Profile" = "Profil de qualité";
|
||||
"Queue is empty" = "La file d'attente est vide";
|
||||
@@ -184,18 +184,18 @@
|
||||
"Pause" = "Pause";
|
||||
"Picture in Picture" = "Image dans l'image";
|
||||
"Play" = "Lecture";
|
||||
"Play in PiP" = "Lire en image dans l'image";
|
||||
"Play Last" = "Ajouter à la fin de la file d’attente";
|
||||
"Play in PiP" = "Lire en IDI";
|
||||
"Play Last" = "Lire en dernier";
|
||||
"Name" = "Nom";
|
||||
"Password" = "Mot de passe";
|
||||
"Playlists" = "Listes de lecture";
|
||||
"New Playlist" = "Nouvelle liste de lecture";
|
||||
"Pause when player is closed" = "Pause lorsque le lecteur est fermé";
|
||||
"Pause when player is closed" = "Pause lors de la fermeture du lecteur";
|
||||
"Recents" = "Récentes";
|
||||
"Search" = "Recherche";
|
||||
|
||||
/* Selected video was played on given date */
|
||||
"Watched %@" = "Regardée %@";
|
||||
"Watched %@" = "Visionnée %@";
|
||||
"Remove from the queue" = "Retirer de la file d'attente";
|
||||
"Queue" = "File d'attente";
|
||||
"Refresh" = "Actualiser";
|
||||
@@ -206,8 +206,8 @@
|
||||
"Replies" = "Réponses";
|
||||
"Save" = "Enregistrer";
|
||||
"Save history of played videos" = "Enregistrer l'historique des vidéos lues";
|
||||
"Watched" = "Regardée";
|
||||
"Save history of searches, channels and playlists" = "Enregistrer l'historique des recherches, des chaînes et des listes de lecture";
|
||||
"Watched" = "Visionnée";
|
||||
"Save history of searches, channels and playlists" = "Enregistrer l'historique des recherches, chaînes et listes de lecture";
|
||||
"Sections" = "Sections";
|
||||
|
||||
/* Video date filter in search */
|
||||
@@ -220,8 +220,8 @@
|
||||
/* Selected video is being played */
|
||||
"Watching now" = "En lecture";
|
||||
"Unsubscribe" = "Se désabonner";
|
||||
"Upload date" = "Date de mise en ligne";
|
||||
"Thumbnails" = "Aperçus";
|
||||
"Upload date" = "Date de publication";
|
||||
"Thumbnails" = "Miniatures";
|
||||
"URL" = "URL";
|
||||
"Trending" = "Tendances";
|
||||
|
||||
@@ -231,8 +231,8 @@
|
||||
"Subscribe" = "S'abonner";
|
||||
"Sort" = "Trier";
|
||||
"It can be changed later in settings. You can use your own locations too." = "Peut être modifié ultérieurement dans les paramètres. Vous pouvez également utiliser vos propres instances.";
|
||||
"For videos which feature music as the primary content." = "Pour les vidéos qui n'utilisent que la musique comme contenu principal.";
|
||||
"Formats will be selected in order as listed.\nHLS is an adaptive format (resolution setting does not apply)." = "Les formats seront sélectionnés dans l'ordre de la liste.\nL'HLS est un format adaptatif (le paramètre de résolution ne s'applique pas).";
|
||||
"For videos which feature music as the primary content." = "Pour les vidéos dont le contenu principal est la musique.";
|
||||
"Formats will be selected in order as listed.\nHLS is an adaptive format (resolution setting does not apply)." = "Les formats seront sélectionnés dans l'ordre de la liste.\nHLS est un format adaptatif (le paramètre de résolution ne s'applique pas).";
|
||||
"MPV Documentation" = "Documentation de MPV";
|
||||
|
||||
/* SponsorBlock category name */
|
||||
@@ -246,8 +246,8 @@
|
||||
"Browsing" = "Navigation";
|
||||
"Buffering stream..." = "Mise en tampon du flux...";
|
||||
"Bugs and great feature ideas can be sent to the GitHub issues tracker. " = "Les bugs et les idées de fonctionnalités peuvent être envoyés via GitHub. ";
|
||||
"Close PiP and open player when application enters foreground" = "Ferme l'IDI et ouvre le lecteur lorsque l'application passe au premier plan";
|
||||
"Close PiP when player is opened" = "Ferme l'IDI lorsque le lecteur est ouvert";
|
||||
"Close PiP and open player when application enters foreground" = "Fermer l'IDI et ouvrir le lecteur lorsque l'application passe au premier plan";
|
||||
"Close PiP when player is opened" = "Fermer l'IDI lorsque le lecteur est ouvert";
|
||||
"Close PiP when starting playing other video" = "Fermer l'IDI lors de la lecture d'une autre vidéo";
|
||||
"Close player when closing video" = "Fermer le lecteur lors de la fermeture de la vidéo";
|
||||
"Close player when starting PiP" = "Fermer le lecteur au lancement de l'IDI";
|
||||
@@ -264,22 +264,22 @@
|
||||
"Don't use public locations" = "Ne pas utiliser d'instances publiques";
|
||||
"Enable Return YouTube Dislike" = "Activer Return YouTube Dislike";
|
||||
"Enter fullscreen in landscape" = "Entrer en plein écran en mode paysage";
|
||||
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Rappel d'aimer la vidéo, de s'abonner ou d'intéragir avec le créateur sur une plateforme gratuite ou payante.";
|
||||
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Rappels d'aimer la vidéo, de s'abonner ou d'interagir avec le créateur sur une plateforme gratuite ou payante.";
|
||||
"Frontend URL" = "URL frontale";
|
||||
"Public Locations" = "Instances publiques";
|
||||
"Public Manifest" = "Manifeste publique";
|
||||
"Honor orientation lock" = "Respecter le verrouillage de l'orientation";
|
||||
"If you are interested what's coming in future updates, you can track project Milestones." = "Si vous êtes intéressé par ce qui est prévu dans les futures mises à jour, vous pouvez suivre les étapes du projet.";
|
||||
"If you are reporting a bug, include all relevant details (especially: app version, used device and system version, steps to reproduce)." = "Si vous signalez un bug, incluez tous les détails pertinents (notamment : la version de l'application, la version de l'appareil et du système utilisés, les étapes pour reproduire celui-ci).";
|
||||
"If you are reporting a bug, include all relevant details (especially: app version, used device and system version, steps to reproduce)." = "Si vous signalez un bug, incluez tous les détails pertinents (notamment : version de l'application, appareil utilisé et version du système, étapes à suivre pour reproduire celui-ci).";
|
||||
"Instance of current account" = "Instance du compte actif";
|
||||
|
||||
/* SponsorBlock category name */
|
||||
"Interaction" = "Rappel d’interaction (abonnement)";
|
||||
"Interaction" = "Rappel d’interaction";
|
||||
"Interface" = "Interface";
|
||||
|
||||
/* SponsorBlock category name */
|
||||
"Intro" = "Entracte/Animation d'intro";
|
||||
"Large layout is not suitable for all devices and using it may cause controls not to fit on the screen." = "Le format large pour la disposition des boutons n'est pas adapté à tous les appareils et son utilisation peut empêcher les contrôles de s'afficher à l'écran.";
|
||||
"Intro" = "Introduction";
|
||||
"Large layout is not suitable for all devices and using it may cause controls not to fit on the screen." = "Le grand format n'est pas adapté à tous les appareils et son utilisation peut empêcher certains contrôles de s'afficher à l'écran.";
|
||||
|
||||
/* Loading stream OSD */
|
||||
"Loading streams..." = "Chargement des flux...";
|
||||
@@ -291,7 +291,7 @@
|
||||
|
||||
/* SponsorBlock category name */
|
||||
"Outro" = "Générique de fin";
|
||||
"Part of a video promoting a product or service not directly related to the creator. The creator will receive payment or compensation in the form of money or free products." = "Partie de la vidéo promotant un produit ou service qui n'est pas directement relié au créateur. le créateur recevra un paiement ou une compensation sous forme de monnaie ou de produits gratuits.";
|
||||
"Part of a video promoting a product or service not directly related to the creator. The creator will receive payment or compensation in the form of money or free products." = "Partie d'une vidéo promouvant un produit ou un service sans lien direct avec le créateur. Le créateur recevra un paiement ou des produits gratuits.";
|
||||
"Reset search filters" = "Réinitialiser les filtres de recherche";
|
||||
"Resolution" = "Résolution";
|
||||
"Restart" = "Redémarrer";
|
||||
@@ -304,11 +304,11 @@
|
||||
/* SponsorBlock category name */
|
||||
"Sponsor" = "Sponsor";
|
||||
"SponsorBlock" = "SponsorBlock";
|
||||
"SponsorBlock API Instance" = "SponsorBlock exemple d'API";
|
||||
"SponsorBlock API Instance" = "Instance de l'API SponsorBlock";
|
||||
"This cannot be reverted" = "Vous ne pourrez pas revenir en arrière";
|
||||
"Switch to public locations" = "Passer à une instance publique";
|
||||
"Switch to public locations" = "Passer à des instances publiques";
|
||||
"System controls buttons" = "Boutons de contrôle système";
|
||||
"System controls show buttons for %@" = "Montrer les boutons du contrôle système pour %@";
|
||||
"System controls show buttons for %@" = "Montrer les boutons de contrôle système pour %@";
|
||||
"This cannot be reverted. You might need to switch between views or restart the app to see changes." = "Vous ne pourrez pas revenir en arrière. Vous devrez changer de vue ou redémarrer l'application pour voir les changements.";
|
||||
"unknown" = "inconnu";
|
||||
"This information will be processed only on your device and used to connect you to the server in the specified country." = "Ces informations sont traitées uniquement sur votre appareil et utilisées pour vous connecter au serveur dans un pays spécifique.";
|
||||
@@ -317,42 +317,42 @@
|
||||
"Reset watched status when playing again" = "Réinitialiser le status de visionnage lors d'une nouvelle lecture";
|
||||
"Share %@ link" = "Partager le lien %@";
|
||||
"Show anonymous accounts" = "Afficher les comptes anonymes";
|
||||
"Show channel name" = "Afficher le nom de la chaine";
|
||||
"Show channel name" = "Afficher le nom de la chaîne";
|
||||
"Show history" = "Afficher l'historique";
|
||||
"Show progress of watching on thumbnails" = "Afficher la progression de l'avancement dans les miniatures";
|
||||
"Show progress of watching on thumbnails" = "Afficher la progression du visionnage sur les miniatures";
|
||||
"Switch to other public location" = "Passer à une autre instance publique";
|
||||
"Show keywords" = "Afficher le clavier";
|
||||
"Show keywords" = "Afficher les mots-clés";
|
||||
"Show playback statistics" = "Afficher les statistiques de lecture";
|
||||
"Sidebar" = "Barre du côté";
|
||||
"Sidebar" = "Barre latérale";
|
||||
"Sign In Required" = "Authentification requise";
|
||||
|
||||
/* Player controls layout size */
|
||||
"Small" = "Petit";
|
||||
"Small" = "Petits";
|
||||
|
||||
/* Player controls layout size */
|
||||
"Smaller" = "Le plus petit";
|
||||
"Smaller" = "Très petits";
|
||||
"Source" = "Source";
|
||||
"Used to create links from videos, channels and playlists" = "Utilisé pour créer des liens depuis les vidéos, les chaînes et playlists";
|
||||
"Used to create links from videos, channels and playlists" = "Utilisé pour créer des liens depuis les vidéos, chaînes et playlists";
|
||||
"Restart/Play next" = "Redémarrer/Lire la suivante";
|
||||
"Rotate to portrait when exiting fullscreen" = "Revenir au mode portrait quand le mode plein écran est quitté";
|
||||
"Rotate to portrait when exiting fullscreen" = "Rotation en mode portrait à la sortie du plein écran";
|
||||
"Round corners" = "Coins arrondis";
|
||||
"Seek with horizontal swipe on video" = "Rechercher avec un glissement horizontal sur la vidéo";
|
||||
"Settings" = "Paramètres";
|
||||
"Share %@ link with time" = "Partager le lien %@ avec le temps";
|
||||
"Show account username" = "Afficher le nom d'utilisateur";
|
||||
"Show sidebar when space permits" = "Afficher la barre du côté quand l'espace le permet";
|
||||
"Show sidebar when space permits" = "Afficher la barre latérale lorsque l'espace le permet";
|
||||
"Show video length" = "Afficher la durée de la vidéo";
|
||||
"Shuffle" = "Aléatoire";
|
||||
"Shuffle All" = "Tout en aléatoire";
|
||||
"Could not refresh Subscriptions" = "Impossible de rafraichir les abonnements";
|
||||
"Could not refresh Subscriptions" = "Impossible d'actualiser Abonnements";
|
||||
"Increase rate" = "Augmenter la vitesse";
|
||||
"Issues Tracker" = "Suivi de problèmes";
|
||||
"You need to select an account\nto access %@ section" = "Vous avez besoin de sélectionner un compte\npour accéder à la section %@";
|
||||
"You can use automatic profile selection based on current device status or switch it in video playback settings controls." = "Vous pouvez utiliser une sélection automatique de profil basé sur le statut actuel de l'appareil ou le changer dans les paramètre de lecture vidéo.";
|
||||
"You have no playlists\n\nTap on \"New Playlist\" to create one" = "Vous n'avez pas de liste de lecture\n\nTaper sur \"nouvelle liste de lecture\" pour en créer une";
|
||||
"You need to create an instance and accounts\nto access %@ section" = "Vous avez besoin de créer une instance et un compte\npour accéder à la section %@";
|
||||
"You need to select an account\nto access %@ section" = "Vous devez sélectionner un compte\npour accéder à la section %@";
|
||||
"You can use automatic profile selection based on current device status or switch it in video playback settings controls." = "Vous pouvez utiliser la sélection automatique du profil en fonction de l'état actuel de l'appareil ou la modifier dans les commandes des paramètres de lecture vidéo.";
|
||||
"You have no playlists\n\nTap on \"New Playlist\" to create one" = "Vous n'avez aucune liste de lecture\n\nAppuyez sur \"Nouvelle liste de lecture\" pour en créer une";
|
||||
"You need to create an instance and accounts\nto access %@ section" = "Vous devez créer une instance et un compte\npour accéder à la section %@";
|
||||
"Press and hold remote button to open captions and quality menus" = "Appuyez et maintenez le bouton télécommande pour ouvrir le menu des sous-titres et de la qualité vidéo";
|
||||
"Keep last played video in the queue after restart" = "Garder la dernière vidéo jouée en file d'attente après le redémarrage";
|
||||
"Keep last played video in the queue after restart" = "Conserver la dernière vidéo lue dans la file d'attente après le redémarrage";
|
||||
"Proxy videos" = "Proxy vidéos";
|
||||
|
||||
/* Video sort order in search */
|
||||
@@ -360,39 +360,39 @@
|
||||
|
||||
/* Video duration filter in search */
|
||||
"Short" = "Courtes";
|
||||
"This will remove all your custom profiles and return their default values. This cannot be reverted." = "Ceci va supprimer tous vos profils modifiés et les remettre à leur valeurs par défaut. Vous ne pourrez pas revenir en arrière.";
|
||||
"This will remove all your custom profiles and return their default values. This cannot be reverted." = "Cette opération supprimera tous vos profils personnalisés et rétablira leurs valeurs par défaut. Il n'est pas possible de revenir en arrière.";
|
||||
|
||||
/* Player controls layout size */
|
||||
"Very Large" = "Très Large";
|
||||
"Stream & Player" = "Flux et Lecteur";
|
||||
"Very Large" = "Très grands";
|
||||
"Stream & Player" = "Flux & Lecteur";
|
||||
"Statistics" = "Statistiques";
|
||||
"%@ formats" = "%@ formats";
|
||||
"Open logs in Finder" = "Ouvrir les lots dans le Finder";
|
||||
"Open logs in Finder" = "Ouvrir les logs dans le Finder";
|
||||
"Share Logs..." = "Partager les logs…";
|
||||
"Could not load streams" = "Impossible de charger les flux";
|
||||
"Could not open video" = "Impossible d'ouvrir la vidéo";
|
||||
"Channel could not be found" = "Chaîne impossible à trouver";
|
||||
"Channel could not be found" = "Impossible de trouver la chaîne";
|
||||
"Could not extract channel information" = "Impossible d'extraire les informations de la chaîne";
|
||||
"Could not extract SID from received cookies: %@" = "Impossible d'extraire le SID depuis les cookies reçus : %@";
|
||||
"Could not update your token." = "Impossible de mettre à jour votre jeton.";
|
||||
"Could not refresh Trending" = "Impossible de rafraîchir les tendances";
|
||||
"For custom locations you can configure Frontend URL in Locations settings" = "Pour des instances personnalisées vous pouvez configurer l'URL frontale dans les paramètres d'instance";
|
||||
"This URL could not be opened" = "Cette URL ne peut pas être ouverte";
|
||||
"Could not open channel" = "Cette chaîne ne peut pas être ouverte";
|
||||
"Could not refresh Popular" = "Impossible d'actualiser les tendances";
|
||||
"Could not refresh Trending" = "Impossible d'actualiser Tendances";
|
||||
"For custom locations you can configure Frontend URL in Locations settings" = "Pour des instances personnalisées, vous pouvez configurer l'URL frontale dans les paramètres d'instance";
|
||||
"This URL could not be opened" = "Impossible d'ouvrir cette URL";
|
||||
"Could not open channel" = "Impossible d'ouvrir cette chaîne";
|
||||
"Could not refresh Popular" = "Impossible d'actualiser Populaire";
|
||||
"Could not create share link" = "Impossible de créer un lien de partage";
|
||||
"Could not load video" = "Impossible de charger la vidéo";
|
||||
"If you want this app to be available in your language, join translation project." = "Si vous voulez que cette application soit disponible dans votre langue, rejoignez le projet de traduction.";
|
||||
"Translations" = "Traductions";
|
||||
"Decrease rate" = "Diminuer la vitesse";
|
||||
"Finding something to play..." = "Trouver quelque chose à jouer…";
|
||||
"That's nice to hear. It is fun to deliver apps other people want to use. You can consider donating to the project or help by contributing to new features development." = "Cela fait plaisir à entendre. C'est amusant de délivrer des apps que d'autres personnes veulent utiliser. Vous pouvez considérer faire un don au projet ou aider en contribuant au développement de nouvelles fonctionnalités.";
|
||||
"Typically near or at the end of the video when the credits pop up and/or endcards are shown." = "Typiquement proche de la fin de la vidéo, quand les crédits ou les cartes sont montré.";
|
||||
"When partially watched video is played" = "Quand une vidéo partiellement regardée est jouée";
|
||||
"That's nice to hear. It is fun to deliver apps other people want to use. You can consider donating to the project or help by contributing to new features development." = "Cela fait plaisir à entendre. C'est amusant de délivrer des applications que d'autres personnes veulent utiliser. Vous pouvez considérer faire un don au projet ou aider en contribuant au développement de nouvelles fonctionnalités.";
|
||||
"Typically near or at the end of the video when the credits pop up and/or endcards are shown." = "Généralement à la fin de la vidéo, lorsque le générique apparaît ou les images de fin sont affichées.";
|
||||
"When partially watched video is played" = "Lorsqu'une vidéo partiellement visionnée est lue";
|
||||
"Wi-Fi" = "Wi-Fi";
|
||||
"Wiki" = "Wiki";
|
||||
"Yattee" = "Yattee";
|
||||
"Yattee %@ (build %@)" = "Yattee %@ (version %@)";
|
||||
"Yattee %@ (build %@)" = "Yattee %@ (build %@)";
|
||||
"You can switch between profiles in playback settings controls." = "Vous pouvez changer de profil dans les paramètres de lecture.";
|
||||
"Make default" = "Mettre par défaut";
|
||||
"Hardware decoder" = "Décodeur matériel";
|
||||
@@ -400,68 +400,68 @@
|
||||
"Cached time" = "Temps mis en cache";
|
||||
"Dropped frames" = "Images perdus";
|
||||
"Any format" = "Tout formats";
|
||||
"Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "Liste de lecture vide\n\nAppuyez et maintenez sur une vidéo et faite\n\"Ajouter à la liste de lecture\"";
|
||||
"Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "La liste de lecture est vide\n\nAppuyez longuement sur une vidéo, puis sur\n\"Ajouter à la liste de lecture\"";
|
||||
"Comments are disabled" = "Les commentaires sont désactivés";
|
||||
"No comments" = "Aucun commentaire";
|
||||
"No chapters information available" = "Aucune information de chapitrage n'est disponible";
|
||||
"Could not open playlist" = "Impossible d'ouvrir la liste de lecture";
|
||||
"Could not extract video ID" = "Impossible d'extraire l'ID de la vidéo";
|
||||
"This video could not be opened" = "Cette vidéo ne peut pas être ouverte";
|
||||
"This video could not be opened" = "Impossible d'ouvrir cette vidéo";
|
||||
"Could not extract playlist ID" = "Impossible d'extraire l'ID de la liste de lecture";
|
||||
"No locations available at the moment" = "Aucune instance disponible pour le moment";
|
||||
"Could not refresh Playlists" = "Impossible de rafraîchir les listes de lectures";
|
||||
"Could not refresh Playlists" = "Impossible d'actualiser Listes de lectures";
|
||||
|
||||
/* Video date filter in search
|
||||
Video duration filter in search */
|
||||
"Any" = "Tous";
|
||||
"Any" = "Toutes";
|
||||
"Autoplaying Next" = "Lecture automatique de la suivante";
|
||||
"Backend" = "Interne";
|
||||
"You can find information about using Yattee in the Wiki pages." = "Vous pouvez trouver des informations expliquant comment utiliser Yattee sur les pages wiki.";
|
||||
"You have no Playlists" = "Vous n’avez pas de listes de lecture";
|
||||
"Add Channels, Playlists and Searches to Favorites using" = "Ajoutez des chaînes, des listes de lecture ou des recherches à vos Favoris en utilisant";
|
||||
"Rate & Captions" = "Notation et Légendes";
|
||||
"Backend" = "Lecteur";
|
||||
"You can find information about using Yattee in the Wiki pages." = "Vous trouverez des informations sur l'utilisation de Yattee dans les pages Wiki.";
|
||||
"You have no Playlists" = "Vous n’avez aucune liste de lecture";
|
||||
"Add Channels, Playlists and Searches to Favorites using" = "Ajoutez des chaînes, listes de lecture et recherches à vos Favoris en utilisant";
|
||||
"Rate & Captions" = "Vitesse & Sous-titres";
|
||||
"Matrix Chat" = "Discussion Matrix";
|
||||
"Milestones" = "Étapes";
|
||||
|
||||
/* Loading stream OSD */
|
||||
"Opening %@ stream..." = "Ouverture du flux %@…";
|
||||
"Regular size" = "Taille normale";
|
||||
"Regular Size" = "Taille Normale";
|
||||
"Regular Size" = "Taille normale";
|
||||
"Related" = "En relation";
|
||||
"Seek gesture sensitivity" = "Sensibilité du geste de recherche";
|
||||
"Seek gesture speed" = "Vitesse du geste de recherche";
|
||||
"Segments typically found at the start of a video that include an animation, still frame or clip which are also seen in other videos by the same creator." = "Portion que l'on trouve typiquement en début de vidéo incluant une animation, un cadre fixe ou un clip qui est aussi vu dans les autres vidéos du créateur.";
|
||||
"Segments typically found at the start of a video that include an animation, still frame or clip which are also seen in other videos by the same creator." = "Segments généralement situés au début d'une vidéo et comprenant une animation, une image fixe ou un clip que l'on retrouve également dans d'autres vidéos du même créateur.";
|
||||
"No documents" = "Aucun document";
|
||||
"Recent Documents" = "Documents récents";
|
||||
"Show Inspector" = "Afficher l’inspecteur";
|
||||
"Files" = "Fichiers";
|
||||
"Buttons labels" = "Étiquettes des boutons";
|
||||
"Buttons labels" = "Libellés des boutons";
|
||||
"Open" = "Ouvrir";
|
||||
"Only for local files and URLs" = "Uniquement pour les fichiers locaux et les URLs";
|
||||
"Share" = "Partager";
|
||||
"Enter links to open, one per line" = "Entrer les liens à ouvrir, un par ligne";
|
||||
"Enter links to open, one per line" = "Entrez les liens à ouvrir, un par ligne";
|
||||
"Always" = "Toujours";
|
||||
"Channels" = "Chaînes";
|
||||
"Open Files" = "Ouvrir les fichiers";
|
||||
"Open Files" = "Ouvrir Fichiers";
|
||||
"Recent History" = "Historique récent";
|
||||
"Show Favorites" = "Afficher les favoris";
|
||||
"Show Home" = "Afficher l'accueil";
|
||||
"Home" = "Accueil";
|
||||
"Pages toolbar position" = "Position de la barre d’outils sur les pages";
|
||||
"Pages toolbar position" = "Position de la barre d’outils des pages";
|
||||
"Show Documents" = "Afficher les documents";
|
||||
"URL to Open" = "URL à ouvrir";
|
||||
"Enter link to open" = "Entrer le lien à ouvrir";
|
||||
"Video Details" = "Details des vidéos";
|
||||
"Enter link to open" = "Entrez le lien à ouvrir";
|
||||
"Video Details" = "Informations sur la vidéo";
|
||||
"Reload manifest" = "Recharger le manifest";
|
||||
"Clear Queue before opening" = "Effacer la file d’attente avant d’ouvrir";
|
||||
"Edit Favorites…" = "Éditer les favoris…";
|
||||
"Edit Favorites…" = "Modifier les favoris…";
|
||||
"Pages buttons" = "Boutons des pages";
|
||||
"Could not open Files" = "Impossible d’ouvrir les fichiers";
|
||||
"Paste" = "Coller";
|
||||
"Open Videos" = "Ouvrir les vidéos";
|
||||
"Open Videos" = "Ouvrir des vidéos";
|
||||
"Playback Mode" = "Mode de lecture";
|
||||
"Add" = "Ajouter";
|
||||
"Hide" = "Cacher";
|
||||
"Hide" = "Masquer";
|
||||
"Size" = "Taille";
|
||||
"Address" = "Adresse";
|
||||
"Could not delete document" = "Impossible de supprimer le document";
|
||||
@@ -478,7 +478,7 @@
|
||||
"Open Video" = "Ouvrir la vidéo";
|
||||
"Video actions buttons" = "Boutons d'action vidéo";
|
||||
"Documents" = "Documents";
|
||||
"Show Open Videos toolbar button" = "Afficher le bouton de la barre d'outils Ouvrir les vidéos";
|
||||
"Show Open Videos toolbar button" = "Afficher le bouton de la barre d'outils \"Ouvrir des vidéos\"";
|
||||
"File" = "Fichier";
|
||||
"Are you sure you want to remove this document?" = "Êtes-vous sûr de vouloir supprimer ce document ?";
|
||||
"Left" = "Gauche";
|
||||
@@ -487,10 +487,10 @@
|
||||
"Verified" = "Vérifiée";
|
||||
"Show sidebar" = "Afficher la barre latérale";
|
||||
"Driver" = "Driver";
|
||||
"Show Open Videos quick actions" = "Actions rapides Afficher les vidéos ouvertes";
|
||||
"Show Open Videos quick actions" = "Afficher les actions rapides \"Ouvrir des vidéos\"";
|
||||
"Format" = "Format";
|
||||
"Are you sure you want to remove %@ location?" = "Êtes-vous sûr de vouloir supprimer l'instance %@ ?";
|
||||
"Show only icons" = "Afficher seulement les icônes";
|
||||
"Show only icons" = "Afficher uniquement les icônes";
|
||||
"Audio" = "Audio";
|
||||
"Share files from Finder on a Mac\nor iTunes on Windows" = "Partager des fichiers depuis Finder sur Mac\nou iTunes sur Windows";
|
||||
"Channel" = "Chaîne";
|
||||
@@ -507,11 +507,11 @@
|
||||
"Close video" = "Fermer la vidéo";
|
||||
"Show cache status" = "Montrer l'état du cache";
|
||||
"Cache" = "Cache";
|
||||
"Maximum feed items" = "Nombres d'éléments maximum du flux";
|
||||
"Open channels with description expanded" = "Ouvrir les chaînes avec la description complète";
|
||||
"Are you sure you want to clear cache?" = "Voulez-vous vraiment vider le cache ?";
|
||||
"Maximum feed items" = "Nombre maximal d'éléments du flux";
|
||||
"Open channels with description expanded" = "Ouvrir les chaînes avec la description déployée";
|
||||
"Are you sure you want to clear cache?" = "Êtes-vous sûr de vouloir vider le cache ?";
|
||||
"Right click channel thumbnail to open context menu with more actions" = "Effectuez un clic droit sur la miniature d'une chaîne pour ouvrir un menu contextuel avec plus d'actions";
|
||||
"Play next item" = "Jouer le prochain élément";
|
||||
"Play next item" = "Lire l'élément suivant";
|
||||
"Lock orientation" = "Verrouiller l'orientation";
|
||||
"Subscribe/Unsubscribe" = "S'abonner/Se désabonner";
|
||||
"Total size: %@" = "Taille totale : %@";
|
||||
@@ -520,22 +520,22 @@
|
||||
"Feed" = "Flux";
|
||||
"Music Mode" = "Mode Musique";
|
||||
"Gesture: fowards" = "Geste : en avant";
|
||||
"Short videos: visible" = "Shorts : visible";
|
||||
"Mark channel feed as watched" = "Marquer le flux de chaînes comme regardé";
|
||||
"Short videos: hidden" = "Shorts : caché";
|
||||
"Mark channel feed as unwatched" = "Marquer le flux de chaînes comme non regardé";
|
||||
"Short videos: visible" = "Shorts : visibles";
|
||||
"Mark channel feed as watched" = "Marquer le flux de chaîne comme visionné";
|
||||
"Short videos: hidden" = "Shorts : masqués";
|
||||
"Mark channel feed as unwatched" = "Marquer le flux de chaîne comme non visionné";
|
||||
"Play all unwatched" = "Lire toutes les vidéos non visionnées";
|
||||
"Player Bar" = "Barre de lecture";
|
||||
"Double tap gesture" = "Geste de double pression";
|
||||
"Always show controls buttons" = "Toujours afficher les boutons de contrôle";
|
||||
"Always show controls buttons" = "Toujours afficher les boutons de commandes";
|
||||
"Tap and hold channel thumbnail to open context menu with more actions" = "Touchez et maintenez la miniature de la chaîne pour ouvrir le menu contextuel avec plus d'actions";
|
||||
"Gesture settings control skipping interval for double tap gesture on left/right side of the player. Changing system controls settings requires restart." = "Les paramètres des gestes contrôlent l'intervalle de saut pour les gestes de double pression sur le côté gauche/droit du lecteur. La modification des paramètres de contrôle du système nécessite un redémarrage.";
|
||||
"Gesture settings control skipping interval for double click on left/right side of the player. Changing system controls settings requires restart." = "Les paramètres des gestes contrôlent l'intervalle de saut pour le double clic sur le côté gauche/droit du lecteur. La modification des paramètres de contrôle du système nécessite un redémarrage.";
|
||||
"Gesture settings control skipping interval for remote arrow buttons (for 2nd generation Siri Remote or newer). Changing system controls settings requires restart." = "Les paramètres des gestes contrôlent l'intervalle de saut pour les boutons fléchés de la télécommande (pour la télécommande Siri de 2e génération ou plus récente). La modification des paramètres de contrôle du système nécessite un redémarrage.";
|
||||
"Open expanded" = "Ouvrir en grand";
|
||||
"Gesture settings control skipping interval for double tap gesture on left/right side of the player. Changing system controls settings requires restart." = "Les paramètres gestuels contrôlent l'intervalle de saut pour le geste de double pression sur le côté gauche/droit du lecteur. La modification des paramètres de commandes du système nécessite un redémarrage.";
|
||||
"Gesture settings control skipping interval for double click on left/right side of the player. Changing system controls settings requires restart." = "Les paramètres gestuels contrôlent l'intervalle de saut pour le double clic sur le côté gauche/droit du lecteur. La modification des paramètres de commandes du système nécessite un redémarrage.";
|
||||
"Gesture settings control skipping interval for remote arrow buttons (for 2nd generation Siri Remote or newer). Changing system controls settings requires restart." = "Les paramètres gestuels contrôlent l'intervalle de saut pour les boutons fléchés de la télécommande (pour la télécommande Siri de 2e génération ou plus récente). La modification des paramètres de commandes du système nécessite un redémarrage.";
|
||||
"Open expanded" = "Ouvrir déployée";
|
||||
"Single tap gesture" = "Geste de simple pression";
|
||||
"Maximum width expanded" = "Largeur maximale étendue";
|
||||
"Show unwatched feed badges" = "Afficher les badges de flux non regardés";
|
||||
"Maximum width expanded" = "Largeur maximale déployée";
|
||||
"Show unwatched feed badges" = "Afficher les badges de flux non visionnés";
|
||||
"Seeking" = "Recherche";
|
||||
"Controls Buttons" = "Boutons de contrôle";
|
||||
"System controls" = "Contrôles système";
|
||||
@@ -545,12 +545,12 @@
|
||||
"Actions Buttons" = "Boutons d’action";
|
||||
"List" = "Liste";
|
||||
"Cells" = "Tuiles";
|
||||
"Show Next in Queue" = "Afficher la suivante dans la file";
|
||||
"Next in Queue" = "Suivante dans la file";
|
||||
"Show Next in Queue" = "Afficher \"Suivantes dans la file\"";
|
||||
"Next in Queue" = "Suivantes dans la file";
|
||||
"Do nothing" = "Ne rien faire";
|
||||
"Open video description expanded" = "Ouvrir la description de la vidéo";
|
||||
"Mark all as unwatched" = "Marquer toutes comme non regardées";
|
||||
"Mark all as watched" = "Marquer toutes comme regardées";
|
||||
"Open video description expanded" = "Ouvrir la description de la vidéo déployée";
|
||||
"Mark all as unwatched" = "Marquer toutes comme non visionnées";
|
||||
"Mark all as watched" = "Marquer toutes comme visionnées";
|
||||
"Queue - shuffled" = "File d'attente - mélangée";
|
||||
"Replay" = "Replay";
|
||||
"Description" = "Description";
|
||||
@@ -558,8 +558,15 @@
|
||||
"Playback Settings" = "Paramètres de lecture";
|
||||
"Autoplay next" = "Lecture automatique";
|
||||
"Stream" = "Flux";
|
||||
"Show toggle watch status button" = "Afficher le bouton d'état de visionnage";
|
||||
"Show toggle watch status button" = "Afficher le bouton de changement d'état de visionnage";
|
||||
"Toggle size" = "Taille de bouton";
|
||||
"Toggle player" = "Activer le lecteur";
|
||||
"Fullscreen" = "Plein écran";
|
||||
"Lock" = "Verrouiller";
|
||||
"Enter account credentials to connect..." = "Entrez les informations d'identification du compte pour vous connecter...";
|
||||
"Seek" = "Recherche";
|
||||
"Show scroll to top button in comments" = "Afficher le bouton de retour en haut de la page dans les commentaires";
|
||||
"Opened File" = "Fichier ouvert";
|
||||
"Opening file..." = "Ouverture du fichier...";
|
||||
"Enter location address to connect..." = "Entrez l'adresse de l'instance pour se connecter...";
|
||||
"File Extension" = "Extension de fichier";
|
||||
|
||||
@@ -564,3 +564,19 @@
|
||||
"Autoplay next" = "Odtwarzaj automatycznie";
|
||||
"Stream" = "Strumień";
|
||||
"Show Next in Queue" = "Pokaż Następne w kolejce";
|
||||
"Show scroll to top button in comments" = "Pokaż przycisk przewijania do góry w komentarzach";
|
||||
"Seek" = "Przewijanie";
|
||||
"Enter account credentials to connect..." = "Wprowadź dane logowania do konta, aby połączyć...";
|
||||
"Enter location address to connect..." = "Wprowadź adres lokalizacji, aby połączyć...";
|
||||
"Opened File" = "Otwarty plik";
|
||||
"File Extension" = "Rozszerzenie pliku";
|
||||
"Opening file..." = "Otwieranie pliku...";
|
||||
"Public account" = "Konto publiczne";
|
||||
"Your Accounts" = "Twoje konta";
|
||||
"Browse without account" = "Przeglądanie bez konta";
|
||||
"Use system controls with AVPlayer" = "Użyj systemowych kontrolek z AVPlayer";
|
||||
"Close video and player on end" = "Zamknij wideo i odtwarzacz po zakończeniu";
|
||||
"Rotate when entering fullscreen on landscape video" = "Obróć podczas przechodzenia do trybu pełnoekranowego w poziomym wideo";
|
||||
"Landscape right" = "Obrót w prawo";
|
||||
"No rotation" = "Brak rotacji";
|
||||
"Landscape left" = "Obrót w lewo";
|
||||
|
||||
@@ -191,9 +191,9 @@
|
||||
"Round corners" = "Bordas arredondadas";
|
||||
"Save" = "Salvar";
|
||||
"Restart/Play next" = "Reiniciar/Tocar próximo";
|
||||
"Seek gesture sensitivity" = "Sensitividade do gesto de busca";
|
||||
"Seek gesture speed" = "Velocidade do gesto de busca";
|
||||
"Seek with horizontal swipe on video" = "Buscar com deslize horizontal no vídeo";
|
||||
"Seek gesture sensitivity" = "Sensitividade do gesto de vasculhar";
|
||||
"Seek gesture speed" = "Velocidade do gesto de vasculhar";
|
||||
"Seek with horizontal swipe on video" = "Vasculhar com deslize horizontal no vídeo";
|
||||
"Proxy videos" = "Proxy de vídeos";
|
||||
"Rate" = "Taxa";
|
||||
|
||||
@@ -531,7 +531,7 @@
|
||||
"Player Bar" = "Barra do Player";
|
||||
"Gesture settings control skipping interval for double tap gesture on left/right side of the player. Changing system controls settings requires restart." = "As configurações de gesto controlam o intervalo de pulo para o gesto de dois toques no lado esquerdo/direito do player. Mudar as configurações dos controles do sistema requer reinício.";
|
||||
"Gesture settings control skipping interval for remote arrow buttons (for 2nd generation Siri Remote or newer). Changing system controls settings requires restart." = "As configurações de gesto controlam o intervalo de pulo para os botões de seta do controle remoto (para Siri Remote de 2ª geração ou posteriores). Mudar as configurações dos controles do sistema requer reinício.";
|
||||
"Seeking" = "Busca";
|
||||
"Seeking" = "Vasculhação";
|
||||
"Show unwatched feed badges" = "Mostrar ícone de feed não visto";
|
||||
"Clear all" = "Limpar tudo";
|
||||
"Gesture: backwards" = "Gesto: para trás";
|
||||
@@ -563,3 +563,10 @@
|
||||
"Open video description expanded" = "Abrir descrição do vídeo expandida";
|
||||
"Replay" = "Replay";
|
||||
"Stream" = "Stream";
|
||||
"Show scroll to top button in comments" = "Mostrar botão de voltar ao topo nos comentários";
|
||||
"Enter location address to connect..." = "Insira o endereço da localização para conectar…";
|
||||
"Seek" = "Vasculhar";
|
||||
"Enter account credentials to connect..." = "Insira as credenciais da conta para conectar…";
|
||||
"Opened File" = "Arquivo Aberto";
|
||||
"File Extension" = "Extensão do Arquivo";
|
||||
"Opening file..." = "Abrindo arquivo…";
|
||||
|
||||
@@ -563,3 +563,10 @@
|
||||
"Loop one" = "Buclă unu";
|
||||
"Autoplay next" = "Urmează redarea automată";
|
||||
"Stream" = "Flux";
|
||||
"Show scroll to top button in comments" = "Afișați butonul de derulare până sus în comentarii";
|
||||
"Seek" = "Căuta";
|
||||
"Enter account credentials to connect..." = "Introduceți acreditările contului pentru a vă conecta...";
|
||||
"Enter location address to connect..." = "Introdu adresa locației pentru a te conecta...";
|
||||
"Opened File" = "Fișier deschis";
|
||||
"Opening file..." = "Deschiderea fișierului...";
|
||||
"File Extension" = "Extensie fișier";
|
||||
|
||||
@@ -67,9 +67,9 @@
|
||||
"I am lost" = "Я загубився";
|
||||
"I found a bug /" = "Я знайшов баг /";
|
||||
"I have a feature request" = "В мене є пропозиція";
|
||||
"I like this app!" = "Мені подобається цей додаток!";
|
||||
"I like this app!" = "Мені подобається цей застосунок!";
|
||||
"I want to ask a question" = "Хочу задати питання";
|
||||
"If you are interested what's coming in future updates, you can track project Milestones." = "Якщо вам цікаво, що буде в наступних оновленнях, ви можете відстежувати етапи проекту.";
|
||||
"If you are interested what's coming in future updates, you can track project Milestones." = "Якщо вам цікаво, що буде в наступних оновленнях, ви можете відстежувати етапи проєкту.";
|
||||
"Increase rate" = "Підвищити якість";
|
||||
"Info" = "Інфо";
|
||||
"Instance of current account" = "Экземпляр поточного акаунта";
|
||||
@@ -197,7 +197,7 @@
|
||||
"Help" = "Допомога";
|
||||
"Related" = "Схожі";
|
||||
"Honor orientation lock" = "Блокування орієнтації";
|
||||
"If you are reporting a bug, include all relevant details (especially: app version, used device and system version, steps to reproduce)." = "Якщо ви повідомляєте про помилку, вказуйте всі відповідні деталі (особливо: версію програми, використовуваний пристрій, версію системи і кроки для відтворення).";
|
||||
"If you are reporting a bug, include all relevant details (especially: app version, used device and system version, steps to reproduce)." = "Якщо ви повідомляєте про помилку, вказуйте всі відповідні деталі (особливо: версію застосунку, використовуваний пристрій, версію системи і кроки для відтворення).";
|
||||
"Promoting a product or service that is directly related to the creator themselves. This usually includes merchandise or promotion of monetized platforms." = "Просування продукту або послуги, які безпосередньо пов'язані з самим автором. Зазвичай це стосується товарів або просування монетизованих платформ.";
|
||||
"Red" = "Червоний";
|
||||
"Refresh" = "Поновити";
|
||||
@@ -229,7 +229,7 @@
|
||||
"Clear Search History" = "Очистити історію пошуку";
|
||||
"Clear Search History..." = "Очистити історію пошуку...";
|
||||
"Clear the queue" = "Очистити чергу";
|
||||
"Close PiP and open player when application enters foreground" = "Закрийте PiP і відкрийте плеєр, коли програма відкриється";
|
||||
"Close PiP and open player when application enters foreground" = "Закрийте PiP і відкрийте плеєр, коли застосунок відкриється";
|
||||
"Close player when closing video" = "Закрийте плеєр, коли зупините відео";
|
||||
"Close player when starting PiP" = "Закрийте плеєр при старті PiP";
|
||||
"Close Video" = "Закрити відео";
|
||||
@@ -411,14 +411,14 @@
|
||||
"Share %@ link with time" = "Поділитися посиланням %@ з часом";
|
||||
"Reset watched status when playing again" = "Скинути статус перегляду при повторному відкритті";
|
||||
"Show account username" = "Відображати імʼя акаунту";
|
||||
"Restart the app to apply the settings above." = "Перезапустіть додаток, щоб застосувати нові налаштування.";
|
||||
"Restart the app to apply the settings above." = "Перезапустіть застосунок, щоб застосувати нові налаштування.";
|
||||
"Show anonymous accounts" = "Показувати анонімні акаунти";
|
||||
"Show channel name" = "Показувати назву каналу";
|
||||
"Sign In Required" = "Необхідно увійти в систему";
|
||||
"It can be changed later in settings. You can use your own locations too." = "Пізніше це можна змінити в налаштуваннях. Ви також можете використовувати власні локації.";
|
||||
"System controls buttons" = "Кнопки керування системою";
|
||||
"That's nice to hear. It is fun to deliver apps other people want to use. You can consider donating to the project or help by contributing to new features development." = "Приємно це чути. Створювати додатки, якими хочуть користуватися інші люди, дуже приємно. Ви можете розглянути можливість пожертвувати кошти на проект або допомогти, взявши участь у розробці нових функцій.";
|
||||
"This cannot be reverted. You might need to switch between views or restart the app to see changes." = "Це не можна буде скасувати. Можливо, вам доведеться переключитися між режимами перегляду або перезапустити програму, щоб побачити зміни.";
|
||||
"That's nice to hear. It is fun to deliver apps other people want to use. You can consider donating to the project or help by contributing to new features development." = "Приємно це чути. Створювати застосунки, якими хочуть користуватися інші люди, дуже приємно. Ви можете розглянути можливість пожертвувати кошти на проєкт або допомогти, взявши участь у розробці нових функцій.";
|
||||
"This cannot be reverted. You might need to switch between views or restart the app to see changes." = "Це не можна буде скасувати. Можливо, вам доведеться перемкнутися між режимами перегляду або перезапустити застосунок, щоб побачити зміни.";
|
||||
"Keep last played video in the queue after restart" = "Залишати останнє проглянуте відео в черзі після перезапуску";
|
||||
"Could not load streams" = "Не вдалося загрузити трансляції";
|
||||
"Could not open video" = "Не вдалося відкрити відео";
|
||||
@@ -430,7 +430,7 @@
|
||||
"Could not load video" = "Не вдалося завантажити відео";
|
||||
"No locations available at the moment" = "Зараз немає доступних API (локацій)";
|
||||
"Could not refresh Playlists" = "Не вдалося поновити плейлисти";
|
||||
"If you want this app to be available in your language, join translation project." = "Ви можете змінити або створити свій переклад на сторінці проекту.";
|
||||
"If you want this app to be available in your language, join translation project." = "Ви можете змінити або створити свій переклад на сторінці проєкту.";
|
||||
"No documents" = "Немає документів";
|
||||
"Are you sure you want to remove this document?" = "Ви впевнені, що бажаєте видалити цей документ?";
|
||||
"Recent Documents" = "Нещодавні документи";
|
||||
@@ -563,3 +563,7 @@
|
||||
"Loop one" = "Цикл один";
|
||||
"Autoplay next" = "Автовідтворення далі";
|
||||
"Stream" = "Потік";
|
||||
"Enter account credentials to connect..." = "Введіть облікові дані для з'єднання...";
|
||||
"Seek" = "Шукати";
|
||||
"Show scroll to top button in comments" = "Показати кнопку прокрутки вгору в коментарях";
|
||||
"Enter location address to connect..." = "Введіть адресу розташування для з'єднання...";
|
||||
|
||||
@@ -205,6 +205,7 @@
|
||||
371B7E6A2759791900D21217 /* CommentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E692759791900D21217 /* CommentsModel.swift */; };
|
||||
371B7E6B2759791900D21217 /* CommentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E692759791900D21217 /* CommentsModel.swift */; };
|
||||
371B7E6C2759791900D21217 /* CommentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E692759791900D21217 /* CommentsModel.swift */; };
|
||||
371B88F82A1A310100D57683 /* String+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3782B9512755667600990149 /* String+Format.swift */; };
|
||||
371CC76829466ED000979C1A /* AccountsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371CC76729466ED000979C1A /* AccountsView.swift */; };
|
||||
371CC76929466ED000979C1A /* AccountsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371CC76729466ED000979C1A /* AccountsView.swift */; };
|
||||
371CC76A29466ED000979C1A /* AccountsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371CC76729466ED000979C1A /* AccountsView.swift */; };
|
||||
@@ -253,7 +254,6 @@
|
||||
37319F0627103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; };
|
||||
37319F0727103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; };
|
||||
3732BFD028B83763009F3F4D /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 3732BFCF28B83763009F3F4D /* KeychainAccess */; };
|
||||
3732C9FD28C012E600E7DCAF /* SafeArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37ECED55289FE166002BC2C9 /* SafeArea.swift */; };
|
||||
3735C4E729A2D3D70051D251 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 3735C4E629A2D3D70051D251 /* Logging */; };
|
||||
3736A1FE286BB72300C9E5EE /* libavdevice.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3736A1EF286BB72300C9E5EE /* libavdevice.xcframework */; };
|
||||
3736A1FF286BB72300C9E5EE /* libavdevice.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3736A1EF286BB72300C9E5EE /* libavdevice.xcframework */; };
|
||||
@@ -375,7 +375,6 @@
|
||||
374D11E72943C56300CB4350 /* Cache in Frameworks */ = {isa = PBXBuildFile; productRef = 374D11E62943C56300CB4350 /* Cache */; };
|
||||
374DE88028BB896C0062BBF2 /* PlayerDragGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374DE87F28BB896C0062BBF2 /* PlayerDragGesture.swift */; };
|
||||
374DE88128BB896C0062BBF2 /* PlayerDragGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374DE87F28BB896C0062BBF2 /* PlayerDragGesture.swift */; };
|
||||
374DE88328BB8A280062BBF2 /* PlayerOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374DE88228BB8A280062BBF2 /* PlayerOrientation.swift */; };
|
||||
375168D62700FAFF008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; };
|
||||
375168D72700FDB8008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; };
|
||||
375168D82700FDB9008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; };
|
||||
@@ -725,6 +724,11 @@
|
||||
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 */; };
|
||||
37B7CFEC2A197844001B0564 /* AppleAVPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BD226A1D4780092E2DB /* AppleAVPlayerView.swift */; };
|
||||
37B7CFEE2A19789F001B0564 /* MacOSPiPDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B7CFED2A19789F001B0564 /* MacOSPiPDelegate.swift */; };
|
||||
37B7CFEF2A197A08001B0564 /* SafeAreaModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DCD3142A18F7630059A470 /* SafeAreaModel.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 */; };
|
||||
@@ -781,7 +785,6 @@
|
||||
37BE0BD326A1D4780092E2DB /* AppleAVPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BD226A1D4780092E2DB /* AppleAVPlayerView.swift */; };
|
||||
37BE0BD426A1D47D0092E2DB /* AppleAVPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BD226A1D4780092E2DB /* AppleAVPlayerView.swift */; };
|
||||
37BE0BD726A1D4A90092E2DB /* AppleAVPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BD526A1D4A90092E2DB /* AppleAVPlayerViewController.swift */; };
|
||||
37BE0BDC26A2367F0092E2DB /* AppleAVPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BDB26A2367F0092E2DB /* AppleAVPlayerView.swift */; };
|
||||
37C069782725962F00F7F6CB /* ScreenSaverManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C069772725962F00F7F6CB /* ScreenSaverManager.swift */; };
|
||||
37C0697A2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C069792725C09E00F7F6CB /* PlayerQueueItemBridge.swift */; };
|
||||
37C0697B2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C069792725C09E00F7F6CB /* PlayerQueueItemBridge.swift */; };
|
||||
@@ -885,6 +888,11 @@
|
||||
37D9BA0629507F69002586BD /* PlayerControlsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D9BA0529507F69002586BD /* PlayerControlsSettings.swift */; };
|
||||
37D9BA0729507F69002586BD /* PlayerControlsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D9BA0529507F69002586BD /* PlayerControlsSettings.swift */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
@@ -941,7 +949,6 @@
|
||||
37EBD8CA27AF26C200F1C24B /* MPVBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EBD8C927AF26C200F1C24B /* MPVBackend.swift */; };
|
||||
37EBD8CB27AF26C200F1C24B /* MPVBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EBD8C927AF26C200F1C24B /* MPVBackend.swift */; };
|
||||
37EBD8CC27AF26C200F1C24B /* MPVBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EBD8C927AF26C200F1C24B /* MPVBackend.swift */; };
|
||||
37ECED56289FE166002BC2C9 /* SafeArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37ECED55289FE166002BC2C9 /* SafeArea.swift */; };
|
||||
37EE6DC528A305AD00BFD632 /* Reachability in Frameworks */ = {isa = PBXBuildFile; productRef = 37EE6DC428A305AD00BFD632 /* Reachability */; };
|
||||
37EF5C222739D37B00B03725 /* MenuModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF5C212739D37B00B03725 /* MenuModel.swift */; };
|
||||
37EF5C232739D37B00B03725 /* MenuModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF5C212739D37B00B03725 /* MenuModel.swift */; };
|
||||
@@ -1258,7 +1265,6 @@
|
||||
374C0542272496E4009BDDBE /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = macOS/AppDelegate.swift; sourceTree = SOURCE_ROOT; };
|
||||
374C0544272496FD009BDDBE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
374DE87F28BB896C0062BBF2 /* PlayerDragGesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerDragGesture.swift; sourceTree = "<group>"; };
|
||||
374DE88228BB8A280062BBF2 /* PlayerOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerOrientation.swift; sourceTree = "<group>"; };
|
||||
375168D52700FAFF008F96A6 /* Debounce.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debounce.swift; sourceTree = "<group>"; };
|
||||
3751B4B127836902000B7DF4 /* SearchPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchPage.swift; sourceTree = "<group>"; };
|
||||
3751BA7D27E63F1D007B1A60 /* MPVOGLView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPVOGLView.swift; sourceTree = "<group>"; };
|
||||
@@ -1381,6 +1387,9 @@
|
||||
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>"; };
|
||||
37B7CFED2A19789F001B0564 /* MacOSPiPDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MacOSPiPDelegate.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>"; };
|
||||
@@ -1403,7 +1412,6 @@
|
||||
37BE0BCE26A0E2D50092E2DB /* VideoPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.swift; sourceTree = "<group>"; };
|
||||
37BE0BD226A1D4780092E2DB /* AppleAVPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleAVPlayerView.swift; sourceTree = "<group>"; };
|
||||
37BE0BD526A1D4A90092E2DB /* AppleAVPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleAVPlayerViewController.swift; sourceTree = "<group>"; };
|
||||
37BE0BDB26A2367F0092E2DB /* AppleAVPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleAVPlayerView.swift; sourceTree = "<group>"; };
|
||||
37C069772725962F00F7F6CB /* ScreenSaverManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenSaverManager.swift; sourceTree = "<group>"; };
|
||||
37C069792725C09E00F7F6CB /* PlayerQueueItemBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueueItemBridge.swift; sourceTree = "<group>"; };
|
||||
37C0697D2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMTime+DefaultTimescale.swift"; sourceTree = "<group>"; };
|
||||
@@ -1458,6 +1466,9 @@
|
||||
37D836BB294927E700005E5E /* ChannelsCacheModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelsCacheModel.swift; sourceTree = "<group>"; };
|
||||
37D9169A27388A81002B1BAA /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
@@ -1485,7 +1496,6 @@
|
||||
37EBD8C327AF0DA800F1C24B /* PlayerBackend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerBackend.swift; sourceTree = "<group>"; };
|
||||
37EBD8C527AF26B300F1C24B /* AVPlayerBackend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerBackend.swift; sourceTree = "<group>"; };
|
||||
37EBD8C927AF26C200F1C24B /* MPVBackend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPVBackend.swift; sourceTree = "<group>"; };
|
||||
37ECED55289FE166002BC2C9 /* SafeArea.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeArea.swift; sourceTree = "<group>"; };
|
||||
37EF5C212739D37B00B03725 /* MenuModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuModel.swift; sourceTree = "<group>"; };
|
||||
37EF9A75275BEB8E0043B585 /* CommentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentView.swift; sourceTree = "<group>"; };
|
||||
37EFAC0728C138CD00ED9B89 /* ControlsOverlayModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlsOverlayModel.swift; sourceTree = "<group>"; };
|
||||
@@ -1812,7 +1822,6 @@
|
||||
374DE87F28BB896C0062BBF2 /* PlayerDragGesture.swift */,
|
||||
3703100127B0713600ECDDAA /* PlayerGestures.swift */,
|
||||
373031F22838388A000CFD59 /* PlayerLayerView.swift */,
|
||||
374DE88228BB8A280062BBF2 /* PlayerOrientation.swift */,
|
||||
3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */,
|
||||
373197D82732015300EF734F /* RelatedView.swift */,
|
||||
3795593527B08538007FF8F4 /* StreamControl.swift */,
|
||||
@@ -1899,6 +1908,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 */,
|
||||
);
|
||||
@@ -2180,6 +2191,8 @@
|
||||
3749BF9227ADA142000480FF /* BridgingHeader.h */,
|
||||
37992DC726CC50BC003D4C27 /* Info.plist */,
|
||||
3782430A291E5AFA005DEC1C /* Yattee (iOS).entitlements */,
|
||||
37DCD3102A18E8150059A470 /* OrientationModel.swift */,
|
||||
37DCD3142A18F7630059A470 /* SafeAreaModel.swift */,
|
||||
);
|
||||
path = iOS;
|
||||
sourceTree = "<group>";
|
||||
@@ -2211,8 +2224,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
374C0542272496E4009BDDBE /* AppDelegate.swift */,
|
||||
37BE0BDB26A2367F0092E2DB /* AppleAVPlayerView.swift */,
|
||||
37FD43DB270470B70073EE42 /* InstancesSettings.swift */,
|
||||
37B7CFED2A19789F001B0564 /* MacOSPiPDelegate.swift */,
|
||||
3751BA7D27E63F1D007B1A60 /* MPVOGLView.swift */,
|
||||
37F7AB5428A951B200FB46B5 /* Power.swift */,
|
||||
37E04C0E275940FB00172673 /* VerticalScrollingFix.swift */,
|
||||
@@ -2228,6 +2241,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
379775922689365600DD52A8 /* Array+Next.swift */,
|
||||
37DCD3162A191A180059A470 /* AVPlayerViewController+FullScreen.swift */,
|
||||
376578842685429C00D4EA09 /* CaseIterable+Next.swift */,
|
||||
37C0697D2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift */,
|
||||
378AE942274EF00A006A4EE1 /* Color+Background.swift */,
|
||||
@@ -2255,9 +2269,9 @@
|
||||
37992DC826CC50CD003D4C27 /* iOS */,
|
||||
37BE0BD826A214500092E2DB /* macOS */,
|
||||
37D4B159267164AE00C925CA /* tvOS */,
|
||||
37D4B1B72672CFE300C925CA /* Model */,
|
||||
37D4B0C12671614700C925CA /* Shared */,
|
||||
3722AEBA274DA312005EA4D6 /* Backports */,
|
||||
37D4B1B72672CFE300C925CA /* Model */,
|
||||
37C7A9022679058300E721B4 /* Extensions */,
|
||||
37DD9DCC2785EE6F00539416 /* Vendor */,
|
||||
3748186426A762300084E870 /* Fixtures */,
|
||||
@@ -2298,7 +2312,6 @@
|
||||
3729037D2739E47400EA99F6 /* MenuCommands.swift */,
|
||||
37B7958F2771DAE0001CF27B /* OpenURLHandler.swift */,
|
||||
374924EC2921669B0017D862 /* PreferenceKeys.swift */,
|
||||
37ECED55289FE166002BC2C9 /* SafeArea.swift */,
|
||||
3700155E271B12DD0049C794 /* SiestaConfiguration.swift */,
|
||||
37FFC43F272734C3009FFD26 /* Throttle.swift */,
|
||||
378FFBC328660172009E3FBE /* URLParser.swift */,
|
||||
@@ -3045,6 +3058,7 @@
|
||||
378FFBC728660172009E3FBE /* URLParser.swift in Sources */,
|
||||
37C0C0FF28665EAC007F6F78 /* VideosApp.swift in Sources */,
|
||||
378FFBC92866018A009E3FBE /* URLParserTests.swift in Sources */,
|
||||
371B88F82A1A310100D57683 /* String+Format.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -3057,7 +3071,6 @@
|
||||
374710052755291C00CE0F87 /* SearchTextField.swift in Sources */,
|
||||
37494EA529200B14000DF176 /* DocumentsView.swift in Sources */,
|
||||
374DE88028BB896C0062BBF2 /* PlayerDragGesture.swift in Sources */,
|
||||
37ECED56289FE166002BC2C9 /* SafeArea.swift in Sources */,
|
||||
37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
|
||||
37C2211D27ADA33300305B41 /* MPVViewController.swift in Sources */,
|
||||
37A362BE29537AAA00BDF328 /* PlaybackSettings.swift in Sources */,
|
||||
@@ -3094,7 +3107,6 @@
|
||||
377FF88F291A99580028EB0B /* HistoryView.swift in Sources */,
|
||||
3782B94F27553A6700990149 /* SearchSuggestions.swift in Sources */,
|
||||
378E50FF26FE8EEE00F49626 /* AccountViewButton.swift in Sources */,
|
||||
374DE88328BB8A280062BBF2 /* PlayerOrientation.swift in Sources */,
|
||||
374924F029216C630017D862 /* VideoActions.swift in Sources */,
|
||||
37169AA62729E2CC0011DE61 /* AccountsBridge.swift in Sources */,
|
||||
37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */,
|
||||
@@ -3176,6 +3188,7 @@
|
||||
37FD77002932C4DA00D91A5F /* URL+ByReplacingYatteeProtocol.swift in Sources */,
|
||||
373CFADB269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
||||
3714166F267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
||||
37DCD3112A18E8150059A470 /* OrientationModel.swift in Sources */,
|
||||
3782B9522755667600990149 /* String+Format.swift in Sources */,
|
||||
37F9619F27BD90BB00058149 /* PlayerBackendType.swift in Sources */,
|
||||
373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
||||
@@ -3246,6 +3259,7 @@
|
||||
37A9965A26D6F8CA006E3224 /* HorizontalCells.swift in Sources */,
|
||||
37EBD8CA27AF26C200F1C24B /* MPVBackend.swift in Sources */,
|
||||
37635FE4291EA6CF00C11E79 /* AccentButton.swift in Sources */,
|
||||
37DCD3152A18F7630059A470 /* SafeAreaModel.swift in Sources */,
|
||||
37D526DE2720AC4400ED2F5E /* VideosAPI.swift in Sources */,
|
||||
37484C2526FC83E000287258 /* InstanceForm.swift in Sources */,
|
||||
37DD9DBD2785D60300539416 /* ScrollViewMatcher.swift in Sources */,
|
||||
@@ -3297,6 +3311,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 */,
|
||||
@@ -3306,6 +3322,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 */,
|
||||
@@ -3335,11 +3352,11 @@
|
||||
37D836BD294927E700005E5E /* ChannelsCacheModel.swift in Sources */,
|
||||
3727B74B27872B880021C15E /* VisualEffectBlur-macOS.swift in Sources */,
|
||||
374710062755291C00CE0F87 /* SearchTextField.swift in Sources */,
|
||||
37B7CFEC2A197844001B0564 /* AppleAVPlayerView.swift in Sources */,
|
||||
37F0F4EB286F397E00C06C2E /* SettingsModel.swift in Sources */,
|
||||
378AE93F274EDFB5006A4EE1 /* Tint+Backport.swift in Sources */,
|
||||
37C194C826F6A9C8005D3B96 /* RecentsModel.swift in Sources */,
|
||||
37737786276F9858000521C1 /* Windows.swift in Sources */,
|
||||
37BE0BDC26A2367F0092E2DB /* AppleAVPlayerView.swift in Sources */,
|
||||
3786D05F294C737300D23E82 /* RequestErrorButton.swift in Sources */,
|
||||
3743CA53270F284F00E4D32B /* View+Borders.swift in Sources */,
|
||||
37599F39272B4D740087F250 /* FavoriteButton.swift in Sources */,
|
||||
@@ -3404,6 +3421,7 @@
|
||||
37B81B0026D2CA3700675966 /* VideoDetails.swift in Sources */,
|
||||
3752069A285E8DD300CA655F /* Chapter.swift in Sources */,
|
||||
373EBD69291F252D002ADB9C /* EditFavorites.swift in Sources */,
|
||||
37B7CFEE2A19789F001B0564 /* MacOSPiPDelegate.swift in Sources */,
|
||||
37484C1A26FC837400287258 /* PlayerSettings.swift in Sources */,
|
||||
3776925329463C310055EC18 /* PlaylistsCacheModel.swift in Sources */,
|
||||
37BD07C32698AD4F003EBB87 /* ContentView.swift in Sources */,
|
||||
@@ -3616,6 +3634,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 */,
|
||||
@@ -3699,7 +3718,6 @@
|
||||
376B2E0926F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
||||
378FFBC628660172009E3FBE /* URLParser.swift in Sources */,
|
||||
37141671267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
||||
3732C9FD28C012E600E7DCAF /* SafeArea.swift in Sources */,
|
||||
37A2B348294723850050933E /* CacheModel.swift in Sources */,
|
||||
37C3A24727235DA70087A57A /* ChannelPlaylist.swift in Sources */,
|
||||
3788AC2926F6840700F6BAA9 /* FavoriteItemView.swift in Sources */,
|
||||
@@ -3737,6 +3755,7 @@
|
||||
3786D060294C737300D23E82 /* RequestErrorButton.swift in Sources */,
|
||||
37A362C429537FED00BDF328 /* PlaybackSettingsPresentationDetents+Backport.swift in Sources */,
|
||||
37BDFF1929487B99000C6404 /* PlaylistVideosView.swift in Sources */,
|
||||
37B7CFEF2A197A08001B0564 /* SafeAreaModel.swift in Sources */,
|
||||
37C0698427260B2100F7F6CB /* ThumbnailsModel.swift in Sources */,
|
||||
374924DC2921050B0017D862 /* LocationsSettings.swift in Sources */,
|
||||
37D6025B28C17375009E8D98 /* PlaybackStatsView.swift in Sources */,
|
||||
@@ -3815,6 +3834,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 */,
|
||||
@@ -3962,7 +3982,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 146;
|
||||
CURRENT_PROJECT_VERSION = 150;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
|
||||
@@ -3973,7 +3993,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.4.4;
|
||||
MARKETING_VERSION = 1.4.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "stream.yattee.app.Open-in-Yattee";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -3993,7 +4013,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 146;
|
||||
CURRENT_PROJECT_VERSION = 150;
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||
@@ -4005,7 +4025,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.4.4;
|
||||
MARKETING_VERSION = 1.4.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "stream.yattee.app.Open-in-Yattee";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -4024,11 +4044,11 @@
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 146;
|
||||
CURRENT_PROJECT_VERSION = 150;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
MARKETING_VERSION = 1.4.4;
|
||||
MARKETING_VERSION = 1.4.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "net.arekf.Shared-Tests";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = auto;
|
||||
@@ -4044,11 +4064,11 @@
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 146;
|
||||
CURRENT_PROJECT_VERSION = 150;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
MARKETING_VERSION = 1.4.4;
|
||||
MARKETING_VERSION = 1.4.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "net.arekf.Shared-Tests";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = auto;
|
||||
@@ -4204,7 +4224,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "iOS/Yattee (iOS).entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 146;
|
||||
CURRENT_PROJECT_VERSION = 150;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
@@ -4228,7 +4248,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(inherited)";
|
||||
MARKETING_VERSION = 1.4.4;
|
||||
MARKETING_VERSION = 1.4.5;
|
||||
OTHER_LDFLAGS = (
|
||||
"-lstdc++",
|
||||
"-Wl,-no_compact_unwind",
|
||||
@@ -4257,7 +4277,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 146;
|
||||
CURRENT_PROJECT_VERSION = 150;
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1";
|
||||
@@ -4278,7 +4298,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(inherited)";
|
||||
MARKETING_VERSION = 1.4.4;
|
||||
MARKETING_VERSION = 1.4.5;
|
||||
OTHER_LDFLAGS = (
|
||||
"-lstdc++",
|
||||
"-Wl,-no_compact_unwind",
|
||||
@@ -4309,7 +4329,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 146;
|
||||
CURRENT_PROJECT_VERSION = 150;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@@ -4328,7 +4348,7 @@
|
||||
"$(PROJECT_DIR)/Vendor/mpv/macOS/lib",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
MARKETING_VERSION = 1.4.4;
|
||||
MARKETING_VERSION = 1.4.5;
|
||||
OTHER_LDFLAGS = "-Wl,-no_compact_unwind";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
|
||||
PRODUCT_NAME = Yattee;
|
||||
@@ -4351,7 +4371,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 146;
|
||||
CURRENT_PROJECT_VERSION = 150;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = 78Z5H3M6RJ;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
@@ -4371,7 +4391,7 @@
|
||||
"$(PROJECT_DIR)/Vendor/mpv/macOS/lib",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
MARKETING_VERSION = 1.4.4;
|
||||
MARKETING_VERSION = 1.4.5;
|
||||
OTHER_LDFLAGS = "-Wl,-no_compact_unwind";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
|
||||
PRODUCT_NAME = Yattee;
|
||||
@@ -4389,7 +4409,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 146;
|
||||
CURRENT_PROJECT_VERSION = 150;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -4397,7 +4417,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.4.4;
|
||||
MARKETING_VERSION = 1.4.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "stream.yattee.Tests-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -4413,7 +4433,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 146;
|
||||
CURRENT_PROJECT_VERSION = 150;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -4421,7 +4441,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.4.4;
|
||||
MARKETING_VERSION = 1.4.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "stream.yattee.Tests-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -4439,7 +4459,7 @@
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 146;
|
||||
CURRENT_PROJECT_VERSION = 150;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -4448,7 +4468,7 @@
|
||||
"@loader_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||
MARKETING_VERSION = 1.4.4;
|
||||
MARKETING_VERSION = 1.4.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "stream.yattee.Tests-macOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
@@ -4464,7 +4484,7 @@
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 146;
|
||||
CURRENT_PROJECT_VERSION = 150;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -4473,7 +4493,7 @@
|
||||
"@loader_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||
MARKETING_VERSION = 1.4.4;
|
||||
MARKETING_VERSION = 1.4.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "stream.yattee.Tests-macOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
@@ -4490,7 +4510,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 146;
|
||||
CURRENT_PROJECT_VERSION = 150;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -4506,7 +4526,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(inherited)";
|
||||
MARKETING_VERSION = 1.4.4;
|
||||
MARKETING_VERSION = 1.4.5;
|
||||
OTHER_LDFLAGS = (
|
||||
"-Wl,-no_compact_unwind",
|
||||
"-lstdc++",
|
||||
@@ -4530,7 +4550,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 146;
|
||||
CURRENT_PROJECT_VERSION = 150;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
"DEVELOPMENT_TEAM[sdk=appletvos*]" = 78Z5H3M6RJ;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -4547,7 +4567,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(inherited)";
|
||||
MARKETING_VERSION = 1.4.4;
|
||||
MARKETING_VERSION = 1.4.5;
|
||||
OTHER_LDFLAGS = (
|
||||
"-Wl,-no_compact_unwind",
|
||||
"-lstdc++",
|
||||
@@ -4571,14 +4591,14 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 146;
|
||||
CURRENT_PROJECT_VERSION = 150;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.4.4;
|
||||
MARKETING_VERSION = 1.4.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.YatteeUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = appletvos;
|
||||
@@ -4595,14 +4615,14 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 146;
|
||||
CURRENT_PROJECT_VERSION = 150;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.4.4;
|
||||
MARKETING_VERSION = 1.4.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.YatteeUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = appletvos;
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Alamofire/Alamofire.git",
|
||||
"state" : {
|
||||
"revision" : "78424be314842833c04bc3bef5b72e85fff99204",
|
||||
"version" : "5.6.4"
|
||||
"revision" : "bc268c28fb170f494de9e9927c371b8342979ece",
|
||||
"version" : "5.7.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -24,7 +24,7 @@
|
||||
"location" : "https://github.com/hyperoslo/Cache.git",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "eeaf771d8d2e8247fbd6da2e27c986d99803fb1f"
|
||||
"revision" : "d048bf404a5c8362c6cf840c2096d5777975cd27"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -96,7 +96,7 @@
|
||||
"location" : "https://github.com/SDWebImage/SDWebImage",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "09f0fdd1890b7c9420109bc7b45dcee91b66b7de"
|
||||
"revision" : "7f9fb5d43ecd4aa714c00746f54873f354403438"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -158,8 +158,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/siteline/SwiftUI-Introspect.git",
|
||||
"state" : {
|
||||
"revision" : "c18951c747ab62af7c15e17a81bd37d4fd5a9979",
|
||||
"version" : "0.2.3"
|
||||
"revision" : "5b3f3996c7a2a84d5f4ba0e03cd7d584154778f2",
|
||||
"version" : "0.3.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -31,7 +31,7 @@ struct Orientation {
|
||||
logger.info("rotating to \(orientationString)")
|
||||
|
||||
if #available(iOS 16, *) {
|
||||
guard let windowScene = SafeArea.scene else { return }
|
||||
guard let windowScene = Self.scene else { return }
|
||||
let rotateOrientationMask = rotateOrientation == .portrait ? UIInterfaceOrientationMask.portrait :
|
||||
rotateOrientation == .landscapeLeft ? .landscapeLeft :
|
||||
rotateOrientation == .landscapeRight ? .landscapeRight :
|
||||
@@ -46,4 +46,11 @@ struct Orientation {
|
||||
|
||||
UINavigationController.attemptRotationToDeviceOrientation()
|
||||
}
|
||||
|
||||
private static var scene: UIWindowScene? {
|
||||
UIApplication.shared.connectedScenes
|
||||
.filter { $0.activationState == .foregroundActive }
|
||||
.compactMap { $0 as? UIWindowScene }
|
||||
.first
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import Defaults
|
||||
import Foundation
|
||||
import Repeat
|
||||
import SwiftUI
|
||||
|
||||
extension VideoPlayerView {
|
||||
final class OrientationModel {
|
||||
static var shared = OrientationModel()
|
||||
|
||||
var orientation = UIInterfaceOrientation.portrait
|
||||
var lastOrientation: UIInterfaceOrientation?
|
||||
var orientationDebouncer = Debouncer(.milliseconds(300))
|
||||
internal var orientationObserver: Any?
|
||||
|
||||
private var player = PlayerModel.shared
|
||||
|
||||
func configureOrientationUpdatesBasedOnAccelerometer() {
|
||||
let currentOrientation = OrientationTracker.shared.currentInterfaceOrientation
|
||||
if currentOrientation.isLandscape,
|
||||
@@ -15,8 +25,8 @@ extension VideoPlayerView {
|
||||
player.presentingPlayer
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
player.controls.presentingControls = false
|
||||
player.enterFullScreen(showControls: false)
|
||||
self.player.controls.presentingControls = false
|
||||
self.player.enterFullScreen(showControls: false)
|
||||
}
|
||||
|
||||
player.onPresentPlayer.append {
|
||||
@@ -30,42 +40,42 @@ extension VideoPlayerView {
|
||||
queue: .main
|
||||
) { _ in
|
||||
guard !Defaults[.honorSystemOrientationLock],
|
||||
player.presentingPlayer,
|
||||
!player.playingInPictureInPicture,
|
||||
player.lockedOrientation.isNil
|
||||
self.player.presentingPlayer,
|
||||
!self.player.playingInPictureInPicture,
|
||||
self.player.lockedOrientation.isNil
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
let orientation = OrientationTracker.shared.currentInterfaceOrientation
|
||||
|
||||
guard lastOrientation != orientation else {
|
||||
guard self.lastOrientation != orientation else {
|
||||
return
|
||||
}
|
||||
|
||||
lastOrientation = orientation
|
||||
self.lastOrientation = orientation
|
||||
|
||||
DispatchQueue.main.async {
|
||||
guard Defaults[.enterFullscreenInLandscape],
|
||||
player.presentingPlayer
|
||||
self.player.presentingPlayer
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
orientationDebouncer.callback = {
|
||||
self.orientationDebouncer.callback = {
|
||||
DispatchQueue.main.async {
|
||||
if orientation.isLandscape {
|
||||
player.controls.presentingControls = false
|
||||
player.enterFullScreen(showControls: false)
|
||||
self.player.controls.presentingControls = false
|
||||
self.player.enterFullScreen(showControls: false)
|
||||
Orientation.lockOrientation(OrientationTracker.shared.currentInterfaceOrientationMask, andRotateTo: orientation)
|
||||
} else {
|
||||
player.exitFullScreen(showControls: false)
|
||||
self.player.exitFullScreen(showControls: false)
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
orientationDebouncer.call()
|
||||
self.orientationDebouncer.call()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,4 +84,12 @@ extension VideoPlayerView {
|
||||
guard let observer = orientationObserver else { return }
|
||||
NotificationCenter.default.removeObserver(observer)
|
||||
}
|
||||
|
||||
func lockOrientation(_ orientation: UIInterfaceOrientationMask, andRotateTo rotateOrientation: UIInterfaceOrientation? = nil) {
|
||||
if let rotateOrientation {
|
||||
self.orientation = rotateOrientation
|
||||
lastOrientation = rotateOrientation
|
||||
}
|
||||
Orientation.lockOrientation(orientation, andRotateTo: rotateOrientation)
|
||||
}
|
||||
}
|
||||
15
iOS/SafeAreaModel.swift
Normal file
15
iOS/SafeAreaModel.swift
Normal file
@@ -0,0 +1,15 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
final class SafeAreaModel: ObservableObject {
|
||||
static var shared = SafeAreaModel()
|
||||
@Published var safeArea = EdgeInsets()
|
||||
|
||||
var horizontalInsets: Double {
|
||||
safeArea.leading + safeArea.trailing
|
||||
}
|
||||
|
||||
var verticalInsets: Double {
|
||||
safeArea.top + safeArea.bottom
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct AppleAVPlayerView: NSViewRepresentable {
|
||||
func makeNSView(context _: Context) -> some NSView {
|
||||
PlayerLayerView(frame: .zero)
|
||||
}
|
||||
|
||||
func updateNSView(_: NSViewType, context _: Context) {}
|
||||
}
|
||||
34
macOS/MacOSPiPDelegate.swift
Normal file
34
macOS/MacOSPiPDelegate.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
import AVKit
|
||||
import Foundation
|
||||
|
||||
final class MacOSPiPDelegate: NSObject, AVPlayerViewPictureInPictureDelegate {
|
||||
var playerModel: PlayerModel { .shared }
|
||||
|
||||
func playerViewShouldAutomaticallyDismissAtPicture(inPictureStart _: AVPlayerView) -> Bool {
|
||||
false
|
||||
}
|
||||
|
||||
func playerViewWillStartPicture(inPicture _: AVPlayerView) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
||||
self?.playerModel.playingInPictureInPicture = true
|
||||
self?.playerModel.hide()
|
||||
}
|
||||
}
|
||||
|
||||
func playerViewWillStopPicture(inPicture _: AVPlayerView) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
||||
self?.playerModel.playingInPictureInPicture = false
|
||||
self?.playerModel.show()
|
||||
}
|
||||
}
|
||||
|
||||
func playerView(
|
||||
_: AVPlayerView,
|
||||
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: (Bool) -> Void
|
||||
) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
||||
self?.playerModel.show()
|
||||
}
|
||||
completionHandler(true)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user