mirror of
https://github.com/yattee/yattee.git
synced 2025-12-15 04:28:14 +00:00
Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bf3df1a29 | ||
|
|
34a805b986 | ||
|
|
36f680be62 | ||
|
|
a27ab02433 | ||
|
|
59dd0785b3 | ||
|
|
d7be915e7e | ||
|
|
3752f67630 | ||
|
|
dfe7565138 | ||
|
|
4d02538cb9 | ||
|
|
3229528a09 | ||
|
|
fffc4f4a5f | ||
|
|
e85bfe5007 | ||
|
|
b00b733fd5 | ||
|
|
119c663436 | ||
|
|
e8fcee23ef | ||
|
|
d56ef74a99 | ||
|
|
98f5b1a22b | ||
|
|
f0b7bd3ab8 | ||
|
|
2d7a101ce0 | ||
|
|
b2114174b4 | ||
|
|
e9f502a486 | ||
|
|
6978e9437c | ||
|
|
2026201a5f | ||
|
|
633af02577 | ||
|
|
1fd62f04aa | ||
|
|
e749307a0e | ||
|
|
d76ec881be | ||
|
|
72a39a2c75 | ||
|
|
8a84db5a2d | ||
|
|
663c37e3d2 | ||
|
|
ea2b329df2 | ||
|
|
bd79f56800 | ||
|
|
9a650b4ac0 | ||
|
|
13382270d5 | ||
|
|
24626c2299 | ||
|
|
18ac577c7f | ||
|
|
617af2cd20 | ||
|
|
1b778318dc | ||
|
|
c9ce574c7a | ||
|
|
9a1f0d7aaa | ||
|
|
1cb695848c | ||
|
|
740a2f85ac | ||
|
|
1a22ac71be | ||
|
|
f0d581d512 | ||
|
|
049a42f2e8 | ||
|
|
cea2684a29 | ||
|
|
772e5016c4 | ||
|
|
ed3d9a7d7c | ||
|
|
bde9aade11 | ||
|
|
a194738bb6 | ||
|
|
45567254f2 | ||
|
|
a3139ad059 | ||
|
|
598f17479f | ||
|
|
e888abfba9 | ||
|
|
e1e53b2d36 | ||
|
|
9510d91d61 | ||
|
|
59da0e71b6 | ||
|
|
6bdfb7368c | ||
|
|
7b26fdf400 | ||
|
|
67b41e36d5 | ||
|
|
c9c60349df | ||
|
|
6a70663f06 | ||
|
|
3d556d836f | ||
|
|
8feeb33a55 | ||
|
|
497c3bfc12 | ||
|
|
b51eadc7a9 | ||
|
|
7d0c1180c4 | ||
|
|
0c1fb02d50 | ||
|
|
6f358fab56 | ||
|
|
7631e2a8ed | ||
|
|
3a5f3fdfde | ||
|
|
e3633bdaf7 | ||
|
|
e912d910bc | ||
|
|
5ccb0f90d5 | ||
|
|
278bc343c2 | ||
|
|
5dc197664d | ||
|
|
192550ba7a |
32
CHANGELOG.md
32
CHANGELOG.md
@@ -1,8 +1,11 @@
|
||||
## Build 190
|
||||
* Improved stream resolution handling by @stonerl in https://github.com/yattee/yattee/pull/747
|
||||
* Fix some potential crashes by @stonerl in https://github.com/yattee/yattee/pull/748
|
||||
* Fix regression and improve curentChapter handling by @stonerl in https://github.com/yattee/yattee/pull/749
|
||||
* Refined chapter font scaling by @stonerl in https://github.com/yattee/yattee/pull/750
|
||||
## Build 194
|
||||
* Gestures: swipe up toggles fullscreen by @stonerl in https://github.com/yattee/yattee/pull/778
|
||||
* don’t open video when dismissing context menu by @stonerl in https://github.com/yattee/yattee/pull/780
|
||||
* mpv: remove video layer when entering background by @stonerl in https://github.com/yattee/yattee/pull/793
|
||||
* hi-res invidious logos by @stonerl in https://github.com/yattee/yattee/pull/791
|
||||
* enable -O3 by @stonerl in https://github.com/yattee/yattee/pull/794
|
||||
* Better audio ducking by @stonerl in https://github.com/yattee/yattee/pull/779
|
||||
* fix picture in picture by @stonerl in https://github.com/yattee/yattee/pull/789
|
||||
|
||||
## Previous builds
|
||||
* Add skip, play/pause, and fullscreen shortcuts to macOS player (by @rickykresslein)
|
||||
@@ -21,6 +24,25 @@
|
||||
* Add import export of missing settings
|
||||
* macOS: Fix settings windows layout
|
||||
* Fix seek OSD layout on tvOS, revert OSD position
|
||||
* Invidious: propper HTTP basic auth support by @stonerl in https://github.com/yattee/yattee/pull/762
|
||||
* Apply correct orientation by @stonerl in https://github.com/yattee/yattee/pull/770
|
||||
* Circular Invidious logo by @stonerl in https://github.com/yattee/yattee/pull/769
|
||||
* Video Thumbnails: retry 3 times fetching from URL by @stonerl in https://github.com/yattee/yattee/pull/768
|
||||
* Make thumbnail fill the view in music mode by @stonerl in https://github.com/yattee/yattee/pull/766
|
||||
* Changes to defaults by @stonerl in https://github.com/yattee/yattee/pull/767
|
||||
* Fixed fullscreen handling for backgrounding by @stonerl in https://github.com/yattee/yattee/pull/772
|
||||
* Update now playing info when using system controls – Partial fix for 503 by @stonerl in https://github.com/yattee/yattee/pull/765
|
||||
* Fix crash on HLS live playback by @stonerl in https://github.com/yattee/yattee/pull/775
|
||||
* Fix mpv crashing on macOS by @stonerl in https://github.com/yattee/yattee/pull/754
|
||||
* Refreshed icons for iOS and macOS by @stonerl in https://github.com/yattee/yattee/pull/752
|
||||
* Add new MPVKit repo by @stonerl in https://github.com/yattee/yattee/pull/753
|
||||
* Add Chinese (Simplified) - zh-Hans to LanguageCodes by @stonerl in https://github.com/yattee/yattee/pull/757
|
||||
* Color changes to VideoActions by @stonerl in https://github.com/yattee/yattee/pull/759
|
||||
* Hide VideoActions Bar when no buttons is visible by @stonerl in https://github.com/yattee/yattee/pull/760
|
||||
* Improved stream resolution handling by @stonerl in https://github.com/yattee/yattee/pull/747
|
||||
* Fix some potential crashes by @stonerl in https://github.com/yattee/yattee/pull/748
|
||||
* Fix regression and improve curentChapter handling by @stonerl in https://github.com/yattee/yattee/pull/749
|
||||
* Refined chapter font scaling by @stonerl in https://github.com/yattee/yattee/pull/750
|
||||
* Improved thumbnail handling by @stonerl in https://github.com/yattee/yattee/pull/740
|
||||
* iOS: make timestamps in comments touchable by @stonerl in https://github.com/yattee/yattee/pull/741
|
||||
* Improvements to opening channels from Videos by @stonerl in https://github.com/yattee/yattee/pull/742
|
||||
|
||||
16
Gemfile.lock
16
Gemfile.lock
@@ -10,17 +10,17 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.968.0)
|
||||
aws-sdk-core (3.201.5)
|
||||
aws-partitions (1.970.0)
|
||||
aws-sdk-core (3.203.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.88.0)
|
||||
aws-sdk-core (~> 3, >= 3.201.0)
|
||||
aws-sdk-kms (1.89.0)
|
||||
aws-sdk-core (~> 3, >= 3.203.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.159.0)
|
||||
aws-sdk-core (~> 3, >= 3.201.0)
|
||||
aws-sdk-s3 (1.160.0)
|
||||
aws-sdk-core (~> 3, >= 3.203.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.9.1)
|
||||
@@ -171,8 +171,7 @@ GEM
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.3.6)
|
||||
strscan
|
||||
rexml (3.3.7)
|
||||
rouge (2.0.7)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
@@ -185,7 +184,6 @@ GEM
|
||||
simctl (1.6.10)
|
||||
CFPropertyList
|
||||
naturally
|
||||
strscan (3.1.0)
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
|
||||
@@ -10,11 +10,28 @@ struct AccountsBridge: Defaults.Bridge {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse the urlString to check for embedded username and password
|
||||
var sanitizedUrlString = value.urlString
|
||||
if var urlComponents = URLComponents(string: value.urlString) {
|
||||
if let user = urlComponents.user, let password = urlComponents.password {
|
||||
// Sanitize the embedded username and password
|
||||
let sanitizedUser = user.addingPercentEncoding(withAllowedCharacters: .urlUserAllowed) ?? user
|
||||
let sanitizedPassword = password.addingPercentEncoding(withAllowedCharacters: .urlPasswordAllowed) ?? password
|
||||
|
||||
// Update the URL components with sanitized credentials
|
||||
urlComponents.user = sanitizedUser
|
||||
urlComponents.password = sanitizedPassword
|
||||
|
||||
// Reconstruct the sanitized URL
|
||||
sanitizedUrlString = urlComponents.string ?? value.urlString
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
"id": value.id,
|
||||
"instanceID": value.instanceID ?? "",
|
||||
"name": value.name,
|
||||
"apiURL": value.urlString,
|
||||
"apiURL": sanitizedUrlString,
|
||||
"username": value.username,
|
||||
"password": value.password ?? ""
|
||||
]
|
||||
|
||||
@@ -247,27 +247,27 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
}
|
||||
|
||||
func feed(_ page: Int?) -> Resource? {
|
||||
resource(baseURL: account.url, path: "\(Self.basePath)/auth/feed")
|
||||
resourceWithAuthCheck(baseURL: account.url, path: "\(Self.basePath)/auth/feed")
|
||||
.withParam("page", String(page ?? 1))
|
||||
}
|
||||
|
||||
var feed: Resource? {
|
||||
resource(baseURL: account.url, path: basePathAppending("auth/feed"))
|
||||
resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/feed"))
|
||||
}
|
||||
|
||||
var subscriptions: Resource? {
|
||||
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
|
||||
resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
|
||||
}
|
||||
|
||||
func subscribe(_ channelID: String, onCompletion: @escaping () -> Void = {}) {
|
||||
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
|
||||
resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
|
||||
.child(channelID)
|
||||
.request(.post)
|
||||
.onCompletion { _ in onCompletion() }
|
||||
}
|
||||
|
||||
func unsubscribe(_ channelID: String, onCompletion: @escaping () -> Void) {
|
||||
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
|
||||
resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
|
||||
.child(channelID)
|
||||
.request(.delete)
|
||||
.onCompletion { _ in onCompletion() }
|
||||
@@ -308,11 +308,11 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
return nil
|
||||
}
|
||||
|
||||
return resource(baseURL: account.url, path: basePathAppending("auth/playlists"))
|
||||
return resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/playlists"))
|
||||
}
|
||||
|
||||
func playlist(_ id: String) -> Resource? {
|
||||
resource(baseURL: account.url, path: basePathAppending("auth/playlists/\(id)"))
|
||||
resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/playlists/\(id)"))
|
||||
}
|
||||
|
||||
func playlistVideos(_ id: String) -> Resource? {
|
||||
@@ -445,6 +445,9 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
|
||||
urlComponents.scheme = instanceURLComponents.scheme
|
||||
urlComponents.host = instanceURLComponents.host
|
||||
urlComponents.user = instanceURLComponents.user
|
||||
urlComponents.password = instanceURLComponents.password
|
||||
urlComponents.port = instanceURLComponents.port
|
||||
|
||||
guard let url = urlComponents.url else {
|
||||
return nil
|
||||
@@ -553,6 +556,30 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
)
|
||||
}
|
||||
|
||||
// Determines if the request requires Basic Auth credentials to be removed
|
||||
private func needsBasicAuthRemoval(for path: String) -> Bool {
|
||||
return path.hasPrefix("\(Self.basePath)/auth/")
|
||||
}
|
||||
|
||||
// Creates a resource URL with consideration for removing Basic Auth credentials
|
||||
private func createResourceURL(baseURL: URL, path: String) -> URL {
|
||||
var resourceURL = baseURL
|
||||
|
||||
// Remove Basic Auth credentials if required
|
||||
if needsBasicAuthRemoval(for: path), var urlComponents = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) {
|
||||
urlComponents.user = nil
|
||||
urlComponents.password = nil
|
||||
resourceURL = urlComponents.url ?? baseURL
|
||||
}
|
||||
|
||||
return resourceURL.appendingPathComponent(path)
|
||||
}
|
||||
|
||||
func resourceWithAuthCheck(baseURL: URL, path: String) -> Resource {
|
||||
let sanitizedURL = createResourceURL(baseURL: baseURL, path: path)
|
||||
return super.resource(absoluteURL: sanitizedURL)
|
||||
}
|
||||
|
||||
private func extractThumbnails(from details: JSON) -> [Thumbnail] {
|
||||
details["videoThumbnails"].arrayValue.compactMap { json in
|
||||
guard let url = json["url"].url,
|
||||
@@ -563,13 +590,20 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
return nil
|
||||
}
|
||||
|
||||
// some of instances are not configured properly and return thumbnails links
|
||||
// with incorrect scheme
|
||||
// Some instances are not configured properly and return thumbnail links
|
||||
// with an incorrect scheme or a missing port.
|
||||
components.scheme = accountUrlComponents.scheme
|
||||
components.port = accountUrlComponents.port
|
||||
|
||||
// If basic HTTP authentication is used,
|
||||
// the username and password need to be prepended to the URL.
|
||||
components.user = accountUrlComponents.user
|
||||
components.password = accountUrlComponents.password
|
||||
|
||||
guard let thumbnailUrl = components.url else {
|
||||
return nil
|
||||
}
|
||||
print("Final thumbnail URL: \(thumbnailUrl)")
|
||||
|
||||
return Thumbnail(url: thumbnailUrl, quality: .init(rawValue: quality)!)
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
|
||||
private var frequentTimeObserver: Any?
|
||||
private var infrequentTimeObserver: Any?
|
||||
private var playerTimeControlStatusObserver: Any?
|
||||
private var playerTimeControlStatusObserver: NSKeyValueObservation?
|
||||
|
||||
private var statusObservation: NSKeyValueObservation?
|
||||
|
||||
@@ -119,10 +119,30 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
#if os(iOS)
|
||||
controller.player = avPlayer
|
||||
#endif
|
||||
logger.info("AVPlayerBackend initialized.")
|
||||
}
|
||||
|
||||
deinit {
|
||||
// Invalidate any observers to avoid memory leaks
|
||||
statusObservation?.invalidate()
|
||||
playerTimeControlStatusObserver?.invalidate()
|
||||
|
||||
// Remove any time observers added to AVPlayer
|
||||
if let frequentObserver = frequentTimeObserver {
|
||||
avPlayer.removeTimeObserver(frequentObserver)
|
||||
}
|
||||
if let infrequentObserver = infrequentTimeObserver {
|
||||
avPlayer.removeTimeObserver(infrequentObserver)
|
||||
}
|
||||
|
||||
// Remove notification observers
|
||||
removeItemDidPlayToEndTimeObserver()
|
||||
|
||||
logger.info("AVPlayerBackend deinitialized.")
|
||||
}
|
||||
|
||||
func canPlay(_ stream: Stream) -> Bool {
|
||||
stream.kind == .hls || stream.kind == .stream || (stream.kind == .adaptive && stream.format == .mp4)
|
||||
stream.kind == .hls || stream.kind == .stream
|
||||
}
|
||||
|
||||
func playStream(
|
||||
@@ -344,7 +364,11 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
|
||||
let startPlaying = {
|
||||
#if !os(macOS)
|
||||
try? AVAudioSession.sharedInstance().setActive(true)
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setActive(true)
|
||||
} catch {
|
||||
self.logger.error("Error setting up audio session: \(error)")
|
||||
}
|
||||
#endif
|
||||
|
||||
self.setRate(self.model.currentRate)
|
||||
@@ -779,7 +803,7 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
opened = true
|
||||
controller.startPictureInPicture()
|
||||
} else {
|
||||
print("PiP not possible, waited \(delay) seconds")
|
||||
self.logger.info("PiP not possible, waited \(delay) seconds")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import AVFAudio
|
||||
import CoreMedia
|
||||
import Defaults
|
||||
import Foundation
|
||||
import Libmpv
|
||||
import Logging
|
||||
import MediaPlayer
|
||||
import MPVKit
|
||||
import Repeat
|
||||
import SwiftUI
|
||||
|
||||
@@ -248,13 +248,6 @@ final class MPVBackend: PlayerBackend {
|
||||
#if !os(macOS)
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setActive(true)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(self.handleAudioSessionInterruption(_:)),
|
||||
name: AVAudioSession.interruptionNotification,
|
||||
object: nil
|
||||
)
|
||||
} catch {
|
||||
self.logger.error("Error setting up audio session: \(error)")
|
||||
}
|
||||
@@ -649,33 +642,4 @@ final class MPVBackend: PlayerBackend {
|
||||
logger.info("MPV backend received unhandled property: \(name)")
|
||||
}
|
||||
}
|
||||
|
||||
#if !os(macOS)
|
||||
@objc func handleAudioSessionInterruption(_ notification: Notification) {
|
||||
logger.info("Audio session interruption received.")
|
||||
|
||||
guard let info = notification.userInfo,
|
||||
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt
|
||||
else {
|
||||
logger.info("AVAudioSessionInterruptionTypeKey is missing or not a UInt in userInfo.")
|
||||
return
|
||||
}
|
||||
|
||||
let type = AVAudioSession.InterruptionType(rawValue: typeValue)
|
||||
|
||||
logger.info("Interruption type received: \(String(describing: type))")
|
||||
|
||||
switch type {
|
||||
case .began:
|
||||
pause()
|
||||
logger.info("Audio session interrupted.")
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self, name: AVAudioSession.interruptionNotification, object: nil)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import CoreMedia
|
||||
import Defaults
|
||||
import Foundation
|
||||
import Libmpv
|
||||
import Logging
|
||||
import MPVKit
|
||||
#if !os(macOS)
|
||||
import Siesta
|
||||
import UIKit
|
||||
@@ -99,6 +99,11 @@ final class MPVClient: ObservableObject {
|
||||
checkError(mpv_set_option_string(mpv, "demuxer-lavf-analyzeduration", "1"))
|
||||
checkError(mpv_set_option_string(mpv, "demuxer-lavf-probe-info", Defaults[.mpvDemuxerLavfProbeInfo]))
|
||||
|
||||
// Disable ytdl, since it causes crashes on macOS.
|
||||
#if os(macOS)
|
||||
checkError(mpv_set_option_string(mpv, "ytdl", "no"))
|
||||
#endif
|
||||
|
||||
checkError(mpv_initialize(mpv))
|
||||
|
||||
let api = UnsafeMutableRawPointer(mutating: (MPV_RENDER_API_TYPE_OPENGL as NSString).utf8String)
|
||||
|
||||
@@ -153,7 +153,8 @@ extension PlayerBackend {
|
||||
// Filter out non-HLS streams and streams with resolution more than maxResolution
|
||||
let nonHLSStreams = streams.filter {
|
||||
let isHLS = $0.kind == .hls
|
||||
let isWithinResolution = $0.resolution <= maxResolution.value
|
||||
// Safely unwrap resolution and maxResolution.value to avoid crashes
|
||||
let isWithinResolution = ($0.resolution != nil && maxResolution.value != nil) ? $0.resolution! <= maxResolution.value! : false
|
||||
logger.info("Stream ID: \($0.id) - Kind: \(String(describing: $0.kind)) - Resolution: \(String(describing: $0.resolution)) - Bitrate: \($0.bitrate ?? 0)")
|
||||
logger.info("Is HLS: \(isHLS), Is within resolution: \(isWithinResolution)")
|
||||
return !isHLS && isWithinResolution
|
||||
@@ -187,7 +188,8 @@ extension PlayerBackend {
|
||||
}
|
||||
|
||||
let filteredStreams = adjustedStreams.filter { stream in
|
||||
let isWithinResolution = stream.resolution <= maxResolution.value
|
||||
// Safely unwrap resolution and maxResolution.value to avoid crashes
|
||||
let isWithinResolution = (stream.resolution != nil && maxResolution.value != nil) ? stream.resolution! <= maxResolution.value! : false
|
||||
logger.info("Filtered stream ID: \(stream.id) - Is within max resolution: \(isWithinResolution)")
|
||||
return isWithinResolution
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ final class PlayerModel: ObservableObject {
|
||||
|
||||
static var shared = PlayerModel()
|
||||
|
||||
let logger = Logger(label: "stream.yattee.app")
|
||||
let logger = Logger(label: "stream.yattee.player.model")
|
||||
|
||||
var playerItem: AVPlayerItem?
|
||||
|
||||
@@ -56,6 +56,7 @@ final class PlayerModel: ObservableObject {
|
||||
@Published var presentingPlayer = false { didSet { handlePresentationChange() } }
|
||||
@Published var activeBackend = PlayerBackendType.mpv
|
||||
@Published var forceBackendOnPlay: PlayerBackendType?
|
||||
@Published var wasFullscreen = false
|
||||
|
||||
var avPlayerBackend = AVPlayerBackend()
|
||||
var mpvBackend = MPVBackend()
|
||||
@@ -203,6 +204,14 @@ final class PlayerModel: ObservableObject {
|
||||
#if !os(macOS)
|
||||
mpvBackend.controller = mpvController
|
||||
mpvBackend.client = mpvController.client
|
||||
|
||||
// Register for audio session interruption notifications
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleAudioSessionInterruption(_:)),
|
||||
name: AVAudioSession.interruptionNotification,
|
||||
object: nil
|
||||
)
|
||||
#endif
|
||||
|
||||
playbackMode = Defaults[.playbackMode]
|
||||
@@ -219,6 +228,12 @@ final class PlayerModel: ObservableObject {
|
||||
currentRate = playerRate
|
||||
}
|
||||
|
||||
#if !os(macOS)
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self, name: AVAudioSession.interruptionNotification, object: nil)
|
||||
}
|
||||
#endif
|
||||
|
||||
func show() {
|
||||
#if os(macOS)
|
||||
if presentingPlayer {
|
||||
@@ -678,38 +693,24 @@ final class PlayerModel: ObservableObject {
|
||||
avPlayerBackend.startPictureInPictureOnPlay = false
|
||||
avPlayerBackend.startPictureInPictureOnSwitch = false
|
||||
|
||||
if activeBackend == .appleAVPlayer {
|
||||
guard activeBackend != .appleAVPlayer else {
|
||||
avPlayerBackend.tryStartingPictureInPicture()
|
||||
return
|
||||
}
|
||||
|
||||
// First, we need to create an array with supported formats.
|
||||
let formatOrderPiP: [QualityProfile.Format] = [.hls, .stream, .mp4]
|
||||
avPlayerBackend.startPictureInPictureOnSwitch = true
|
||||
|
||||
guard let video = currentVideo else { return }
|
||||
guard let stream = avPlayerBackend.bestPlayable(availableStreams, maxResolution: .hd720p30, formatOrder: formatOrderPiP) else { return }
|
||||
|
||||
if avPlayerBackend.video == video {
|
||||
if activeBackend != .appleAVPlayer {
|
||||
avPlayerBackend.startPictureInPictureOnSwitch = true
|
||||
}
|
||||
changeActiveBackend(from: activeBackend, to: .appleAVPlayer)
|
||||
} else {
|
||||
avPlayerBackend.startPictureInPictureOnPlay = true
|
||||
playStream(stream, of: video, preservingTime: true, upgrading: true, withBackend: avPlayerBackend)
|
||||
}
|
||||
|
||||
var retryCount = 0
|
||||
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
|
||||
if let pipController = self?.pipController, pipController.isPictureInPictureActive, self?.avPlayerBackend.isPlaying == true {
|
||||
self?.exitFullScreen()
|
||||
self?.controls.objectWillChange.send()
|
||||
timer.invalidate()
|
||||
} else if retryCount < 3, self?.activeBackend == .appleAVPlayer, self?.avPlayerBackend.startPictureInPictureOnSwitch == false {
|
||||
// If PiP didn't start, try starting it again up to 3 times,
|
||||
self?.avPlayerBackend.startPictureInPictureOnSwitch = true
|
||||
self?.avPlayerBackend.tryStartingPictureInPicture()
|
||||
retryCount += 1
|
||||
saveTime {
|
||||
self.changeActiveBackend(from: .mpv, to: .appleAVPlayer)
|
||||
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
|
||||
if let pipController = self?.pipController, pipController.isPictureInPictureActive, self?.avPlayerBackend.isPlaying == true {
|
||||
self?.exitFullScreen()
|
||||
self?.controls.objectWillChange.send()
|
||||
timer.invalidate()
|
||||
} else if self?.activeBackend == .appleAVPlayer, self?.avPlayerBackend.startPictureInPictureOnSwitch == false {
|
||||
self?.avPlayerBackend.startPictureInPictureOnSwitch = true
|
||||
self?.avPlayerBackend.tryStartingPictureInPicture()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -739,19 +740,27 @@ final class PlayerModel: ObservableObject {
|
||||
show()
|
||||
#endif
|
||||
|
||||
if previousActiveBackend == .mpv {
|
||||
saveTime {
|
||||
self.changeActiveBackend(from: self.activeBackend, to: .mpv, isInClosePip: true)
|
||||
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
|
||||
if self?.activeBackend == .mpv, self?.mpvBackend.isPlaying == true {
|
||||
self?.backend.closePiP()
|
||||
self?.controls.resetTimer()
|
||||
timer.invalidate()
|
||||
}
|
||||
avPlayerBackend.closePiP()
|
||||
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
|
||||
if self?.activeBackend == .appleAVPlayer, self?.avPlayerBackend.isPlaying == true, self?.playingInPictureInPicture == false {
|
||||
timer.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
guard previousActiveBackend == .mpv else { return }
|
||||
|
||||
saveTime {
|
||||
self.changeActiveBackend(from: .appleAVPlayer, to: .mpv, isInClosePip: true)
|
||||
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
|
||||
if self?.activeBackend == .mpv, self?.mpvBackend.isPlaying == true {
|
||||
timer.invalidate()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
backend.closePiP()
|
||||
}
|
||||
|
||||
// We need to remove the itme from the player, if not it will be displayed when next video goe to PiP.
|
||||
Delay.by(1.0) {
|
||||
self.avPlayerBackend.closeItem()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -880,26 +889,29 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
|
||||
func updateRemoteCommandCenter() {
|
||||
let skipForwardCommand = MPRemoteCommandCenter.shared().skipForwardCommand
|
||||
let skipBackwardCommand = MPRemoteCommandCenter.shared().skipBackwardCommand
|
||||
let previousTrackCommand = MPRemoteCommandCenter.shared().previousTrackCommand
|
||||
let nextTrackCommand = MPRemoteCommandCenter.shared().nextTrackCommand
|
||||
let commandCenter = MPRemoteCommandCenter.shared()
|
||||
let skipForwardCommand = commandCenter.skipForwardCommand
|
||||
let skipBackwardCommand = commandCenter.skipBackwardCommand
|
||||
let previousTrackCommand = commandCenter.previousTrackCommand
|
||||
let nextTrackCommand = commandCenter.nextTrackCommand
|
||||
|
||||
if !remoteCommandCenterConfigured {
|
||||
remoteCommandCenterConfigured = true
|
||||
|
||||
#if !os(macOS)
|
||||
try? AVAudioSession.sharedInstance().setCategory(
|
||||
.playback,
|
||||
mode: .moviePlayback
|
||||
)
|
||||
|
||||
UIApplication.shared.beginReceivingRemoteControlEvents()
|
||||
#endif
|
||||
|
||||
let interval = TimeInterval(systemControlsSeekDuration) ?? 10
|
||||
let preferredIntervals = [NSNumber(value: interval)]
|
||||
|
||||
// Remove existing targets to avoid duplicates
|
||||
skipForwardCommand.removeTarget(nil)
|
||||
skipBackwardCommand.removeTarget(nil)
|
||||
previousTrackCommand.removeTarget(nil)
|
||||
nextTrackCommand.removeTarget(nil)
|
||||
commandCenter.playCommand.removeTarget(nil)
|
||||
commandCenter.pauseCommand.removeTarget(nil)
|
||||
commandCenter.togglePlayPauseCommand.removeTarget(nil)
|
||||
commandCenter.changePlaybackPositionCommand.removeTarget(nil)
|
||||
|
||||
// Re-add targets for handling commands
|
||||
skipForwardCommand.preferredIntervals = preferredIntervals
|
||||
skipBackwardCommand.preferredIntervals = preferredIntervals
|
||||
|
||||
@@ -923,22 +935,22 @@ final class PlayerModel: ObservableObject {
|
||||
return .success
|
||||
}
|
||||
|
||||
MPRemoteCommandCenter.shared().playCommand.addTarget { [weak self] _ in
|
||||
commandCenter.playCommand.addTarget { [weak self] _ in
|
||||
self?.play()
|
||||
return .success
|
||||
}
|
||||
|
||||
MPRemoteCommandCenter.shared().pauseCommand.addTarget { [weak self] _ in
|
||||
commandCenter.pauseCommand.addTarget { [weak self] _ in
|
||||
self?.pause()
|
||||
return .success
|
||||
}
|
||||
|
||||
MPRemoteCommandCenter.shared().togglePlayPauseCommand.addTarget { [weak self] _ in
|
||||
commandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in
|
||||
self?.togglePlay()
|
||||
return .success
|
||||
}
|
||||
|
||||
MPRemoteCommandCenter.shared().changePlaybackPositionCommand.addTarget { [weak self] remoteEvent in
|
||||
commandCenter.changePlaybackPositionCommand.addTarget { [weak self] remoteEvent in
|
||||
guard let event = remoteEvent as? MPChangePlaybackPositionCommandEvent else { return .commandFailed }
|
||||
|
||||
self?.backend.seek(to: event.positionTime, seekType: .userInteracted)
|
||||
@@ -975,24 +987,53 @@ final class PlayerModel: ObservableObject {
|
||||
func handleEnterForeground() {
|
||||
setNeedsDrawing(presentingPlayer)
|
||||
|
||||
if !musicMode, activeBackend == .appleAVPlayer {
|
||||
if !musicMode, activeBackend == .mpv {
|
||||
mpvBackend.addVideoTrackFromStream()
|
||||
mpvBackend.setVideoToAuto()
|
||||
mpvBackend.controls.resetTimer()
|
||||
} else if !musicMode, activeBackend == .appleAVPlayer {
|
||||
avPlayerBackend.bindPlayerToLayer()
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
if wasFullscreen {
|
||||
wasFullscreen = false
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
Delay.by(0.3) {
|
||||
self?.enterFullScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
guard closePiPAndOpenPlayerOnEnteringForeground, playingInPictureInPicture else {
|
||||
return
|
||||
}
|
||||
|
||||
show()
|
||||
closePiP()
|
||||
// Needs to be delayed a bit, otherwise the PiP windows stays open
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||
self?.closePiP()
|
||||
}
|
||||
}
|
||||
|
||||
func handleEnterBackground() {
|
||||
if Defaults[.pauseOnEnteringBackground], !playingInPictureInPicture, !musicMode {
|
||||
pause()
|
||||
} else if !playingInPictureInPicture {
|
||||
} else if !playingInPictureInPicture, activeBackend == .appleAVPlayer {
|
||||
avPlayerBackend.removePlayerFromLayer()
|
||||
} else if activeBackend == .mpv, !musicMode {
|
||||
mpvBackend.setVideoToNo()
|
||||
}
|
||||
#if os(iOS)
|
||||
guard playingFullScreen else { return }
|
||||
wasFullscreen = playingFullScreen
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
Delay.by(0.3) {
|
||||
self?.exitFullScreen(showControls: false)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1017,18 +1058,22 @@ final class PlayerModel: ObservableObject {
|
||||
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
|
||||
}
|
||||
|
||||
let currentTime = (backend.currentTime?.seconds.isFinite ?? false) ? backend.currentTime!.seconds : 0
|
||||
|
||||
// Determine the media type based on musicMode
|
||||
let mediaType: NSNumber
|
||||
if musicMode {
|
||||
mediaType = MPMediaType.anyAudio.rawValue as NSNumber
|
||||
} else {
|
||||
mediaType = MPMediaType.anyVideo.rawValue as NSNumber
|
||||
}
|
||||
|
||||
// Prepare the Now Playing info dictionary
|
||||
var nowPlayingInfo: [String: AnyObject] = [
|
||||
MPMediaItemPropertyTitle: video.displayTitle as AnyObject,
|
||||
MPMediaItemPropertyArtist: video.displayAuthor as AnyObject,
|
||||
@@ -1036,7 +1081,7 @@ final class PlayerModel: ObservableObject {
|
||||
MPNowPlayingInfoPropertyElapsedPlaybackTime: currentTime as AnyObject,
|
||||
MPNowPlayingInfoPropertyPlaybackQueueCount: queue.count as AnyObject,
|
||||
MPNowPlayingInfoPropertyPlaybackQueueIndex: 1 as AnyObject,
|
||||
MPMediaItemPropertyMediaType: MPMediaType.anyVideo.rawValue as AnyObject
|
||||
MPMediaItemPropertyMediaType: mediaType
|
||||
]
|
||||
|
||||
if !currentArtwork.isNil {
|
||||
@@ -1057,7 +1102,7 @@ final class PlayerModel: ObservableObject {
|
||||
|
||||
func updateCurrentArtwork() {
|
||||
guard let video = currentVideo,
|
||||
let thumbnailURL = video.thumbnailURL(quality: .medium)
|
||||
let thumbnailURL = video.thumbnailURL(quality: Constants.isIPhone ? .medium : .maxres)
|
||||
else {
|
||||
return
|
||||
}
|
||||
@@ -1203,6 +1248,42 @@ final class PlayerModel: ObservableObject {
|
||||
return nil
|
||||
}
|
||||
|
||||
#if !os(macOS)
|
||||
@objc func handleAudioSessionInterruption(_ notification: Notification) {
|
||||
logger.info("Audio session interruption received.")
|
||||
logger.info("Notification received: \(notification)")
|
||||
|
||||
guard let info = notification.userInfo,
|
||||
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
|
||||
let type = AVAudioSession.InterruptionType(rawValue: typeValue)
|
||||
else {
|
||||
logger.info("AVAudioSessionInterruptionTypeKey is missing or not a UInt in userInfo.")
|
||||
return
|
||||
}
|
||||
|
||||
logger.info("Interruption type received: \(type)")
|
||||
|
||||
switch type {
|
||||
case .began:
|
||||
logger.info("Audio session interrupted.")
|
||||
// We need to call pause() to set all variables correctly, and play()
|
||||
// directly afterwards, because the .began interrupt is sent after audio
|
||||
// ducking ended and playback would pause. Audio ducking usually happens
|
||||
// when using headphones.
|
||||
pause()
|
||||
play()
|
||||
case .ended:
|
||||
logger.info("Audio session interruption ended.")
|
||||
// We need to call pause() to set all variables correctly.
|
||||
// Otherwise, playback does not resume when the interruption ends.
|
||||
pause()
|
||||
play()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
private func assignKeyPressMonitor() {
|
||||
keyPressMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { keyEvent -> NSEvent? in
|
||||
@@ -1240,7 +1321,7 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
|
||||
private func destroyKeyPressMonitor() {
|
||||
if let keyPressMonitor = keyPressMonitor {
|
||||
if let keyPressMonitor {
|
||||
NSEvent.removeMonitor(keyPressMonitor)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@ struct QualityProfile: Hashable, Identifiable, Defaults.Serializable {
|
||||
static var defaultProfile = Self(id: "default", backend: .mpv, resolution: .hd720p60, formats: [.stream], order: Array(Format.allCases.indices))
|
||||
|
||||
enum Format: String, CaseIterable, Identifiable, Defaults.Serializable {
|
||||
case hls
|
||||
case stream
|
||||
case avc1
|
||||
case stream
|
||||
case webm
|
||||
case mp4
|
||||
case av1
|
||||
case webm
|
||||
case hls
|
||||
|
||||
var id: String {
|
||||
rawValue
|
||||
@@ -30,18 +30,18 @@ struct QualityProfile: Hashable, Identifiable, Defaults.Serializable {
|
||||
|
||||
var streamFormat: Stream.Format? {
|
||||
switch self {
|
||||
case .hls:
|
||||
return nil
|
||||
case .stream:
|
||||
return nil
|
||||
case .avc1:
|
||||
return .avc1
|
||||
case .stream:
|
||||
return nil
|
||||
case .webm:
|
||||
return .webm
|
||||
case .mp4:
|
||||
return .mp4
|
||||
case .av1:
|
||||
return .av1
|
||||
case .webm:
|
||||
return .webm
|
||||
case .hls:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,14 +59,16 @@ struct QualityProfile: Hashable, Identifiable, Defaults.Serializable {
|
||||
}
|
||||
|
||||
var formatsDescription: String {
|
||||
if formats.count == Format.allCases.count {
|
||||
switch formats.count {
|
||||
case Format.allCases.count:
|
||||
return "Any format".localized()
|
||||
}
|
||||
if formats.count <= 3 {
|
||||
case 0:
|
||||
return "No format selected".localized()
|
||||
case 1 ... 3:
|
||||
return formats.map(\.description).joined(separator: ", ")
|
||||
default:
|
||||
return String(format: "%@ formats".localized(), String(formats.count))
|
||||
}
|
||||
|
||||
return String(format: "%@ formats".localized(), String(formats.count))
|
||||
}
|
||||
|
||||
func isPreferred(_ stream: Stream) -> Bool {
|
||||
|
||||
@@ -5,10 +5,27 @@ final class ThumbnailsModel: ObservableObject {
|
||||
static var shared = ThumbnailsModel()
|
||||
|
||||
@Published var unloadable = Set<URL>()
|
||||
private var retryCounts = [URL: Int]()
|
||||
private let maxRetries = 3
|
||||
private let retryDelay: TimeInterval = 1.0
|
||||
|
||||
func insertUnloadable(_ url: URL) {
|
||||
DispatchQueue.main.async {
|
||||
self.unloadable.insert(url)
|
||||
let retries = (retryCounts[url] ?? 0) + 1
|
||||
|
||||
if retries >= maxRetries {
|
||||
DispatchQueue.main.async {
|
||||
self.unloadable.insert(url)
|
||||
self.retryCounts.removeValue(forKey: url)
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.retryCounts[url] = retries
|
||||
}
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + retryDelay) {
|
||||
DispatchQueue.main.async {
|
||||
self.retryCounts[url] = retries
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Invidious.svg",
|
||||
"filename" : "Invidious_512x512@1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Invidious_512x512@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Invidious_512x512@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="512pt" height="512pt" version="1.0" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><g><rect x="-.0072516" y=".00056299" width="512.01" height="512.02" fill="#575757" stroke-width=".063019"/><path d="m247.17 455.95c-19.792-0.78921-38.719-4.2564-57.154-10.47-60.968-20.55-108.68-68.579-127-127.86-7.8955-25.538-10.062-53.943-6.2586-82.067 3.7105-27.439 13.603-53.515 29.342-77.344 12.069-18.273 29.138-36.277 47.228-49.816 36.891-27.61 85.944-42.49 132.38-40.157 25.88 1.3001 49.939 6.765 73.106 16.606 8.1948 3.481 20.024 9.6845 27.696 14.525 14.15 8.9272 22.367 15.498 34.482 27.573 13.254 13.211 22.128 24.276 30.398 37.906 7.2081 11.879 14.099 27.15 18.229 40.397 1.5996 5.1305 4.442 16.456 5.6852 22.653 2.3908 11.917 2.6998 15.722 2.7049 33.312 6e-3 18.515-0.46256 24.413-2.9166 36.758-9.3274 46.92-35.58 88.167-74.872 117.64-22.814 17.112-50.027 29.535-78.547 35.858-16.714 3.7059-35.421 5.2453-54.498 4.4846zm-35.1-78.786c-5.3e-4 -0.52647-0.0741-2.0564-0.16311-3.3999l-0.16178-2.4427-4.7018-0.26271c-4.0477-0.22614-4.7968-0.33363-5.3847-0.77253-2.0235-1.5108-1.4679-6.0695 2.2494-18.457 0.8637-2.8781 3.3371-11.321 5.4966-18.762 2.1594-7.4409 5.2002-17.836 6.7573-23.101 1.5571-5.2648 4.1948-14.282 5.8615-20.038 1.6667-5.7562 3.6145-12.4 4.3284-14.764 0.71391-2.3641 3.2583-11.037 5.6542-19.272 4.9475-17.007 8.1626-27.723 8.9438-29.811 0.51852-1.3858 0.54785-1.4139 0.99761-0.95317 0.25486 0.26106 3.8462 7.3667 7.9807 15.79 4.1345 8.4236 13.089 26.573 19.898 40.331 17.188 34.73 37.849 76.578 43.261 87.622l4.5356 9.257 11.359-0.0895c6.2475-0.0492 11.615-0.19623 11.929-0.32672 0.5614-0.23385 0.54167-0.2959-1.3723-4.3176-1.068-2.2442-8.1436-16.601-15.724-31.904-48.687-98.293-61.22-123.86-67.889-138.48-4.7022-10.309-6.9031-14.807-7.7139-15.762-0.82931-0.97742-1.6319-1.0638-2.3704-0.25525-1.1993 1.313-4.1046 10.063-9.3869 28.27-2.0569 7.0899-6.5372 22.425-9.9562 34.077-6.6396 22.629-8.5182 29.037-14.33 48.883-2.0354 6.9495-4.7977 16.369-6.1385 20.931-1.3408 4.5628-4.033 13.81-5.9826 20.549-4.304 14.877-6.136 20.889-7.3886 24.25-2.1371 5.7334-2.5723 6.3292-4.9216 6.7384-0.88855 0.15472-2.4102 0.28196-3.3815 0.28275-2.1993 3e-3 -3.5494 0.36339-4.0558 1.0863-0.42176 0.60215-0.56421 4.8802-0.18251 5.4812 0.20573 0.32388 2.4672 0.37414 23.34 0.51873l8.6151 0.0597-7e-4 -0.95723zm36.751-205.59c4.3282-0.92335 8.4607-4.943 9.4374-9.1796 0.36569-1.5862 0.32543-4.9758-0.077-6.4799-0.85108-3.1813-3.2688-6.291-6.039-7.7675-3.8111-2.0313-9.456-2.0295-13.272 5e-3 -5.9828 3.1888-8.1556 11.089-4.7878 17.408 2.6995 5.0648 8.3611 7.3754 14.738 6.015z" fill="#f0f0f0" stroke-width=".025526"/></g><g transform="matrix(.069892 0 0 -.069892 44.236 474.48)"><path d="m2787 4669c-124-65-123-255 3-319 86-44 196-16 247 62 58 87 26 211-67 258-51 26-132 26-183-1z" fill="#00b6f0" stroke="#00b6f0" stroke-width="4.25"/><path d="m2882 4108c-12-16-63-166-102-303-30-104-101-350-165-565-20-69-58-199-85-290-26-91-64-221-85-290-20-69-58-199-85-290-26-91-64-221-85-290-20-69-57-195-81-280-59-207-93-299-115-310-10-6-35-10-56-10-73 0-84-8-81-54l3-41 228-3 228-2-3 47-3 48-73 3c-66 3-74 5-84 27-13 28 0 104 37 225 13 41 47 156 75 255s66 230 85 290c18 61 56 191 85 290 28 99 66 230 85 290 18 61 56 191 85 290 85 297 123 419 131 429 5 5 17-11 28-35 10-24 192-393 403-819s447-902 523-1058l139-282h168c92 0 168 4 168 8s-75 158-166 342c-588 1183-969 1958-1033 2100-29 63-69 151-89 195-44 95-58 110-80 83z" fill="#575757"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 3.4 KiB |
BIN
Shared/Assets.xcassets/Invidious.imageset/Invidious_512x512@1x.png
vendored
Normal file
BIN
Shared/Assets.xcassets/Invidious.imageset/Invidious_512x512@1x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
BIN
Shared/Assets.xcassets/Invidious.imageset/Invidious_512x512@2x.png
vendored
Normal file
BIN
Shared/Assets.xcassets/Invidious.imageset/Invidious_512x512@2x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 85 KiB |
BIN
Shared/Assets.xcassets/Invidious.imageset/Invidious_512x512@3x.png
vendored
Normal file
BIN
Shared/Assets.xcassets/Invidious.imageset/Invidious_512x512@3x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
@@ -39,6 +39,30 @@ enum Constants {
|
||||
#endif
|
||||
}
|
||||
|
||||
static var isTvOS: Bool {
|
||||
#if os(tvOS)
|
||||
true
|
||||
#else
|
||||
false
|
||||
#endif
|
||||
}
|
||||
|
||||
static var isMacOS: Bool {
|
||||
#if os(macOS)
|
||||
true
|
||||
#else
|
||||
false
|
||||
#endif
|
||||
}
|
||||
|
||||
static var isIOS: Bool {
|
||||
#if os(iOS)
|
||||
true
|
||||
#else
|
||||
false
|
||||
#endif
|
||||
}
|
||||
|
||||
static var progressViewScale: Double {
|
||||
#if os(macOS)
|
||||
0.4
|
||||
|
||||
@@ -20,14 +20,14 @@ extension Defaults.Keys {
|
||||
static let showOpenActionsToolbarItem = Key<Bool>("showOpenActionsToolbarItem", default: false)
|
||||
#if os(iOS)
|
||||
static let showDocuments = Key<Bool>("showDocuments", default: false)
|
||||
static let lockPortraitWhenBrowsing = Key<Bool>("lockPortraitWhenBrowsing", default: UIDevice.current.userInterfaceIdiom == .phone)
|
||||
static let lockPortraitWhenBrowsing = Key<Bool>("lockPortraitWhenBrowsing", default: Constants.isIPhone)
|
||||
#endif
|
||||
|
||||
#if !os(tvOS)
|
||||
#if os(macOS)
|
||||
static let accountPickerDisplaysUsernameDefault = true
|
||||
#else
|
||||
static let accountPickerDisplaysUsernameDefault = UIDevice.current.userInterfaceIdiom == .pad
|
||||
static let accountPickerDisplaysUsernameDefault = Constants.isIPad
|
||||
#endif
|
||||
static let accountPickerDisplaysUsername = Key<Bool>("accountPickerDisplaysUsername", default: accountPickerDisplaysUsernameDefault)
|
||||
#endif
|
||||
@@ -41,9 +41,9 @@ extension Defaults.Keys {
|
||||
static let showChannelAvatarInVideosListing = Key<Bool>("showChannelAvatarInVideosListing", default: true)
|
||||
|
||||
static let playerButtonSingleTapGesture = Key<PlayerTapGestureAction>("playerButtonSingleTapGesture", default: .togglePlayer)
|
||||
static let playerButtonDoubleTapGesture = Key<PlayerTapGestureAction>("playerButtonDoubleTapGesture", default: .nothing)
|
||||
static let playerButtonShowsControlButtonsWhenMinimized = Key<Bool>("playerButtonShowsControlButtonsWhenMinimized", default: false)
|
||||
static let playerButtonIsExpanded = Key<Bool>("playerButtonIsExpanded", default: false)
|
||||
static let playerButtonDoubleTapGesture = Key<PlayerTapGestureAction>("playerButtonDoubleTapGesture", default: .togglePlayerVisibility)
|
||||
static let playerButtonShowsControlButtonsWhenMinimized = Key<Bool>("playerButtonShowsControlButtonsWhenMinimized", default: true)
|
||||
static let playerButtonIsExpanded = Key<Bool>("playerButtonIsExpanded", default: true)
|
||||
static let playerBarMaxWidth = Key<String>("playerBarMaxWidth", default: "600")
|
||||
static let channelOnThumbnail = Key<Bool>("channelOnThumbnail", default: false)
|
||||
static let timeOnThumbnail = Key<Bool>("timeOnThumbnail", default: true)
|
||||
@@ -64,7 +64,7 @@ extension Defaults.Keys {
|
||||
static let closeVideoOnEOF = Key<Bool>("closeVideoOnEOF", default: false)
|
||||
|
||||
#if !os(macOS)
|
||||
static let pauseOnEnteringBackground = Key<Bool>("pauseOnEnteringBackground", default: true)
|
||||
static let pauseOnEnteringBackground = Key<Bool>("pauseOnEnteringBackground", default: false)
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
@@ -79,7 +79,7 @@ extension Defaults.Keys {
|
||||
|
||||
static let showChapters = Key<Bool>("showChapters", default: true)
|
||||
static let showChapterThumbnails = Key<Bool>("showChapterThumbnails", default: true)
|
||||
static let showChapterThumbnailsOnlyWhenDifferent = Key<Bool>("showChapterThumbnailsOnlyWhenDifferent", default: true)
|
||||
static let showChapterThumbnailsOnlyWhenDifferent = Key<Bool>("showChapterThumbnailsOnlyWhenDifferent", default: false)
|
||||
static let expandChapters = Key<Bool>("expandChapters", default: true)
|
||||
static let showRelated = Key<Bool>("showRelated", default: true)
|
||||
static let showInspector = Key<ShowInspectorSetting>("showInspector", default: .onlyLocal)
|
||||
@@ -94,10 +94,10 @@ extension Defaults.Keys {
|
||||
|
||||
#if os(iOS)
|
||||
static let honorSystemOrientationLock = Key<Bool>("honorSystemOrientationLock", default: true)
|
||||
static let enterFullscreenInLandscape = Key<Bool>("enterFullscreenInLandscape", default: UIDevice.current.userInterfaceIdiom == .phone)
|
||||
static let enterFullscreenInLandscape = Key<Bool>("enterFullscreenInLandscape", default: Constants.isIPhone)
|
||||
static let rotateToLandscapeOnEnterFullScreen = Key<FullScreenRotationSetting>(
|
||||
"rotateToLandscapeOnEnterFullScreen",
|
||||
default: UIDevice.current.userInterfaceIdiom == .phone ? .landscapeRight : .disabled
|
||||
default: Constants.isIPhone ? .landscapeRight : .disabled
|
||||
)
|
||||
#endif
|
||||
|
||||
@@ -116,14 +116,14 @@ extension Defaults.Keys {
|
||||
|
||||
// MARK: GROUP - Controls
|
||||
|
||||
static let avPlayerUsesSystemControls = Key<Bool>("avPlayerUsesSystemControls", default: true)
|
||||
static let avPlayerUsesSystemControls = Key<Bool>("avPlayerUsesSystemControls", default: Constants.isTvOS)
|
||||
static let horizontalPlayerGestureEnabled = Key<Bool>("horizontalPlayerGestureEnabled", default: true)
|
||||
static let seekGestureSensitivity = Key<Double>("seekGestureSensitivity", default: 30.0)
|
||||
static let seekGestureSpeed = Key<Double>("seekGestureSpeed", default: 0.5)
|
||||
|
||||
#if os(iOS)
|
||||
static let playerControlsLayoutDefault = UIDevice.current.userInterfaceIdiom == .pad ? PlayerControlsLayout.medium : .small
|
||||
static let fullScreenPlayerControlsLayoutDefault = UIDevice.current.userInterfaceIdiom == .pad ? PlayerControlsLayout.medium : .small
|
||||
static let playerControlsLayoutDefault = Constants.isIPad ? PlayerControlsLayout.medium : .small
|
||||
static let fullScreenPlayerControlsLayoutDefault = Constants.isIPad ? PlayerControlsLayout.medium : .small
|
||||
#elseif os(tvOS)
|
||||
static let playerControlsLayoutDefault = PlayerControlsLayout.tvRegular
|
||||
static let fullScreenPlayerControlsLayoutDefault = PlayerControlsLayout.tvRegular
|
||||
@@ -175,61 +175,152 @@ extension Defaults.Keys {
|
||||
|
||||
// MARK: GROUP - Quality
|
||||
|
||||
static let hd2160pMPVProfile = QualityProfile(id: "hd2160pMPVProfile", backend: .mpv, resolution: .hd2160p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let hd1080pMPVProfile = QualityProfile(id: "hd1080pMPVProfile", backend: .mpv, resolution: .hd1080p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let hd720pMPVProfile = QualityProfile(id: "hd720pMPVProfile", backend: .mpv, resolution: .hd720p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let hd720pAVPlayerProfile = QualityProfile(id: "hd720pAVPlayerProfile", backend: .appleAVPlayer, resolution: .hd720p30, formats: [.hls, .stream], order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let sd360pAVPlayerProfile = QualityProfile(id: "sd360pAVPlayerProfile", backend: .appleAVPlayer, resolution: .sd360p30, formats: [.hls, .stream], order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let hd2160p60MPVProfile = QualityProfile(id: "hd2160p60MPVProfile", backend: .mpv, resolution: .hd2160p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let hd1080p60MPVProfile = QualityProfile(id: "hd1080p60MPVProfile", backend: .mpv, resolution: .hd1080p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let hd1080pMPVProfile = QualityProfile(id: "hd1080pMPVProfile", backend: .mpv, resolution: .hd1080p30, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let hd720p60MPVProfile = QualityProfile(id: "hd720p60MPVProfile", backend: .mpv, resolution: .hd720p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let hd720pMPVProfile = QualityProfile(id: "hd720pMPVProfile", backend: .mpv, resolution: .hd720p30, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let sd360pMPVProfile = QualityProfile(id: "sd360pMPVProfile", backend: .mpv, resolution: .sd360p30, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let hd720pAVPlayerProfile = QualityProfile(id: "hd720pAVPlayerProfile", backend: .appleAVPlayer, resolution: .hd720p30, formats: [.stream, .hls], order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let sd360pAVPlayerProfile = QualityProfile(id: "sd360pAVPlayerProfile", backend: .appleAVPlayer, resolution: .sd360p30, formats: [.stream, .hls], order: Array(QualityProfile.Format.allCases.indices))
|
||||
|
||||
#if os(iOS)
|
||||
static let qualityProfilesDefault = UIDevice.current.userInterfaceIdiom == .pad ? [
|
||||
hd2160pMPVProfile,
|
||||
hd1080pMPVProfile,
|
||||
hd720pMPVProfile,
|
||||
hd720pAVPlayerProfile,
|
||||
sd360pAVPlayerProfile
|
||||
] : [
|
||||
hd1080pMPVProfile,
|
||||
hd720pMPVProfile,
|
||||
hd720pAVPlayerProfile,
|
||||
sd360pAVPlayerProfile
|
||||
]
|
||||
enum QualityProfiles {
|
||||
// iPad-specific settings
|
||||
enum iPad {
|
||||
static let qualityProfilesDefault = [
|
||||
hd1080p60MPVProfile,
|
||||
hd1080pMPVProfile,
|
||||
hd720p60MPVProfile,
|
||||
hd720pMPVProfile
|
||||
]
|
||||
|
||||
static let batteryCellularProfileDefault = hd720pMPVProfile.id
|
||||
static let batteryNonCellularProfileDefault = hd720p60MPVProfile.id
|
||||
static let chargingCellularProfileDefault = hd1080pMPVProfile.id
|
||||
static let chargingNonCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
}
|
||||
|
||||
// iPhone-specific settings
|
||||
enum iPhone {
|
||||
static let qualityProfilesDefault = [
|
||||
hd1080p60MPVProfile,
|
||||
hd1080pMPVProfile,
|
||||
hd720p60MPVProfile,
|
||||
hd720pMPVProfile,
|
||||
sd360pMPVProfile
|
||||
]
|
||||
|
||||
static let batteryCellularProfileDefault = sd360pMPVProfile.id
|
||||
static let batteryNonCellularProfileDefault = hd720p60MPVProfile.id
|
||||
static let chargingCellularProfileDefault = hd720pMPVProfile.id
|
||||
static let chargingNonCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
}
|
||||
|
||||
// Access the correct profile based on device type
|
||||
static var currentProfile: (qualityProfilesDefault: [QualityProfile], batteryCellularProfileDefault: String, batteryNonCellularProfileDefault: String, chargingCellularProfileDefault: String, chargingNonCellularProfileDefault: String) {
|
||||
if Constants.isIPad {
|
||||
return (
|
||||
qualityProfilesDefault: iPad.qualityProfilesDefault,
|
||||
batteryCellularProfileDefault: iPad.batteryCellularProfileDefault,
|
||||
batteryNonCellularProfileDefault: iPad.batteryNonCellularProfileDefault,
|
||||
chargingCellularProfileDefault: iPad.chargingCellularProfileDefault,
|
||||
chargingNonCellularProfileDefault: iPad.chargingNonCellularProfileDefault
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
qualityProfilesDefault: iPhone.qualityProfilesDefault,
|
||||
batteryCellularProfileDefault: iPhone.batteryCellularProfileDefault,
|
||||
batteryNonCellularProfileDefault: iPhone.batteryNonCellularProfileDefault,
|
||||
chargingCellularProfileDefault: iPhone.chargingCellularProfileDefault,
|
||||
chargingNonCellularProfileDefault: iPhone.chargingNonCellularProfileDefault
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
static let batteryCellularProfileDefault = hd720pAVPlayerProfile.id
|
||||
static let batteryNonCellularProfileDefault = hd720pAVPlayerProfile.id
|
||||
static let chargingCellularProfileDefault = hd720pAVPlayerProfile.id
|
||||
static let chargingNonCellularProfileDefault = hd1080pMPVProfile.id
|
||||
#elseif os(tvOS)
|
||||
static let qualityProfilesDefault = [
|
||||
hd2160pMPVProfile,
|
||||
hd1080pMPVProfile,
|
||||
hd720pMPVProfile,
|
||||
hd720pAVPlayerProfile
|
||||
]
|
||||
static let batteryCellularProfileDefault = hd1080pMPVProfile.id
|
||||
static let batteryNonCellularProfileDefault = hd1080pMPVProfile.id
|
||||
static let chargingCellularProfileDefault = hd1080pMPVProfile.id
|
||||
static let chargingNonCellularProfileDefault = hd1080pMPVProfile.id
|
||||
enum QualityProfiles {
|
||||
// tvOS-specific settings
|
||||
enum tvOS {
|
||||
static let qualityProfilesDefault = [
|
||||
hd2160p60MPVProfile,
|
||||
hd1080p60MPVProfile,
|
||||
hd720p60MPVProfile,
|
||||
hd720pAVPlayerProfile
|
||||
]
|
||||
|
||||
static let batteryCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
static let batteryNonCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
static let chargingCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
static let chargingNonCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
}
|
||||
|
||||
// Access the correct profile based on device type
|
||||
static var currentProfile: (qualityProfilesDefault: [QualityProfile], batteryCellularProfileDefault: String, batteryNonCellularProfileDefault: String, chargingCellularProfileDefault: String, chargingNonCellularProfileDefault: String) {
|
||||
(
|
||||
qualityProfilesDefault: tvOS.qualityProfilesDefault,
|
||||
batteryCellularProfileDefault: tvOS.batteryCellularProfileDefault,
|
||||
batteryNonCellularProfileDefault: tvOS.batteryNonCellularProfileDefault,
|
||||
chargingCellularProfileDefault: tvOS.chargingCellularProfileDefault,
|
||||
chargingNonCellularProfileDefault: tvOS.chargingNonCellularProfileDefault
|
||||
)
|
||||
}
|
||||
}
|
||||
#else
|
||||
static let qualityProfilesDefault = [
|
||||
hd2160pMPVProfile,
|
||||
hd1080pMPVProfile,
|
||||
hd720pMPVProfile,
|
||||
hd720pAVPlayerProfile
|
||||
]
|
||||
static let batteryCellularProfileDefault = hd1080pMPVProfile.id
|
||||
static let batteryNonCellularProfileDefault = hd1080pMPVProfile.id
|
||||
static let chargingCellularProfileDefault = hd1080pMPVProfile.id
|
||||
static let chargingNonCellularProfileDefault = hd1080pMPVProfile.id
|
||||
enum QualityProfiles {
|
||||
// macOS-specific settings
|
||||
enum macOS {
|
||||
static let qualityProfilesDefault = [
|
||||
hd2160p60MPVProfile,
|
||||
hd1080p60MPVProfile,
|
||||
hd1080pMPVProfile,
|
||||
hd720p60MPVProfile
|
||||
]
|
||||
|
||||
static let batteryCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
static let batteryNonCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
static let chargingCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
static let chargingNonCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
}
|
||||
|
||||
// Access the correct profile for other platforms
|
||||
static var currentProfile: (qualityProfilesDefault: [QualityProfile], batteryCellularProfileDefault: String, batteryNonCellularProfileDefault: String, chargingCellularProfileDefault: String, chargingNonCellularProfileDefault: String) {
|
||||
(
|
||||
qualityProfilesDefault: macOS.qualityProfilesDefault,
|
||||
batteryCellularProfileDefault: macOS.batteryCellularProfileDefault,
|
||||
batteryNonCellularProfileDefault: macOS.batteryNonCellularProfileDefault,
|
||||
chargingCellularProfileDefault: macOS.chargingCellularProfileDefault,
|
||||
chargingNonCellularProfileDefault: macOS.chargingNonCellularProfileDefault
|
||||
)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static let batteryCellularProfile = Key<QualityProfile.ID>("batteryCellularProfile", default: batteryCellularProfileDefault)
|
||||
static let batteryNonCellularProfile = Key<QualityProfile.ID>("batteryNonCellularProfile", default: batteryNonCellularProfileDefault)
|
||||
static let chargingCellularProfile = Key<QualityProfile.ID>("chargingCellularProfile", default: chargingCellularProfileDefault)
|
||||
static let chargingNonCellularProfile = Key<QualityProfile.ID>("chargingNonCellularProfile", default: chargingNonCellularProfileDefault)
|
||||
static let forceAVPlayerForLiveStreams = Key<Bool>("forceAVPlayerForLiveStreams", default: true)
|
||||
|
||||
static let qualityProfiles = Key<[QualityProfile]>("qualityProfiles", default: qualityProfilesDefault)
|
||||
static let batteryCellularProfile = Key<QualityProfile.ID>(
|
||||
"batteryCellularProfile",
|
||||
default: QualityProfiles.currentProfile.batteryCellularProfileDefault
|
||||
)
|
||||
static let batteryNonCellularProfile = Key<QualityProfile.ID>(
|
||||
"batteryNonCellularProfile",
|
||||
default: QualityProfiles.currentProfile.batteryNonCellularProfileDefault
|
||||
)
|
||||
static let chargingCellularProfile = Key<QualityProfile.ID>(
|
||||
"chargingCellularProfile",
|
||||
default: QualityProfiles.currentProfile.chargingCellularProfileDefault
|
||||
)
|
||||
static let chargingNonCellularProfile = Key<QualityProfile.ID>(
|
||||
"chargingNonCellularProfile",
|
||||
default: QualityProfiles.currentProfile.chargingNonCellularProfileDefault
|
||||
)
|
||||
static let forceAVPlayerForLiveStreams = Key<Bool>(
|
||||
"forceAVPlayerForLiveStreams",
|
||||
default: true
|
||||
)
|
||||
static let qualityProfiles = Key<[QualityProfile]>(
|
||||
"qualityProfiles",
|
||||
default: QualityProfiles.currentProfile.qualityProfilesDefault
|
||||
)
|
||||
|
||||
// MARK: GROUP - History
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ enum LanguageCodes: String, CaseIterable {
|
||||
case Vietnamese = "vi"
|
||||
case Xhosa = "xh"
|
||||
case Chinese = "zh"
|
||||
case Chinese_Hans = "zh-Hans"
|
||||
case Zulu = "zu"
|
||||
|
||||
var description: String {
|
||||
@@ -147,6 +148,8 @@ enum LanguageCodes: String, CaseIterable {
|
||||
return "Xhosa"
|
||||
case .Chinese:
|
||||
return "Chinese"
|
||||
case .Chinese_Hans:
|
||||
return "Chinese (Simplified)"
|
||||
case .Zulu:
|
||||
return "Zulu"
|
||||
}
|
||||
|
||||
@@ -248,31 +248,33 @@ struct PlayerControls: View {
|
||||
return [player.playerSize.height - inset, 500].min()!
|
||||
}
|
||||
|
||||
@ViewBuilder var controlsBackground: some View {
|
||||
ZStack {
|
||||
if player.musicMode,
|
||||
let url = controlsBackgroundURL
|
||||
{
|
||||
ThumbnailView(url: url)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.transition(.opacity)
|
||||
.animation(.default)
|
||||
} else if player.videoForDisplay == nil {
|
||||
Color.black
|
||||
@ViewBuilder
|
||||
var controlsBackground: some View {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
if player.musicMode,
|
||||
let video = player.videoForDisplay
|
||||
{
|
||||
let thumbnail = thumbnails.best(video)
|
||||
if let url = thumbnail.url,
|
||||
let quality = thumbnail.quality
|
||||
{
|
||||
let aspectRatio = (quality == .default || quality == .high) ? Constants.aspectRatio4x3 : Constants.aspectRatio16x9
|
||||
|
||||
ThumbnailView(url: url)
|
||||
.aspectRatio(aspectRatio, contentMode: .fill)
|
||||
.frame(width: geometry.size.width, height: geometry.size.height)
|
||||
.transition(.opacity)
|
||||
.animation(.default)
|
||||
.clipped()
|
||||
}
|
||||
} else if player.videoForDisplay == nil {
|
||||
Color.black
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var controlsBackgroundURL: URL? {
|
||||
if let video = player.videoForDisplay,
|
||||
let url = thumbnails.best(video).url
|
||||
{
|
||||
return url
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var timeline: some View {
|
||||
TimelineView(context: .player).foregroundColor(.primary)
|
||||
}
|
||||
@@ -381,7 +383,7 @@ struct PlayerControls: View {
|
||||
}
|
||||
|
||||
private var pipButton: some View {
|
||||
button("PiP", systemImage: player.pipImage, action: player.togglePiPAction)
|
||||
button("PiP", systemImage: player.pipImage, active: player.playingInPictureInPicture, action: player.togglePiPAction)
|
||||
.disabled(!player.pipPossible)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import GLKit
|
||||
import Libmpv
|
||||
import Logging
|
||||
import MPVKit
|
||||
import OpenGLES
|
||||
|
||||
final class MPVOGLView: GLKView {
|
||||
|
||||
@@ -8,7 +8,7 @@ extension VideoPlayerView {
|
||||
.updating($dragGestureOffset) { value, state, _ in
|
||||
guard isVerticalDrag else { return }
|
||||
var translation = value.translation
|
||||
translation.height = max(0, translation.height)
|
||||
translation.height = max(-translation.height, translation.height)
|
||||
state = translation
|
||||
}
|
||||
#endif
|
||||
@@ -18,7 +18,8 @@ extension VideoPlayerView {
|
||||
.onChanged { value in
|
||||
guard player.presentingPlayer,
|
||||
!controlsOverlayModel.presenting,
|
||||
dragGestureState else { return }
|
||||
dragGestureState,
|
||||
!disableToggleGesture else { return }
|
||||
|
||||
if player.controls.presentingControls, !player.musicMode {
|
||||
player.controls.presentingControls = false
|
||||
@@ -61,19 +62,22 @@ extension VideoPlayerView {
|
||||
return
|
||||
}
|
||||
|
||||
guard verticalDrag > 0 else { return }
|
||||
viewDragOffset = verticalDrag
|
||||
|
||||
if verticalDrag > 60,
|
||||
player.playingFullScreen
|
||||
{
|
||||
player.exitFullScreen(showControls: false)
|
||||
#if os(iOS)
|
||||
if Constants.isIPhone {
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
|
||||
}
|
||||
#endif
|
||||
// Toggle fullscreen on upward drag only when not disabled
|
||||
if verticalDrag < -50 {
|
||||
if player.playingFullScreen {
|
||||
player.exitFullScreen(showControls: false)
|
||||
} else {
|
||||
player.enterFullScreen()
|
||||
}
|
||||
disableGestureTemporarily()
|
||||
return
|
||||
}
|
||||
|
||||
// Ignore downward swipes when in fullscreen
|
||||
guard verticalDrag > 0 && !player.playingFullScreen else {
|
||||
return
|
||||
}
|
||||
viewDragOffset = verticalDrag
|
||||
}
|
||||
.onEnded { _ in
|
||||
onPlayerDragGestureEnded()
|
||||
@@ -86,16 +90,6 @@ 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,
|
||||
@@ -117,4 +111,12 @@ extension VideoPlayerView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function to temporarily disable the toggle gesture after a fullscreen change
|
||||
private func disableGestureTemporarily() {
|
||||
disableToggleGesture = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
|
||||
disableToggleGesture = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +86,10 @@ struct VideoActions: View {
|
||||
}
|
||||
}
|
||||
|
||||
func isAnyActionVisible() -> Bool {
|
||||
return Action.allCases.contains { isVisible($0) }
|
||||
}
|
||||
|
||||
func isActionable(_ action: Action) -> Bool {
|
||||
switch action {
|
||||
case .share:
|
||||
@@ -151,7 +155,7 @@ struct VideoActions: View {
|
||||
case .fullScreen:
|
||||
actionButton("Fullscreen", systemImage: player.fullscreenImage, action: player.toggleFullScreenAction)
|
||||
case .pip:
|
||||
actionButton("PiP", systemImage: player.pipImage, action: player.togglePiPAction)
|
||||
actionButton("PiP", systemImage: player.pipImage, active: player.playingInPictureInPicture, action: player.togglePiPAction)
|
||||
#if os(iOS)
|
||||
case .lockOrientation:
|
||||
actionButton("Lock", systemImage: player.lockOrientationImage, active: player.lockedOrientation != nil, action: player.lockOrientationAction)
|
||||
@@ -198,10 +202,10 @@ struct VideoActions: View {
|
||||
VStack(spacing: 3) {
|
||||
Image(systemName: systemImage)
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundColor(active ? Color("AppRedColor") : .accentColor)
|
||||
.foregroundColor(active ? Color("AppRedColor") : .primary)
|
||||
if playerActionsButtonLabelStyle.text {
|
||||
Text(name.localized())
|
||||
.foregroundColor(active ? Color("AppRedColor") : .secondary)
|
||||
.foregroundColor(active ? Color("AppRedColor") : .primary)
|
||||
.font(.caption2)
|
||||
.allowsTightening(true)
|
||||
.lineLimit(1)
|
||||
|
||||
@@ -235,15 +235,22 @@ struct VideoDetails: View {
|
||||
)
|
||||
#endif
|
||||
// swiftlint:enable trailing_closure
|
||||
|
||||
VideoActions(video: player.videoForDisplay)
|
||||
.padding(.vertical, 5)
|
||||
.frame(maxHeight: 50)
|
||||
.frame(maxWidth: .infinity)
|
||||
.borderTop(height: 0.5, color: Color("ControlsBorderColor"))
|
||||
.borderBottom(height: 0.5, color: Color("ControlsBorderColor"))
|
||||
.animation(nil, value: player.currentItem)
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
if VideoActions().isAnyActionVisible() {
|
||||
VideoActions(video: player.videoForDisplay)
|
||||
.padding(.vertical, 5)
|
||||
.frame(maxHeight: 50)
|
||||
.frame(maxWidth: .infinity)
|
||||
.borderTop(height: 0.5, color: Color("ControlsBorderColor"))
|
||||
.borderBottom(height: 0.5, color: Color("ControlsBorderColor"))
|
||||
.animation(nil, value: player.currentItem)
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(Color.clear)
|
||||
.frame(height: 0.5)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color("ControlsBorderColor"))
|
||||
}
|
||||
|
||||
ScrollViewReader { proxy in
|
||||
pageView
|
||||
|
||||
@@ -47,11 +47,18 @@ struct VideoPlayerView: View {
|
||||
#if !os(tvOS)
|
||||
@GestureState var dragGestureState = false
|
||||
@GestureState var dragGestureOffset = CGSize.zero
|
||||
@State var isHorizontalDrag = false // swiftlint:disable:this swiftui_state_private
|
||||
@State var isVerticalDrag = false // swiftlint:disable:this swiftui_state_private
|
||||
@State var viewDragOffset = Self.hiddenOffset // swiftlint:disable:this swiftui_state_private
|
||||
// swiftlint:disable private_swiftui_state
|
||||
@State var isHorizontalDrag = false
|
||||
@State var isVerticalDrag = false
|
||||
@State var viewDragOffset = Self.hiddenOffset
|
||||
// swiftlint:enable private_swiftui_state
|
||||
|
||||
#endif
|
||||
|
||||
// swiftlint:disable private_swiftui_state
|
||||
@State var disableToggleGesture = false
|
||||
// swiftlint:enable private_swiftui_state
|
||||
|
||||
@ObservedObject var player = PlayerModel.shared // swiftlint:disable:this swiftui_state_private
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
@@ -86,6 +86,7 @@ struct InstanceForm: View {
|
||||
.autocapitalization(.none)
|
||||
.keyboardType(.URL)
|
||||
#endif
|
||||
.disableAutocorrection(true)
|
||||
|
||||
#if os(tvOS)
|
||||
VStack {
|
||||
|
||||
@@ -329,8 +329,8 @@ struct PlayerSettings: View {
|
||||
|
||||
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("Landscape left").tag(FullScreenRotationSetting.landscapeLeft)
|
||||
Text("Landscape right").tag(FullScreenRotationSetting.landscapeRight)
|
||||
Text("No rotation").tag(FullScreenRotationSetting.disabled)
|
||||
}
|
||||
.modifier(SettingsPickerModifier())
|
||||
|
||||
@@ -301,7 +301,7 @@ struct QualityProfileForm: View {
|
||||
func isFormatDisabled(_ format: QualityProfile.Format) -> Bool {
|
||||
guard backend == .appleAVPlayer else { return false }
|
||||
|
||||
let avPlayerFormats = [QualityProfile.Format.hls, .stream, .mp4]
|
||||
let avPlayerFormats = [.stream, QualityProfile.Format.hls]
|
||||
|
||||
return !avPlayerFormats.contains(format)
|
||||
}
|
||||
|
||||
@@ -24,14 +24,42 @@ struct VideoContextMenuView: View {
|
||||
|
||||
private var backgroundContext = PersistenceController.shared.container.newBackgroundContext()
|
||||
|
||||
@State private var isOverlayVisible = false
|
||||
|
||||
init(video: Video) {
|
||||
self.video = video
|
||||
_watchRequest = video.watchFetchRequest
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if video.videoID != Video.fixtureID {
|
||||
contextMenu
|
||||
ZStack {
|
||||
// Conditional overlay to block taps on underlying views
|
||||
if isOverlayVisible {
|
||||
Color.clear
|
||||
.contentShape(Rectangle())
|
||||
#if !os(tvOS)
|
||||
// This is not available on tvOS < 16 so we leave out.
|
||||
// TODO: remove #if when setting the minimum deployment target to >= 16
|
||||
.onTapGesture {
|
||||
// Dismiss overlay without triggering other interactions
|
||||
isOverlayVisible = false
|
||||
}
|
||||
#endif
|
||||
.ignoresSafeArea() // Ensure overlay covers the entire screen
|
||||
.accessibilityLabel("Dismiss context menu")
|
||||
.accessibilityHint("Tap to close the context")
|
||||
.accessibilityAddTraits(.isButton)
|
||||
}
|
||||
|
||||
if video.videoID != Video.fixtureID {
|
||||
contextMenu
|
||||
.onAppear {
|
||||
isOverlayVisible = true
|
||||
}
|
||||
.onDisappear {
|
||||
isOverlayVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -619,9 +619,6 @@
|
||||
3797104928D3D10600D5F53C /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 3797104828D3D10600D5F53C /* SDWebImageSwiftUI */; };
|
||||
3797104B28D3D18800D5F53C /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 3797104A28D3D18800D5F53C /* SDWebImageSwiftUI */; };
|
||||
3797104D28D3D19100D5F53C /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 3797104C28D3D19100D5F53C /* SDWebImageSwiftUI */; };
|
||||
3797665B2C79FA6900C10DBD /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3797665A2C79FA6900C10DBD /* MPVKit */; };
|
||||
3797665D2C79FA7500C10DBD /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3797665C2C79FA7500C10DBD /* MPVKit */; };
|
||||
3797665F2C79FA7D00C10DBD /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3797665E2C79FA7D00C10DBD /* MPVKit */; };
|
||||
3797757D268922D100DD52A8 /* Siesta in Frameworks */ = {isa = PBXBuildFile; productRef = 3797757C268922D100DD52A8 /* Siesta */; };
|
||||
37977583268922F600DD52A8 /* InvidiousAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37977582268922F600DD52A8 /* InvidiousAPI.swift */; };
|
||||
37977584268922F600DD52A8 /* InvidiousAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37977582268922F600DD52A8 /* InvidiousAPI.swift */; };
|
||||
@@ -1080,6 +1077,9 @@
|
||||
E258F38A2BF61BD2005B8C28 /* URLTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = E258F3892BF61BD2005B8C28 /* URLTester.swift */; };
|
||||
E258F38B2BF61BD2005B8C28 /* URLTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = E258F3892BF61BD2005B8C28 /* URLTester.swift */; };
|
||||
E258F38C2BF61BD2005B8C28 /* URLTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = E258F3892BF61BD2005B8C28 /* URLTester.swift */; };
|
||||
E265D0C22C7D217000D2BB8E /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = E265D0C12C7D217000D2BB8E /* MPVKit */; };
|
||||
E265D0C42C7D218A00D2BB8E /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = E265D0C32C7D218A00D2BB8E /* MPVKit */; };
|
||||
E265D0C62C7D21A300D2BB8E /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = E265D0C52C7D21A300D2BB8E /* MPVKit */; };
|
||||
E27568B92BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */; };
|
||||
E27568BA2BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */; };
|
||||
E27568BB2BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */; };
|
||||
@@ -1599,7 +1599,7 @@
|
||||
3797104928D3D10600D5F53C /* SDWebImageSwiftUI in Frameworks */,
|
||||
37BD07B92698AB2E003EBB87 /* Siesta in Frameworks */,
|
||||
37FB284D2722099E00A57617 /* SDWebImageWebPCoder in Frameworks */,
|
||||
3797665B2C79FA6900C10DBD /* MPVKit in Frameworks */,
|
||||
E265D0C22C7D217000D2BB8E /* MPVKit in Frameworks */,
|
||||
37CF8B8428535E4F00B71E37 /* SDWebImage in Frameworks */,
|
||||
37C7367A2AC33010007630E1 /* SwiftUIIntrospect in Frameworks */,
|
||||
);
|
||||
@@ -1624,7 +1624,7 @@
|
||||
375B8AB728B583BD00397B31 /* KeychainAccess in Frameworks */,
|
||||
3703205E27D2BB12007A0CB8 /* SDWebImageWebPCoder in Frameworks */,
|
||||
37CF8B8628535E5A00B71E37 /* SDWebImage in Frameworks */,
|
||||
3797665D2C79FA7500C10DBD /* MPVKit in Frameworks */,
|
||||
E265D0C42C7D218A00D2BB8E /* MPVKit in Frameworks */,
|
||||
3703205C27D2BAF3007A0CB8 /* SwiftyJSON in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -1670,7 +1670,7 @@
|
||||
372915E42687E33E00F5A35B /* Defaults in Frameworks */,
|
||||
3772003B27E8EEC800CB2475 /* libbz2.tbd in Frameworks */,
|
||||
37BADCA9269A570B009BE4FB /* Alamofire in Frameworks */,
|
||||
3797665F2C79FA7D00C10DBD /* MPVKit in Frameworks */,
|
||||
E265D0C62C7D21A300D2BB8E /* MPVKit in Frameworks */,
|
||||
37D4B19D2671817900C925CA /* SwiftyJSON in Frameworks */,
|
||||
3797757D268922D100DD52A8 /* Siesta in Frameworks */,
|
||||
);
|
||||
@@ -2585,7 +2585,7 @@
|
||||
371AC0AB294D1A490085989E /* CachedAsyncImage */,
|
||||
379325D429A265A300181CF1 /* Logging */,
|
||||
37C736792AC33010007630E1 /* SwiftUIIntrospect */,
|
||||
3797665A2C79FA6900C10DBD /* MPVKit */,
|
||||
E265D0C12C7D217000D2BB8E /* MPVKit */,
|
||||
);
|
||||
productName = "Yattee (iOS)";
|
||||
productReference = 37D4B0C92671614900C925CA /* Yattee.app */;
|
||||
@@ -2624,7 +2624,7 @@
|
||||
371AC0B1294D1C230085989E /* CachedAsyncImage */,
|
||||
379325D629A265AE00181CF1 /* Logging */,
|
||||
37C736772AC32B28007630E1 /* SwiftUIIntrospect */,
|
||||
3797665C2C79FA7500C10DBD /* MPVKit */,
|
||||
E265D0C32C7D218A00D2BB8E /* MPVKit */,
|
||||
);
|
||||
productName = "Yattee (macOS)";
|
||||
productReference = 37D4B0CF2671614900C925CA /* Yattee.app */;
|
||||
@@ -2702,7 +2702,7 @@
|
||||
377F9F75294403880043F856 /* Cache */,
|
||||
371AC0B3294D1C290085989E /* CachedAsyncImage */,
|
||||
379325D829A265B500181CF1 /* Logging */,
|
||||
3797665E2C79FA7D00C10DBD /* MPVKit */,
|
||||
E265D0C52C7D21A300D2BB8E /* MPVKit */,
|
||||
);
|
||||
productName = Yattee;
|
||||
productReference = 37D4B158267164AE00C925CA /* Yattee.app */;
|
||||
@@ -2822,7 +2822,7 @@
|
||||
374D11E52943C56300CB4350 /* XCRemoteSwiftPackageReference "Cache" */,
|
||||
371AC0AA294D1A490085989E /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */,
|
||||
379325D329A265A300181CF1 /* XCRemoteSwiftPackageReference "swift-log" */,
|
||||
379766592C79FA6900C10DBD /* XCRemoteSwiftPackageReference "MPVKit" */,
|
||||
E265D0C02C7D217000D2BB8E /* XCRemoteSwiftPackageReference "MPVKit" */,
|
||||
);
|
||||
productRefGroup = 37D4B0CA2671614900C925CA /* Products */;
|
||||
projectDirPath = "";
|
||||
@@ -4103,7 +4103,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 190;
|
||||
CURRENT_PROJECT_VERSION = 194;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
|
||||
@@ -4134,7 +4134,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 190;
|
||||
CURRENT_PROJECT_VERSION = 194;
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||
@@ -4165,7 +4165,7 @@
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 190;
|
||||
CURRENT_PROJECT_VERSION = 194;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
@@ -4185,7 +4185,7 @@
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 190;
|
||||
CURRENT_PROJECT_VERSION = 194;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
@@ -4326,6 +4326,7 @@
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 3;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
@@ -4348,7 +4349,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "iOS/Yattee (iOS).entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 190;
|
||||
CURRENT_PROJECT_VERSION = 194;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
@@ -4400,7 +4401,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 190;
|
||||
CURRENT_PROJECT_VERSION = 194;
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1";
|
||||
@@ -4452,7 +4453,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 190;
|
||||
CURRENT_PROJECT_VERSION = 194;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@@ -4491,7 +4492,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 190;
|
||||
CURRENT_PROJECT_VERSION = 194;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = 78Z5H3M6RJ;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
@@ -4525,7 +4526,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 190;
|
||||
CURRENT_PROJECT_VERSION = 194;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -4548,7 +4549,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 190;
|
||||
CURRENT_PROJECT_VERSION = 194;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -4573,7 +4574,7 @@
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 190;
|
||||
CURRENT_PROJECT_VERSION = 194;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -4597,7 +4598,7 @@
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 190;
|
||||
CURRENT_PROJECT_VERSION = 194;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -4623,7 +4624,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 190;
|
||||
CURRENT_PROJECT_VERSION = 194;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -4663,7 +4664,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 190;
|
||||
CURRENT_PROJECT_VERSION = 194;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
"DEVELOPMENT_TEAM[sdk=appletvos*]" = 78Z5H3M6RJ;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -4703,7 +4704,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 190;
|
||||
CURRENT_PROJECT_VERSION = 194;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@@ -4726,7 +4727,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 190;
|
||||
CURRENT_PROJECT_VERSION = 194;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@@ -4960,14 +4961,6 @@
|
||||
minimumVersion = 2.1.0;
|
||||
};
|
||||
};
|
||||
379766592C79FA6900C10DBD /* XCRemoteSwiftPackageReference "MPVKit" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/cxfksword/MPVKit";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 0.38.0;
|
||||
};
|
||||
};
|
||||
3797757B268922D100DD52A8 /* XCRemoteSwiftPackageReference "siesta" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/bustoutsolutions/siesta";
|
||||
@@ -5040,6 +5033,14 @@
|
||||
minimumVersion = 0.3.0;
|
||||
};
|
||||
};
|
||||
E265D0C02C7D217000D2BB8E /* XCRemoteSwiftPackageReference "MPVKit" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/mpvkit/MPVKit.git";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = "0.38.0-fix";
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
@@ -5228,21 +5229,6 @@
|
||||
package = 3797104728D3D10600D5F53C /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */;
|
||||
productName = SDWebImageSwiftUI;
|
||||
};
|
||||
3797665A2C79FA6900C10DBD /* MPVKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 379766592C79FA6900C10DBD /* XCRemoteSwiftPackageReference "MPVKit" */;
|
||||
productName = MPVKit;
|
||||
};
|
||||
3797665C2C79FA7500C10DBD /* MPVKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 379766592C79FA6900C10DBD /* XCRemoteSwiftPackageReference "MPVKit" */;
|
||||
productName = MPVKit;
|
||||
};
|
||||
3797665E2C79FA7D00C10DBD /* MPVKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 379766592C79FA6900C10DBD /* XCRemoteSwiftPackageReference "MPVKit" */;
|
||||
productName = MPVKit;
|
||||
};
|
||||
3797757C268922D100DD52A8 /* Siesta */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 3797757B268922D100DD52A8 /* XCRemoteSwiftPackageReference "siesta" */;
|
||||
@@ -5328,6 +5314,21 @@
|
||||
package = 37FB285227220D8400A57617 /* XCRemoteSwiftPackageReference "SDWebImagePINPlugin" */;
|
||||
productName = SDWebImagePINPlugin;
|
||||
};
|
||||
E265D0C12C7D217000D2BB8E /* MPVKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = E265D0C02C7D217000D2BB8E /* XCRemoteSwiftPackageReference "MPVKit" */;
|
||||
productName = MPVKit;
|
||||
};
|
||||
E265D0C32C7D218A00D2BB8E /* MPVKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = E265D0C02C7D217000D2BB8E /* XCRemoteSwiftPackageReference "MPVKit" */;
|
||||
productName = MPVKit;
|
||||
};
|
||||
E265D0C52C7D21A300D2BB8E /* MPVKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = E265D0C02C7D217000D2BB8E /* XCRemoteSwiftPackageReference "MPVKit" */;
|
||||
productName = MPVKit;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
|
||||
/* Begin XCVersionGroup section */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "1f99971d9d21cffe56d0033bc5c38e9fcd5ff46ca5f7d19c76f5ba0a268ce4b6",
|
||||
"originHash" : "515d8e68c4a31658288fb3f94789ee539399b042082c08c39f4c03c27fd8860c",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "activelabel.swift",
|
||||
@@ -25,7 +25,7 @@
|
||||
"location" : "https://github.com/hyperoslo/Cache.git",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "d2e8f5a53c601b43371fdc90277d7f64b0e89a25"
|
||||
"revision" : "81a0277cbc6b63f4e0cd6f42c4abefa1011bbfa9"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -58,10 +58,10 @@
|
||||
{
|
||||
"identity" : "mpvkit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/cxfksword/MPVKit",
|
||||
"location" : "https://github.com/mpvkit/MPVKit.git",
|
||||
"state" : {
|
||||
"revision" : "f646e4b625e9c8a2ff22a7e0bb5557306300be5d",
|
||||
"version" : "0.38.0"
|
||||
"revision" : "ee72059235566df8b455bff15e3f83a1c9053e78",
|
||||
"version" : "0.38.0-fix"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -66,6 +66,11 @@
|
||||
value = "Yes"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "IDELogRedirectionPolicy"
|
||||
value = "oslogToStdio"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
import Logging
|
||||
import UIKit
|
||||
|
||||
final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var orientationLock = UIInterfaceOrientationMask.all
|
||||
|
||||
private var logger = Logger(label: "stream.yattee.app.delegalate")
|
||||
private(set) static var instance: AppDelegate!
|
||||
|
||||
func application(_: UIApplication, supportedInterfaceOrientationsFor _: UIWindow?) -> UIInterfaceOrientationMask {
|
||||
@@ -12,11 +15,22 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { // swiftlint:disable:this discouraged_optional_collection
|
||||
Self.instance = self
|
||||
#if os(iOS)
|
||||
UIViewController.swizzleHomeIndicatorProperty()
|
||||
|
||||
#if !os(macOS)
|
||||
UIViewController.swizzleHomeIndicatorProperty()
|
||||
OrientationTracker.shared.startDeviceOrientationTracking()
|
||||
|
||||
// Configure the audio session for playback
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback)
|
||||
} catch {
|
||||
logger.error("Failed to set audio session category: \(error)")
|
||||
}
|
||||
|
||||
// Begin receiving remote control events
|
||||
UIApplication.shared.beginReceivingRemoteControlEvents()
|
||||
#endif
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Cocoa
|
||||
import MPVKit
|
||||
import Libmpv
|
||||
import OpenGL.GL
|
||||
import OpenGL.GL3
|
||||
|
||||
|
||||
Reference in New Issue
Block a user