mirror of
https://github.com/yattee/yattee.git
synced 2025-12-16 13:08: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
|
## Build 194
|
||||||
* Improved stream resolution handling by @stonerl in https://github.com/yattee/yattee/pull/747
|
* Gestures: swipe up toggles fullscreen by @stonerl in https://github.com/yattee/yattee/pull/778
|
||||||
* Fix some potential crashes by @stonerl in https://github.com/yattee/yattee/pull/748
|
* don’t open video when dismissing context menu by @stonerl in https://github.com/yattee/yattee/pull/780
|
||||||
* Fix regression and improve curentChapter handling by @stonerl in https://github.com/yattee/yattee/pull/749
|
* mpv: remove video layer when entering background by @stonerl in https://github.com/yattee/yattee/pull/793
|
||||||
* Refined chapter font scaling by @stonerl in https://github.com/yattee/yattee/pull/750
|
* 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
|
## Previous builds
|
||||||
* Add skip, play/pause, and fullscreen shortcuts to macOS player (by @rickykresslein)
|
* Add skip, play/pause, and fullscreen shortcuts to macOS player (by @rickykresslein)
|
||||||
@@ -21,6 +24,25 @@
|
|||||||
* Add import export of missing settings
|
* Add import export of missing settings
|
||||||
* macOS: Fix settings windows layout
|
* macOS: Fix settings windows layout
|
||||||
* Fix seek OSD layout on tvOS, revert OSD position
|
* 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
|
* 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
|
* 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
|
* 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)
|
artifactory (3.0.17)
|
||||||
atomos (0.1.3)
|
atomos (0.1.3)
|
||||||
aws-eventstream (1.3.0)
|
aws-eventstream (1.3.0)
|
||||||
aws-partitions (1.968.0)
|
aws-partitions (1.970.0)
|
||||||
aws-sdk-core (3.201.5)
|
aws-sdk-core (3.203.0)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.651.0)
|
aws-partitions (~> 1, >= 1.651.0)
|
||||||
aws-sigv4 (~> 1.9)
|
aws-sigv4 (~> 1.9)
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.88.0)
|
aws-sdk-kms (1.89.0)
|
||||||
aws-sdk-core (~> 3, >= 3.201.0)
|
aws-sdk-core (~> 3, >= 3.203.0)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-s3 (1.159.0)
|
aws-sdk-s3 (1.160.0)
|
||||||
aws-sdk-core (~> 3, >= 3.201.0)
|
aws-sdk-core (~> 3, >= 3.203.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sigv4 (1.9.1)
|
aws-sigv4 (1.9.1)
|
||||||
@@ -171,8 +171,7 @@ GEM
|
|||||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
uber (< 0.2.0)
|
uber (< 0.2.0)
|
||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
rexml (3.3.6)
|
rexml (3.3.7)
|
||||||
strscan
|
|
||||||
rouge (2.0.7)
|
rouge (2.0.7)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
rubyzip (2.3.2)
|
rubyzip (2.3.2)
|
||||||
@@ -185,7 +184,6 @@ GEM
|
|||||||
simctl (1.6.10)
|
simctl (1.6.10)
|
||||||
CFPropertyList
|
CFPropertyList
|
||||||
naturally
|
naturally
|
||||||
strscan (3.1.0)
|
|
||||||
terminal-notifier (2.0.0)
|
terminal-notifier (2.0.0)
|
||||||
terminal-table (3.0.2)
|
terminal-table (3.0.2)
|
||||||
unicode-display_width (>= 1.1.1, < 3)
|
unicode-display_width (>= 1.1.1, < 3)
|
||||||
|
|||||||
@@ -10,11 +10,28 @@ struct AccountsBridge: Defaults.Bridge {
|
|||||||
return nil
|
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 [
|
return [
|
||||||
"id": value.id,
|
"id": value.id,
|
||||||
"instanceID": value.instanceID ?? "",
|
"instanceID": value.instanceID ?? "",
|
||||||
"name": value.name,
|
"name": value.name,
|
||||||
"apiURL": value.urlString,
|
"apiURL": sanitizedUrlString,
|
||||||
"username": value.username,
|
"username": value.username,
|
||||||
"password": value.password ?? ""
|
"password": value.password ?? ""
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -247,27 +247,27 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func feed(_ page: Int?) -> Resource? {
|
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))
|
.withParam("page", String(page ?? 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
var feed: Resource? {
|
var feed: Resource? {
|
||||||
resource(baseURL: account.url, path: basePathAppending("auth/feed"))
|
resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/feed"))
|
||||||
}
|
}
|
||||||
|
|
||||||
var subscriptions: Resource? {
|
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 = {}) {
|
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)
|
.child(channelID)
|
||||||
.request(.post)
|
.request(.post)
|
||||||
.onCompletion { _ in onCompletion() }
|
.onCompletion { _ in onCompletion() }
|
||||||
}
|
}
|
||||||
|
|
||||||
func unsubscribe(_ channelID: String, onCompletion: @escaping () -> Void) {
|
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)
|
.child(channelID)
|
||||||
.request(.delete)
|
.request(.delete)
|
||||||
.onCompletion { _ in onCompletion() }
|
.onCompletion { _ in onCompletion() }
|
||||||
@@ -308,11 +308,11 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
return nil
|
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? {
|
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? {
|
func playlistVideos(_ id: String) -> Resource? {
|
||||||
@@ -445,6 +445,9 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
|
|
||||||
urlComponents.scheme = instanceURLComponents.scheme
|
urlComponents.scheme = instanceURLComponents.scheme
|
||||||
urlComponents.host = instanceURLComponents.host
|
urlComponents.host = instanceURLComponents.host
|
||||||
|
urlComponents.user = instanceURLComponents.user
|
||||||
|
urlComponents.password = instanceURLComponents.password
|
||||||
|
urlComponents.port = instanceURLComponents.port
|
||||||
|
|
||||||
guard let url = urlComponents.url else {
|
guard let url = urlComponents.url else {
|
||||||
return nil
|
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] {
|
private func extractThumbnails(from details: JSON) -> [Thumbnail] {
|
||||||
details["videoThumbnails"].arrayValue.compactMap { json in
|
details["videoThumbnails"].arrayValue.compactMap { json in
|
||||||
guard let url = json["url"].url,
|
guard let url = json["url"].url,
|
||||||
@@ -563,13 +590,20 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// some of instances are not configured properly and return thumbnails links
|
// Some instances are not configured properly and return thumbnail links
|
||||||
// with incorrect scheme
|
// with an incorrect scheme or a missing port.
|
||||||
components.scheme = accountUrlComponents.scheme
|
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 {
|
guard let thumbnailUrl = components.url else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
print("Final thumbnail URL: \(thumbnailUrl)")
|
||||||
|
|
||||||
return Thumbnail(url: thumbnailUrl, quality: .init(rawValue: quality)!)
|
return Thumbnail(url: thumbnailUrl, quality: .init(rawValue: quality)!)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ final class AVPlayerBackend: PlayerBackend {
|
|||||||
|
|
||||||
private var frequentTimeObserver: Any?
|
private var frequentTimeObserver: Any?
|
||||||
private var infrequentTimeObserver: Any?
|
private var infrequentTimeObserver: Any?
|
||||||
private var playerTimeControlStatusObserver: Any?
|
private var playerTimeControlStatusObserver: NSKeyValueObservation?
|
||||||
|
|
||||||
private var statusObservation: NSKeyValueObservation?
|
private var statusObservation: NSKeyValueObservation?
|
||||||
|
|
||||||
@@ -119,10 +119,30 @@ final class AVPlayerBackend: PlayerBackend {
|
|||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
controller.player = avPlayer
|
controller.player = avPlayer
|
||||||
#endif
|
#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 {
|
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(
|
func playStream(
|
||||||
@@ -344,7 +364,11 @@ final class AVPlayerBackend: PlayerBackend {
|
|||||||
|
|
||||||
let startPlaying = {
|
let startPlaying = {
|
||||||
#if !os(macOS)
|
#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
|
#endif
|
||||||
|
|
||||||
self.setRate(self.model.currentRate)
|
self.setRate(self.model.currentRate)
|
||||||
@@ -779,7 +803,7 @@ final class AVPlayerBackend: PlayerBackend {
|
|||||||
opened = true
|
opened = true
|
||||||
controller.startPictureInPicture()
|
controller.startPictureInPicture()
|
||||||
} else {
|
} 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 CoreMedia
|
||||||
import Defaults
|
import Defaults
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Libmpv
|
||||||
import Logging
|
import Logging
|
||||||
import MediaPlayer
|
import MediaPlayer
|
||||||
import MPVKit
|
|
||||||
import Repeat
|
import Repeat
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@@ -248,13 +248,6 @@ final class MPVBackend: PlayerBackend {
|
|||||||
#if !os(macOS)
|
#if !os(macOS)
|
||||||
do {
|
do {
|
||||||
try AVAudioSession.sharedInstance().setActive(true)
|
try AVAudioSession.sharedInstance().setActive(true)
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(
|
|
||||||
self,
|
|
||||||
selector: #selector(self.handleAudioSessionInterruption(_:)),
|
|
||||||
name: AVAudioSession.interruptionNotification,
|
|
||||||
object: nil
|
|
||||||
)
|
|
||||||
} catch {
|
} catch {
|
||||||
self.logger.error("Error setting up audio session: \(error)")
|
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)")
|
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 CoreMedia
|
||||||
import Defaults
|
import Defaults
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Libmpv
|
||||||
import Logging
|
import Logging
|
||||||
import MPVKit
|
|
||||||
#if !os(macOS)
|
#if !os(macOS)
|
||||||
import Siesta
|
import Siesta
|
||||||
import UIKit
|
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-analyzeduration", "1"))
|
||||||
checkError(mpv_set_option_string(mpv, "demuxer-lavf-probe-info", Defaults[.mpvDemuxerLavfProbeInfo]))
|
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))
|
checkError(mpv_initialize(mpv))
|
||||||
|
|
||||||
let api = UnsafeMutableRawPointer(mutating: (MPV_RENDER_API_TYPE_OPENGL as NSString).utf8String)
|
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
|
// Filter out non-HLS streams and streams with resolution more than maxResolution
|
||||||
let nonHLSStreams = streams.filter {
|
let nonHLSStreams = streams.filter {
|
||||||
let isHLS = $0.kind == .hls
|
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("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)")
|
logger.info("Is HLS: \(isHLS), Is within resolution: \(isWithinResolution)")
|
||||||
return !isHLS && isWithinResolution
|
return !isHLS && isWithinResolution
|
||||||
@@ -187,7 +188,8 @@ extension PlayerBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let filteredStreams = adjustedStreams.filter { stream in
|
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)")
|
logger.info("Filtered stream ID: \(stream.id) - Is within max resolution: \(isWithinResolution)")
|
||||||
return isWithinResolution
|
return isWithinResolution
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
|
|
||||||
static var shared = PlayerModel()
|
static var shared = PlayerModel()
|
||||||
|
|
||||||
let logger = Logger(label: "stream.yattee.app")
|
let logger = Logger(label: "stream.yattee.player.model")
|
||||||
|
|
||||||
var playerItem: AVPlayerItem?
|
var playerItem: AVPlayerItem?
|
||||||
|
|
||||||
@@ -56,6 +56,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
@Published var presentingPlayer = false { didSet { handlePresentationChange() } }
|
@Published var presentingPlayer = false { didSet { handlePresentationChange() } }
|
||||||
@Published var activeBackend = PlayerBackendType.mpv
|
@Published var activeBackend = PlayerBackendType.mpv
|
||||||
@Published var forceBackendOnPlay: PlayerBackendType?
|
@Published var forceBackendOnPlay: PlayerBackendType?
|
||||||
|
@Published var wasFullscreen = false
|
||||||
|
|
||||||
var avPlayerBackend = AVPlayerBackend()
|
var avPlayerBackend = AVPlayerBackend()
|
||||||
var mpvBackend = MPVBackend()
|
var mpvBackend = MPVBackend()
|
||||||
@@ -203,6 +204,14 @@ final class PlayerModel: ObservableObject {
|
|||||||
#if !os(macOS)
|
#if !os(macOS)
|
||||||
mpvBackend.controller = mpvController
|
mpvBackend.controller = mpvController
|
||||||
mpvBackend.client = mpvController.client
|
mpvBackend.client = mpvController.client
|
||||||
|
|
||||||
|
// Register for audio session interruption notifications
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(handleAudioSessionInterruption(_:)),
|
||||||
|
name: AVAudioSession.interruptionNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
playbackMode = Defaults[.playbackMode]
|
playbackMode = Defaults[.playbackMode]
|
||||||
@@ -219,6 +228,12 @@ final class PlayerModel: ObservableObject {
|
|||||||
currentRate = playerRate
|
currentRate = playerRate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !os(macOS)
|
||||||
|
deinit {
|
||||||
|
NotificationCenter.default.removeObserver(self, name: AVAudioSession.interruptionNotification, object: nil)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
func show() {
|
func show() {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
if presentingPlayer {
|
if presentingPlayer {
|
||||||
@@ -678,38 +693,24 @@ final class PlayerModel: ObservableObject {
|
|||||||
avPlayerBackend.startPictureInPictureOnPlay = false
|
avPlayerBackend.startPictureInPictureOnPlay = false
|
||||||
avPlayerBackend.startPictureInPictureOnSwitch = false
|
avPlayerBackend.startPictureInPictureOnSwitch = false
|
||||||
|
|
||||||
if activeBackend == .appleAVPlayer {
|
guard activeBackend != .appleAVPlayer else {
|
||||||
avPlayerBackend.tryStartingPictureInPicture()
|
avPlayerBackend.tryStartingPictureInPicture()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// First, we need to create an array with supported formats.
|
avPlayerBackend.startPictureInPictureOnSwitch = true
|
||||||
let formatOrderPiP: [QualityProfile.Format] = [.hls, .stream, .mp4]
|
|
||||||
|
|
||||||
guard let video = currentVideo else { return }
|
saveTime {
|
||||||
guard let stream = avPlayerBackend.bestPlayable(availableStreams, maxResolution: .hd720p30, formatOrder: formatOrderPiP) else { return }
|
self.changeActiveBackend(from: .mpv, to: .appleAVPlayer)
|
||||||
|
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
|
||||||
if avPlayerBackend.video == video {
|
if let pipController = self?.pipController, pipController.isPictureInPictureActive, self?.avPlayerBackend.isPlaying == true {
|
||||||
if activeBackend != .appleAVPlayer {
|
self?.exitFullScreen()
|
||||||
avPlayerBackend.startPictureInPictureOnSwitch = true
|
self?.controls.objectWillChange.send()
|
||||||
}
|
timer.invalidate()
|
||||||
changeActiveBackend(from: activeBackend, to: .appleAVPlayer)
|
} else if self?.activeBackend == .appleAVPlayer, self?.avPlayerBackend.startPictureInPictureOnSwitch == false {
|
||||||
} else {
|
self?.avPlayerBackend.startPictureInPictureOnSwitch = true
|
||||||
avPlayerBackend.startPictureInPictureOnPlay = true
|
self?.avPlayerBackend.tryStartingPictureInPicture()
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -739,19 +740,27 @@ final class PlayerModel: ObservableObject {
|
|||||||
show()
|
show()
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if previousActiveBackend == .mpv {
|
avPlayerBackend.closePiP()
|
||||||
saveTime {
|
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
|
||||||
self.changeActiveBackend(from: self.activeBackend, to: .mpv, isInClosePip: true)
|
if self?.activeBackend == .appleAVPlayer, self?.avPlayerBackend.isPlaying == true, self?.playingInPictureInPicture == false {
|
||||||
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
|
timer.invalidate()
|
||||||
if self?.activeBackend == .mpv, self?.mpvBackend.isPlaying == true {
|
}
|
||||||
self?.backend.closePiP()
|
}
|
||||||
self?.controls.resetTimer()
|
|
||||||
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() {
|
func updateRemoteCommandCenter() {
|
||||||
let skipForwardCommand = MPRemoteCommandCenter.shared().skipForwardCommand
|
let commandCenter = MPRemoteCommandCenter.shared()
|
||||||
let skipBackwardCommand = MPRemoteCommandCenter.shared().skipBackwardCommand
|
let skipForwardCommand = commandCenter.skipForwardCommand
|
||||||
let previousTrackCommand = MPRemoteCommandCenter.shared().previousTrackCommand
|
let skipBackwardCommand = commandCenter.skipBackwardCommand
|
||||||
let nextTrackCommand = MPRemoteCommandCenter.shared().nextTrackCommand
|
let previousTrackCommand = commandCenter.previousTrackCommand
|
||||||
|
let nextTrackCommand = commandCenter.nextTrackCommand
|
||||||
|
|
||||||
if !remoteCommandCenterConfigured {
|
if !remoteCommandCenterConfigured {
|
||||||
remoteCommandCenterConfigured = true
|
remoteCommandCenterConfigured = true
|
||||||
|
|
||||||
#if !os(macOS)
|
|
||||||
try? AVAudioSession.sharedInstance().setCategory(
|
|
||||||
.playback,
|
|
||||||
mode: .moviePlayback
|
|
||||||
)
|
|
||||||
|
|
||||||
UIApplication.shared.beginReceivingRemoteControlEvents()
|
|
||||||
#endif
|
|
||||||
|
|
||||||
let interval = TimeInterval(systemControlsSeekDuration) ?? 10
|
let interval = TimeInterval(systemControlsSeekDuration) ?? 10
|
||||||
let preferredIntervals = [NSNumber(value: interval)]
|
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
|
skipForwardCommand.preferredIntervals = preferredIntervals
|
||||||
skipBackwardCommand.preferredIntervals = preferredIntervals
|
skipBackwardCommand.preferredIntervals = preferredIntervals
|
||||||
|
|
||||||
@@ -923,22 +935,22 @@ final class PlayerModel: ObservableObject {
|
|||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
|
|
||||||
MPRemoteCommandCenter.shared().playCommand.addTarget { [weak self] _ in
|
commandCenter.playCommand.addTarget { [weak self] _ in
|
||||||
self?.play()
|
self?.play()
|
||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
|
|
||||||
MPRemoteCommandCenter.shared().pauseCommand.addTarget { [weak self] _ in
|
commandCenter.pauseCommand.addTarget { [weak self] _ in
|
||||||
self?.pause()
|
self?.pause()
|
||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
|
|
||||||
MPRemoteCommandCenter.shared().togglePlayPauseCommand.addTarget { [weak self] _ in
|
commandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in
|
||||||
self?.togglePlay()
|
self?.togglePlay()
|
||||||
return .success
|
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 }
|
guard let event = remoteEvent as? MPChangePlaybackPositionCommandEvent else { return .commandFailed }
|
||||||
|
|
||||||
self?.backend.seek(to: event.positionTime, seekType: .userInteracted)
|
self?.backend.seek(to: event.positionTime, seekType: .userInteracted)
|
||||||
@@ -975,24 +987,53 @@ final class PlayerModel: ObservableObject {
|
|||||||
func handleEnterForeground() {
|
func handleEnterForeground() {
|
||||||
setNeedsDrawing(presentingPlayer)
|
setNeedsDrawing(presentingPlayer)
|
||||||
|
|
||||||
if !musicMode, activeBackend == .appleAVPlayer {
|
if !musicMode, activeBackend == .mpv {
|
||||||
|
mpvBackend.addVideoTrackFromStream()
|
||||||
|
mpvBackend.setVideoToAuto()
|
||||||
|
mpvBackend.controls.resetTimer()
|
||||||
|
} else if !musicMode, activeBackend == .appleAVPlayer {
|
||||||
avPlayerBackend.bindPlayerToLayer()
|
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 {
|
guard closePiPAndOpenPlayerOnEnteringForeground, playingInPictureInPicture else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
show()
|
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() {
|
func handleEnterBackground() {
|
||||||
if Defaults[.pauseOnEnteringBackground], !playingInPictureInPicture, !musicMode {
|
if Defaults[.pauseOnEnteringBackground], !playingInPictureInPicture, !musicMode {
|
||||||
pause()
|
pause()
|
||||||
} else if !playingInPictureInPicture {
|
} else if !playingInPictureInPicture, activeBackend == .appleAVPlayer {
|
||||||
avPlayerBackend.removePlayerFromLayer()
|
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
|
#endif
|
||||||
|
|
||||||
@@ -1017,18 +1058,22 @@ final class PlayerModel: ObservableObject {
|
|||||||
guard activeBackend == .mpv else { return }
|
guard activeBackend == .mpv else { return }
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if os(iOS)
|
|
||||||
if activeBackend == .appleAVPlayer, avPlayerUsesSystemControls {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
guard let video = currentItem?.video else {
|
guard let video = currentItem?.video else {
|
||||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = .none
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = .none
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentTime = (backend.currentTime?.seconds.isFinite ?? false) ? backend.currentTime!.seconds : 0
|
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] = [
|
var nowPlayingInfo: [String: AnyObject] = [
|
||||||
MPMediaItemPropertyTitle: video.displayTitle as AnyObject,
|
MPMediaItemPropertyTitle: video.displayTitle as AnyObject,
|
||||||
MPMediaItemPropertyArtist: video.displayAuthor as AnyObject,
|
MPMediaItemPropertyArtist: video.displayAuthor as AnyObject,
|
||||||
@@ -1036,7 +1081,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
MPNowPlayingInfoPropertyElapsedPlaybackTime: currentTime as AnyObject,
|
MPNowPlayingInfoPropertyElapsedPlaybackTime: currentTime as AnyObject,
|
||||||
MPNowPlayingInfoPropertyPlaybackQueueCount: queue.count as AnyObject,
|
MPNowPlayingInfoPropertyPlaybackQueueCount: queue.count as AnyObject,
|
||||||
MPNowPlayingInfoPropertyPlaybackQueueIndex: 1 as AnyObject,
|
MPNowPlayingInfoPropertyPlaybackQueueIndex: 1 as AnyObject,
|
||||||
MPMediaItemPropertyMediaType: MPMediaType.anyVideo.rawValue as AnyObject
|
MPMediaItemPropertyMediaType: mediaType
|
||||||
]
|
]
|
||||||
|
|
||||||
if !currentArtwork.isNil {
|
if !currentArtwork.isNil {
|
||||||
@@ -1057,7 +1102,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
|
|
||||||
func updateCurrentArtwork() {
|
func updateCurrentArtwork() {
|
||||||
guard let video = currentVideo,
|
guard let video = currentVideo,
|
||||||
let thumbnailURL = video.thumbnailURL(quality: .medium)
|
let thumbnailURL = video.thumbnailURL(quality: Constants.isIPhone ? .medium : .maxres)
|
||||||
else {
|
else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1203,6 +1248,42 @@ final class PlayerModel: ObservableObject {
|
|||||||
return nil
|
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)
|
#if os(macOS)
|
||||||
private func assignKeyPressMonitor() {
|
private func assignKeyPressMonitor() {
|
||||||
keyPressMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { keyEvent -> NSEvent? in
|
keyPressMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { keyEvent -> NSEvent? in
|
||||||
@@ -1240,7 +1321,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func destroyKeyPressMonitor() {
|
private func destroyKeyPressMonitor() {
|
||||||
if let keyPressMonitor = keyPressMonitor {
|
if let keyPressMonitor {
|
||||||
NSEvent.removeMonitor(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))
|
static var defaultProfile = Self(id: "default", backend: .mpv, resolution: .hd720p60, formats: [.stream], order: Array(Format.allCases.indices))
|
||||||
|
|
||||||
enum Format: String, CaseIterable, Identifiable, Defaults.Serializable {
|
enum Format: String, CaseIterable, Identifiable, Defaults.Serializable {
|
||||||
case hls
|
|
||||||
case stream
|
|
||||||
case avc1
|
case avc1
|
||||||
|
case stream
|
||||||
|
case webm
|
||||||
case mp4
|
case mp4
|
||||||
case av1
|
case av1
|
||||||
case webm
|
case hls
|
||||||
|
|
||||||
var id: String {
|
var id: String {
|
||||||
rawValue
|
rawValue
|
||||||
@@ -30,18 +30,18 @@ struct QualityProfile: Hashable, Identifiable, Defaults.Serializable {
|
|||||||
|
|
||||||
var streamFormat: Stream.Format? {
|
var streamFormat: Stream.Format? {
|
||||||
switch self {
|
switch self {
|
||||||
case .hls:
|
|
||||||
return nil
|
|
||||||
case .stream:
|
|
||||||
return nil
|
|
||||||
case .avc1:
|
case .avc1:
|
||||||
return .avc1
|
return .avc1
|
||||||
|
case .stream:
|
||||||
|
return nil
|
||||||
|
case .webm:
|
||||||
|
return .webm
|
||||||
case .mp4:
|
case .mp4:
|
||||||
return .mp4
|
return .mp4
|
||||||
case .av1:
|
case .av1:
|
||||||
return .av1
|
return .av1
|
||||||
case .webm:
|
case .hls:
|
||||||
return .webm
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,14 +59,16 @@ struct QualityProfile: Hashable, Identifiable, Defaults.Serializable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var formatsDescription: String {
|
var formatsDescription: String {
|
||||||
if formats.count == Format.allCases.count {
|
switch formats.count {
|
||||||
|
case Format.allCases.count:
|
||||||
return "Any format".localized()
|
return "Any format".localized()
|
||||||
}
|
case 0:
|
||||||
if formats.count <= 3 {
|
return "No format selected".localized()
|
||||||
|
case 1 ... 3:
|
||||||
return formats.map(\.description).joined(separator: ", ")
|
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 {
|
func isPreferred(_ stream: Stream) -> Bool {
|
||||||
|
|||||||
@@ -5,10 +5,27 @@ final class ThumbnailsModel: ObservableObject {
|
|||||||
static var shared = ThumbnailsModel()
|
static var shared = ThumbnailsModel()
|
||||||
|
|
||||||
@Published var unloadable = Set<URL>()
|
@Published var unloadable = Set<URL>()
|
||||||
|
private var retryCounts = [URL: Int]()
|
||||||
|
private let maxRetries = 3
|
||||||
|
private let retryDelay: TimeInterval = 1.0
|
||||||
|
|
||||||
func insertUnloadable(_ url: URL) {
|
func insertUnloadable(_ url: URL) {
|
||||||
DispatchQueue.main.async {
|
let retries = (retryCounts[url] ?? 0) + 1
|
||||||
self.unloadable.insert(url)
|
|
||||||
|
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" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "Invidious.svg",
|
"filename" : "Invidious_512x512@1x.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "Invidious_512x512@2x.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "2x"
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "Invidious_512x512@3x.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "3x"
|
"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
|
#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 {
|
static var progressViewScale: Double {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
0.4
|
0.4
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ extension Defaults.Keys {
|
|||||||
static let showOpenActionsToolbarItem = Key<Bool>("showOpenActionsToolbarItem", default: false)
|
static let showOpenActionsToolbarItem = Key<Bool>("showOpenActionsToolbarItem", default: false)
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
static let showDocuments = Key<Bool>("showDocuments", default: false)
|
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
|
#endif
|
||||||
|
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
static let accountPickerDisplaysUsernameDefault = true
|
static let accountPickerDisplaysUsernameDefault = true
|
||||||
#else
|
#else
|
||||||
static let accountPickerDisplaysUsernameDefault = UIDevice.current.userInterfaceIdiom == .pad
|
static let accountPickerDisplaysUsernameDefault = Constants.isIPad
|
||||||
#endif
|
#endif
|
||||||
static let accountPickerDisplaysUsername = Key<Bool>("accountPickerDisplaysUsername", default: accountPickerDisplaysUsernameDefault)
|
static let accountPickerDisplaysUsername = Key<Bool>("accountPickerDisplaysUsername", default: accountPickerDisplaysUsernameDefault)
|
||||||
#endif
|
#endif
|
||||||
@@ -41,9 +41,9 @@ extension Defaults.Keys {
|
|||||||
static let showChannelAvatarInVideosListing = Key<Bool>("showChannelAvatarInVideosListing", default: true)
|
static let showChannelAvatarInVideosListing = Key<Bool>("showChannelAvatarInVideosListing", default: true)
|
||||||
|
|
||||||
static let playerButtonSingleTapGesture = Key<PlayerTapGestureAction>("playerButtonSingleTapGesture", default: .togglePlayer)
|
static let playerButtonSingleTapGesture = Key<PlayerTapGestureAction>("playerButtonSingleTapGesture", default: .togglePlayer)
|
||||||
static let playerButtonDoubleTapGesture = Key<PlayerTapGestureAction>("playerButtonDoubleTapGesture", default: .nothing)
|
static let playerButtonDoubleTapGesture = Key<PlayerTapGestureAction>("playerButtonDoubleTapGesture", default: .togglePlayerVisibility)
|
||||||
static let playerButtonShowsControlButtonsWhenMinimized = Key<Bool>("playerButtonShowsControlButtonsWhenMinimized", default: false)
|
static let playerButtonShowsControlButtonsWhenMinimized = Key<Bool>("playerButtonShowsControlButtonsWhenMinimized", default: true)
|
||||||
static let playerButtonIsExpanded = Key<Bool>("playerButtonIsExpanded", default: false)
|
static let playerButtonIsExpanded = Key<Bool>("playerButtonIsExpanded", default: true)
|
||||||
static let playerBarMaxWidth = Key<String>("playerBarMaxWidth", default: "600")
|
static let playerBarMaxWidth = Key<String>("playerBarMaxWidth", default: "600")
|
||||||
static let channelOnThumbnail = Key<Bool>("channelOnThumbnail", default: false)
|
static let channelOnThumbnail = Key<Bool>("channelOnThumbnail", default: false)
|
||||||
static let timeOnThumbnail = Key<Bool>("timeOnThumbnail", default: true)
|
static let timeOnThumbnail = Key<Bool>("timeOnThumbnail", default: true)
|
||||||
@@ -64,7 +64,7 @@ extension Defaults.Keys {
|
|||||||
static let closeVideoOnEOF = Key<Bool>("closeVideoOnEOF", default: false)
|
static let closeVideoOnEOF = Key<Bool>("closeVideoOnEOF", default: false)
|
||||||
|
|
||||||
#if !os(macOS)
|
#if !os(macOS)
|
||||||
static let pauseOnEnteringBackground = Key<Bool>("pauseOnEnteringBackground", default: true)
|
static let pauseOnEnteringBackground = Key<Bool>("pauseOnEnteringBackground", default: false)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
@@ -79,7 +79,7 @@ extension Defaults.Keys {
|
|||||||
|
|
||||||
static let showChapters = Key<Bool>("showChapters", default: true)
|
static let showChapters = Key<Bool>("showChapters", default: true)
|
||||||
static let showChapterThumbnails = Key<Bool>("showChapterThumbnails", 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 expandChapters = Key<Bool>("expandChapters", default: true)
|
||||||
static let showRelated = Key<Bool>("showRelated", default: true)
|
static let showRelated = Key<Bool>("showRelated", default: true)
|
||||||
static let showInspector = Key<ShowInspectorSetting>("showInspector", default: .onlyLocal)
|
static let showInspector = Key<ShowInspectorSetting>("showInspector", default: .onlyLocal)
|
||||||
@@ -94,10 +94,10 @@ extension Defaults.Keys {
|
|||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
static let honorSystemOrientationLock = Key<Bool>("honorSystemOrientationLock", default: true)
|
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>(
|
static let rotateToLandscapeOnEnterFullScreen = Key<FullScreenRotationSetting>(
|
||||||
"rotateToLandscapeOnEnterFullScreen",
|
"rotateToLandscapeOnEnterFullScreen",
|
||||||
default: UIDevice.current.userInterfaceIdiom == .phone ? .landscapeRight : .disabled
|
default: Constants.isIPhone ? .landscapeRight : .disabled
|
||||||
)
|
)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -116,14 +116,14 @@ extension Defaults.Keys {
|
|||||||
|
|
||||||
// MARK: GROUP - Controls
|
// 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 horizontalPlayerGestureEnabled = Key<Bool>("horizontalPlayerGestureEnabled", default: true)
|
||||||
static let seekGestureSensitivity = Key<Double>("seekGestureSensitivity", default: 30.0)
|
static let seekGestureSensitivity = Key<Double>("seekGestureSensitivity", default: 30.0)
|
||||||
static let seekGestureSpeed = Key<Double>("seekGestureSpeed", default: 0.5)
|
static let seekGestureSpeed = Key<Double>("seekGestureSpeed", default: 0.5)
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
static let playerControlsLayoutDefault = UIDevice.current.userInterfaceIdiom == .pad ? PlayerControlsLayout.medium : .small
|
static let playerControlsLayoutDefault = Constants.isIPad ? PlayerControlsLayout.medium : .small
|
||||||
static let fullScreenPlayerControlsLayoutDefault = UIDevice.current.userInterfaceIdiom == .pad ? PlayerControlsLayout.medium : .small
|
static let fullScreenPlayerControlsLayoutDefault = Constants.isIPad ? PlayerControlsLayout.medium : .small
|
||||||
#elseif os(tvOS)
|
#elseif os(tvOS)
|
||||||
static let playerControlsLayoutDefault = PlayerControlsLayout.tvRegular
|
static let playerControlsLayoutDefault = PlayerControlsLayout.tvRegular
|
||||||
static let fullScreenPlayerControlsLayoutDefault = PlayerControlsLayout.tvRegular
|
static let fullScreenPlayerControlsLayoutDefault = PlayerControlsLayout.tvRegular
|
||||||
@@ -175,61 +175,152 @@ extension Defaults.Keys {
|
|||||||
|
|
||||||
// MARK: GROUP - Quality
|
// 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 hd2160p60MPVProfile = QualityProfile(id: "hd2160p60MPVProfile", 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 hd1080p60MPVProfile = QualityProfile(id: "hd1080p60MPVProfile", 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 hd1080pMPVProfile = QualityProfile(id: "hd1080pMPVProfile", backend: .mpv, resolution: .hd1080p30, 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 hd720p60MPVProfile = QualityProfile(id: "hd720p60MPVProfile", backend: .mpv, resolution: .hd720p60, formats: QualityProfile.Format.allCases, 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 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)
|
#if os(iOS)
|
||||||
static let qualityProfilesDefault = UIDevice.current.userInterfaceIdiom == .pad ? [
|
enum QualityProfiles {
|
||||||
hd2160pMPVProfile,
|
// iPad-specific settings
|
||||||
hd1080pMPVProfile,
|
enum iPad {
|
||||||
hd720pMPVProfile,
|
static let qualityProfilesDefault = [
|
||||||
hd720pAVPlayerProfile,
|
hd1080p60MPVProfile,
|
||||||
sd360pAVPlayerProfile
|
hd1080pMPVProfile,
|
||||||
] : [
|
hd720p60MPVProfile,
|
||||||
hd1080pMPVProfile,
|
hd720pMPVProfile
|
||||||
hd720pMPVProfile,
|
]
|
||||||
hd720pAVPlayerProfile,
|
|
||||||
sd360pAVPlayerProfile
|
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)
|
#elseif os(tvOS)
|
||||||
static let qualityProfilesDefault = [
|
enum QualityProfiles {
|
||||||
hd2160pMPVProfile,
|
// tvOS-specific settings
|
||||||
hd1080pMPVProfile,
|
enum tvOS {
|
||||||
hd720pMPVProfile,
|
static let qualityProfilesDefault = [
|
||||||
hd720pAVPlayerProfile
|
hd2160p60MPVProfile,
|
||||||
]
|
hd1080p60MPVProfile,
|
||||||
static let batteryCellularProfileDefault = hd1080pMPVProfile.id
|
hd720p60MPVProfile,
|
||||||
static let batteryNonCellularProfileDefault = hd1080pMPVProfile.id
|
hd720pAVPlayerProfile
|
||||||
static let chargingCellularProfileDefault = hd1080pMPVProfile.id
|
]
|
||||||
static let chargingNonCellularProfileDefault = hd1080pMPVProfile.id
|
|
||||||
|
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
|
#else
|
||||||
static let qualityProfilesDefault = [
|
enum QualityProfiles {
|
||||||
hd2160pMPVProfile,
|
// macOS-specific settings
|
||||||
hd1080pMPVProfile,
|
enum macOS {
|
||||||
hd720pMPVProfile,
|
static let qualityProfilesDefault = [
|
||||||
hd720pAVPlayerProfile
|
hd2160p60MPVProfile,
|
||||||
]
|
hd1080p60MPVProfile,
|
||||||
static let batteryCellularProfileDefault = hd1080pMPVProfile.id
|
hd1080pMPVProfile,
|
||||||
static let batteryNonCellularProfileDefault = hd1080pMPVProfile.id
|
hd720p60MPVProfile
|
||||||
static let chargingCellularProfileDefault = hd1080pMPVProfile.id
|
]
|
||||||
static let chargingNonCellularProfileDefault = hd1080pMPVProfile.id
|
|
||||||
|
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
|
#endif
|
||||||
|
|
||||||
static let batteryCellularProfile = Key<QualityProfile.ID>("batteryCellularProfile", default: batteryCellularProfileDefault)
|
static let batteryCellularProfile = Key<QualityProfile.ID>(
|
||||||
static let batteryNonCellularProfile = Key<QualityProfile.ID>("batteryNonCellularProfile", default: batteryNonCellularProfileDefault)
|
"batteryCellularProfile",
|
||||||
static let chargingCellularProfile = Key<QualityProfile.ID>("chargingCellularProfile", default: chargingCellularProfileDefault)
|
default: QualityProfiles.currentProfile.batteryCellularProfileDefault
|
||||||
static let chargingNonCellularProfile = Key<QualityProfile.ID>("chargingNonCellularProfile", default: chargingNonCellularProfileDefault)
|
)
|
||||||
static let forceAVPlayerForLiveStreams = Key<Bool>("forceAVPlayerForLiveStreams", default: true)
|
static let batteryNonCellularProfile = Key<QualityProfile.ID>(
|
||||||
|
"batteryNonCellularProfile",
|
||||||
static let qualityProfiles = Key<[QualityProfile]>("qualityProfiles", default: qualityProfilesDefault)
|
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
|
// MARK: GROUP - History
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ enum LanguageCodes: String, CaseIterable {
|
|||||||
case Vietnamese = "vi"
|
case Vietnamese = "vi"
|
||||||
case Xhosa = "xh"
|
case Xhosa = "xh"
|
||||||
case Chinese = "zh"
|
case Chinese = "zh"
|
||||||
|
case Chinese_Hans = "zh-Hans"
|
||||||
case Zulu = "zu"
|
case Zulu = "zu"
|
||||||
|
|
||||||
var description: String {
|
var description: String {
|
||||||
@@ -147,6 +148,8 @@ enum LanguageCodes: String, CaseIterable {
|
|||||||
return "Xhosa"
|
return "Xhosa"
|
||||||
case .Chinese:
|
case .Chinese:
|
||||||
return "Chinese"
|
return "Chinese"
|
||||||
|
case .Chinese_Hans:
|
||||||
|
return "Chinese (Simplified)"
|
||||||
case .Zulu:
|
case .Zulu:
|
||||||
return "Zulu"
|
return "Zulu"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -248,31 +248,33 @@ struct PlayerControls: View {
|
|||||||
return [player.playerSize.height - inset, 500].min()!
|
return [player.playerSize.height - inset, 500].min()!
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder var controlsBackground: some View {
|
@ViewBuilder
|
||||||
ZStack {
|
var controlsBackground: some View {
|
||||||
if player.musicMode,
|
GeometryReader { geometry in
|
||||||
let url = controlsBackgroundURL
|
ZStack {
|
||||||
{
|
if player.musicMode,
|
||||||
ThumbnailView(url: url)
|
let video = player.videoForDisplay
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
{
|
||||||
.transition(.opacity)
|
let thumbnail = thumbnails.best(video)
|
||||||
.animation(.default)
|
if let url = thumbnail.url,
|
||||||
} else if player.videoForDisplay == nil {
|
let quality = thumbnail.quality
|
||||||
Color.black
|
{
|
||||||
|
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 {
|
var timeline: some View {
|
||||||
TimelineView(context: .player).foregroundColor(.primary)
|
TimelineView(context: .player).foregroundColor(.primary)
|
||||||
}
|
}
|
||||||
@@ -381,7 +383,7 @@ struct PlayerControls: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var pipButton: some 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)
|
.disabled(!player.pipPossible)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import GLKit
|
import GLKit
|
||||||
|
import Libmpv
|
||||||
import Logging
|
import Logging
|
||||||
import MPVKit
|
|
||||||
import OpenGLES
|
import OpenGLES
|
||||||
|
|
||||||
final class MPVOGLView: GLKView {
|
final class MPVOGLView: GLKView {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ extension VideoPlayerView {
|
|||||||
.updating($dragGestureOffset) { value, state, _ in
|
.updating($dragGestureOffset) { value, state, _ in
|
||||||
guard isVerticalDrag else { return }
|
guard isVerticalDrag else { return }
|
||||||
var translation = value.translation
|
var translation = value.translation
|
||||||
translation.height = max(0, translation.height)
|
translation.height = max(-translation.height, translation.height)
|
||||||
state = translation
|
state = translation
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -18,7 +18,8 @@ extension VideoPlayerView {
|
|||||||
.onChanged { value in
|
.onChanged { value in
|
||||||
guard player.presentingPlayer,
|
guard player.presentingPlayer,
|
||||||
!controlsOverlayModel.presenting,
|
!controlsOverlayModel.presenting,
|
||||||
dragGestureState else { return }
|
dragGestureState,
|
||||||
|
!disableToggleGesture else { return }
|
||||||
|
|
||||||
if player.controls.presentingControls, !player.musicMode {
|
if player.controls.presentingControls, !player.musicMode {
|
||||||
player.controls.presentingControls = false
|
player.controls.presentingControls = false
|
||||||
@@ -61,19 +62,22 @@ extension VideoPlayerView {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard verticalDrag > 0 else { return }
|
// Toggle fullscreen on upward drag only when not disabled
|
||||||
viewDragOffset = verticalDrag
|
if verticalDrag < -50 {
|
||||||
|
if player.playingFullScreen {
|
||||||
if verticalDrag > 60,
|
player.exitFullScreen(showControls: false)
|
||||||
player.playingFullScreen
|
} else {
|
||||||
{
|
player.enterFullScreen()
|
||||||
player.exitFullScreen(showControls: false)
|
}
|
||||||
#if os(iOS)
|
disableGestureTemporarily()
|
||||||
if Constants.isIPhone {
|
return
|
||||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore downward swipes when in fullscreen
|
||||||
|
guard verticalDrag > 0 && !player.playingFullScreen else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
viewDragOffset = verticalDrag
|
||||||
}
|
}
|
||||||
.onEnded { _ in
|
.onEnded { _ in
|
||||||
onPlayerDragGestureEnded()
|
onPlayerDragGestureEnded()
|
||||||
@@ -86,16 +90,6 @@ extension VideoPlayerView {
|
|||||||
player.seek.onSeekGestureEnd()
|
player.seek.onSeekGestureEnd()
|
||||||
}
|
}
|
||||||
|
|
||||||
if viewDragOffset > 60,
|
|
||||||
player.playingFullScreen
|
|
||||||
{
|
|
||||||
#if os(iOS)
|
|
||||||
player.lockedOrientation = nil
|
|
||||||
#endif
|
|
||||||
player.exitFullScreen(showControls: false)
|
|
||||||
viewDragOffset = 0
|
|
||||||
return
|
|
||||||
}
|
|
||||||
isVerticalDrag = false
|
isVerticalDrag = false
|
||||||
|
|
||||||
guard player.presentingPlayer,
|
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 {
|
func isActionable(_ action: Action) -> Bool {
|
||||||
switch action {
|
switch action {
|
||||||
case .share:
|
case .share:
|
||||||
@@ -151,7 +155,7 @@ struct VideoActions: View {
|
|||||||
case .fullScreen:
|
case .fullScreen:
|
||||||
actionButton("Fullscreen", systemImage: player.fullscreenImage, action: player.toggleFullScreenAction)
|
actionButton("Fullscreen", systemImage: player.fullscreenImage, action: player.toggleFullScreenAction)
|
||||||
case .pip:
|
case .pip:
|
||||||
actionButton("PiP", systemImage: player.pipImage, action: player.togglePiPAction)
|
actionButton("PiP", systemImage: player.pipImage, active: player.playingInPictureInPicture, action: player.togglePiPAction)
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
case .lockOrientation:
|
case .lockOrientation:
|
||||||
actionButton("Lock", systemImage: player.lockOrientationImage, active: player.lockedOrientation != nil, action: player.lockOrientationAction)
|
actionButton("Lock", systemImage: player.lockOrientationImage, active: player.lockedOrientation != nil, action: player.lockOrientationAction)
|
||||||
@@ -198,10 +202,10 @@ struct VideoActions: View {
|
|||||||
VStack(spacing: 3) {
|
VStack(spacing: 3) {
|
||||||
Image(systemName: systemImage)
|
Image(systemName: systemImage)
|
||||||
.frame(width: 20, height: 20)
|
.frame(width: 20, height: 20)
|
||||||
.foregroundColor(active ? Color("AppRedColor") : .accentColor)
|
.foregroundColor(active ? Color("AppRedColor") : .primary)
|
||||||
if playerActionsButtonLabelStyle.text {
|
if playerActionsButtonLabelStyle.text {
|
||||||
Text(name.localized())
|
Text(name.localized())
|
||||||
.foregroundColor(active ? Color("AppRedColor") : .secondary)
|
.foregroundColor(active ? Color("AppRedColor") : .primary)
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.allowsTightening(true)
|
.allowsTightening(true)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
|
|||||||
@@ -235,15 +235,22 @@ struct VideoDetails: View {
|
|||||||
)
|
)
|
||||||
#endif
|
#endif
|
||||||
// swiftlint:enable trailing_closure
|
// swiftlint:enable trailing_closure
|
||||||
|
if VideoActions().isAnyActionVisible() {
|
||||||
VideoActions(video: player.videoForDisplay)
|
VideoActions(video: player.videoForDisplay)
|
||||||
.padding(.vertical, 5)
|
.padding(.vertical, 5)
|
||||||
.frame(maxHeight: 50)
|
.frame(maxHeight: 50)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.borderTop(height: 0.5, color: Color("ControlsBorderColor"))
|
.borderTop(height: 0.5, color: Color("ControlsBorderColor"))
|
||||||
.borderBottom(height: 0.5, color: Color("ControlsBorderColor"))
|
.borderBottom(height: 0.5, color: Color("ControlsBorderColor"))
|
||||||
.animation(nil, value: player.currentItem)
|
.animation(nil, value: player.currentItem)
|
||||||
.frame(minWidth: 0, maxWidth: .infinity)
|
.frame(minWidth: 0, maxWidth: .infinity)
|
||||||
|
} else {
|
||||||
|
Rectangle()
|
||||||
|
.fill(Color.clear)
|
||||||
|
.frame(height: 0.5)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.background(Color("ControlsBorderColor"))
|
||||||
|
}
|
||||||
|
|
||||||
ScrollViewReader { proxy in
|
ScrollViewReader { proxy in
|
||||||
pageView
|
pageView
|
||||||
|
|||||||
@@ -47,11 +47,18 @@ struct VideoPlayerView: View {
|
|||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
@GestureState var dragGestureState = false
|
@GestureState var dragGestureState = false
|
||||||
@GestureState var dragGestureOffset = CGSize.zero
|
@GestureState var dragGestureOffset = CGSize.zero
|
||||||
@State var isHorizontalDrag = false // swiftlint:disable:this swiftui_state_private
|
// swiftlint:disable private_swiftui_state
|
||||||
@State var isVerticalDrag = false // swiftlint:disable:this swiftui_state_private
|
@State var isHorizontalDrag = false
|
||||||
@State var viewDragOffset = Self.hiddenOffset // swiftlint:disable:this swiftui_state_private
|
@State var isVerticalDrag = false
|
||||||
|
@State var viewDragOffset = Self.hiddenOffset
|
||||||
|
// swiftlint:enable private_swiftui_state
|
||||||
|
|
||||||
#endif
|
#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
|
@ObservedObject var player = PlayerModel.shared // swiftlint:disable:this swiftui_state_private
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ struct InstanceForm: View {
|
|||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
.keyboardType(.URL)
|
.keyboardType(.URL)
|
||||||
#endif
|
#endif
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
VStack {
|
VStack {
|
||||||
|
|||||||
@@ -329,8 +329,8 @@ struct PlayerSettings: View {
|
|||||||
|
|
||||||
private var rotateToLandscapeOnEnterFullScreenPicker: some View {
|
private var rotateToLandscapeOnEnterFullScreenPicker: some View {
|
||||||
Picker("Rotate when entering fullscreen on landscape video", selection: $rotateToLandscapeOnEnterFullScreen) {
|
Picker("Rotate when entering fullscreen on landscape video", selection: $rotateToLandscapeOnEnterFullScreen) {
|
||||||
Text("Landscape left").tag(FullScreenRotationSetting.landscapeRight)
|
Text("Landscape left").tag(FullScreenRotationSetting.landscapeLeft)
|
||||||
Text("Landscape right").tag(FullScreenRotationSetting.landscapeLeft)
|
Text("Landscape right").tag(FullScreenRotationSetting.landscapeRight)
|
||||||
Text("No rotation").tag(FullScreenRotationSetting.disabled)
|
Text("No rotation").tag(FullScreenRotationSetting.disabled)
|
||||||
}
|
}
|
||||||
.modifier(SettingsPickerModifier())
|
.modifier(SettingsPickerModifier())
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ struct QualityProfileForm: View {
|
|||||||
func isFormatDisabled(_ format: QualityProfile.Format) -> Bool {
|
func isFormatDisabled(_ format: QualityProfile.Format) -> Bool {
|
||||||
guard backend == .appleAVPlayer else { return false }
|
guard backend == .appleAVPlayer else { return false }
|
||||||
|
|
||||||
let avPlayerFormats = [QualityProfile.Format.hls, .stream, .mp4]
|
let avPlayerFormats = [.stream, QualityProfile.Format.hls]
|
||||||
|
|
||||||
return !avPlayerFormats.contains(format)
|
return !avPlayerFormats.contains(format)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,14 +24,42 @@ struct VideoContextMenuView: View {
|
|||||||
|
|
||||||
private var backgroundContext = PersistenceController.shared.container.newBackgroundContext()
|
private var backgroundContext = PersistenceController.shared.container.newBackgroundContext()
|
||||||
|
|
||||||
|
@State private var isOverlayVisible = false
|
||||||
|
|
||||||
init(video: Video) {
|
init(video: Video) {
|
||||||
self.video = video
|
self.video = video
|
||||||
_watchRequest = video.watchFetchRequest
|
_watchRequest = video.watchFetchRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if video.videoID != Video.fixtureID {
|
ZStack {
|
||||||
contextMenu
|
// 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 */; };
|
3797104928D3D10600D5F53C /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 3797104828D3D10600D5F53C /* SDWebImageSwiftUI */; };
|
||||||
3797104B28D3D18800D5F53C /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 3797104A28D3D18800D5F53C /* SDWebImageSwiftUI */; };
|
3797104B28D3D18800D5F53C /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 3797104A28D3D18800D5F53C /* SDWebImageSwiftUI */; };
|
||||||
3797104D28D3D19100D5F53C /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 3797104C28D3D19100D5F53C /* 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 */; };
|
3797757D268922D100DD52A8 /* Siesta in Frameworks */ = {isa = PBXBuildFile; productRef = 3797757C268922D100DD52A8 /* Siesta */; };
|
||||||
37977583268922F600DD52A8 /* InvidiousAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37977582268922F600DD52A8 /* InvidiousAPI.swift */; };
|
37977583268922F600DD52A8 /* InvidiousAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37977582268922F600DD52A8 /* InvidiousAPI.swift */; };
|
||||||
37977584268922F600DD52A8 /* 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 */; };
|
E258F38A2BF61BD2005B8C28 /* URLTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = E258F3892BF61BD2005B8C28 /* URLTester.swift */; };
|
||||||
E258F38B2BF61BD2005B8C28 /* 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 */; };
|
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 */; };
|
E27568B92BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */; };
|
||||||
E27568BA2BFAAC2000BDF0AF /* 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 */; };
|
E27568BB2BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */; };
|
||||||
@@ -1599,7 +1599,7 @@
|
|||||||
3797104928D3D10600D5F53C /* SDWebImageSwiftUI in Frameworks */,
|
3797104928D3D10600D5F53C /* SDWebImageSwiftUI in Frameworks */,
|
||||||
37BD07B92698AB2E003EBB87 /* Siesta in Frameworks */,
|
37BD07B92698AB2E003EBB87 /* Siesta in Frameworks */,
|
||||||
37FB284D2722099E00A57617 /* SDWebImageWebPCoder in Frameworks */,
|
37FB284D2722099E00A57617 /* SDWebImageWebPCoder in Frameworks */,
|
||||||
3797665B2C79FA6900C10DBD /* MPVKit in Frameworks */,
|
E265D0C22C7D217000D2BB8E /* MPVKit in Frameworks */,
|
||||||
37CF8B8428535E4F00B71E37 /* SDWebImage in Frameworks */,
|
37CF8B8428535E4F00B71E37 /* SDWebImage in Frameworks */,
|
||||||
37C7367A2AC33010007630E1 /* SwiftUIIntrospect in Frameworks */,
|
37C7367A2AC33010007630E1 /* SwiftUIIntrospect in Frameworks */,
|
||||||
);
|
);
|
||||||
@@ -1624,7 +1624,7 @@
|
|||||||
375B8AB728B583BD00397B31 /* KeychainAccess in Frameworks */,
|
375B8AB728B583BD00397B31 /* KeychainAccess in Frameworks */,
|
||||||
3703205E27D2BB12007A0CB8 /* SDWebImageWebPCoder in Frameworks */,
|
3703205E27D2BB12007A0CB8 /* SDWebImageWebPCoder in Frameworks */,
|
||||||
37CF8B8628535E5A00B71E37 /* SDWebImage in Frameworks */,
|
37CF8B8628535E5A00B71E37 /* SDWebImage in Frameworks */,
|
||||||
3797665D2C79FA7500C10DBD /* MPVKit in Frameworks */,
|
E265D0C42C7D218A00D2BB8E /* MPVKit in Frameworks */,
|
||||||
3703205C27D2BAF3007A0CB8 /* SwiftyJSON in Frameworks */,
|
3703205C27D2BAF3007A0CB8 /* SwiftyJSON in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -1670,7 +1670,7 @@
|
|||||||
372915E42687E33E00F5A35B /* Defaults in Frameworks */,
|
372915E42687E33E00F5A35B /* Defaults in Frameworks */,
|
||||||
3772003B27E8EEC800CB2475 /* libbz2.tbd in Frameworks */,
|
3772003B27E8EEC800CB2475 /* libbz2.tbd in Frameworks */,
|
||||||
37BADCA9269A570B009BE4FB /* Alamofire in Frameworks */,
|
37BADCA9269A570B009BE4FB /* Alamofire in Frameworks */,
|
||||||
3797665F2C79FA7D00C10DBD /* MPVKit in Frameworks */,
|
E265D0C62C7D21A300D2BB8E /* MPVKit in Frameworks */,
|
||||||
37D4B19D2671817900C925CA /* SwiftyJSON in Frameworks */,
|
37D4B19D2671817900C925CA /* SwiftyJSON in Frameworks */,
|
||||||
3797757D268922D100DD52A8 /* Siesta in Frameworks */,
|
3797757D268922D100DD52A8 /* Siesta in Frameworks */,
|
||||||
);
|
);
|
||||||
@@ -2585,7 +2585,7 @@
|
|||||||
371AC0AB294D1A490085989E /* CachedAsyncImage */,
|
371AC0AB294D1A490085989E /* CachedAsyncImage */,
|
||||||
379325D429A265A300181CF1 /* Logging */,
|
379325D429A265A300181CF1 /* Logging */,
|
||||||
37C736792AC33010007630E1 /* SwiftUIIntrospect */,
|
37C736792AC33010007630E1 /* SwiftUIIntrospect */,
|
||||||
3797665A2C79FA6900C10DBD /* MPVKit */,
|
E265D0C12C7D217000D2BB8E /* MPVKit */,
|
||||||
);
|
);
|
||||||
productName = "Yattee (iOS)";
|
productName = "Yattee (iOS)";
|
||||||
productReference = 37D4B0C92671614900C925CA /* Yattee.app */;
|
productReference = 37D4B0C92671614900C925CA /* Yattee.app */;
|
||||||
@@ -2624,7 +2624,7 @@
|
|||||||
371AC0B1294D1C230085989E /* CachedAsyncImage */,
|
371AC0B1294D1C230085989E /* CachedAsyncImage */,
|
||||||
379325D629A265AE00181CF1 /* Logging */,
|
379325D629A265AE00181CF1 /* Logging */,
|
||||||
37C736772AC32B28007630E1 /* SwiftUIIntrospect */,
|
37C736772AC32B28007630E1 /* SwiftUIIntrospect */,
|
||||||
3797665C2C79FA7500C10DBD /* MPVKit */,
|
E265D0C32C7D218A00D2BB8E /* MPVKit */,
|
||||||
);
|
);
|
||||||
productName = "Yattee (macOS)";
|
productName = "Yattee (macOS)";
|
||||||
productReference = 37D4B0CF2671614900C925CA /* Yattee.app */;
|
productReference = 37D4B0CF2671614900C925CA /* Yattee.app */;
|
||||||
@@ -2702,7 +2702,7 @@
|
|||||||
377F9F75294403880043F856 /* Cache */,
|
377F9F75294403880043F856 /* Cache */,
|
||||||
371AC0B3294D1C290085989E /* CachedAsyncImage */,
|
371AC0B3294D1C290085989E /* CachedAsyncImage */,
|
||||||
379325D829A265B500181CF1 /* Logging */,
|
379325D829A265B500181CF1 /* Logging */,
|
||||||
3797665E2C79FA7D00C10DBD /* MPVKit */,
|
E265D0C52C7D21A300D2BB8E /* MPVKit */,
|
||||||
);
|
);
|
||||||
productName = Yattee;
|
productName = Yattee;
|
||||||
productReference = 37D4B158267164AE00C925CA /* Yattee.app */;
|
productReference = 37D4B158267164AE00C925CA /* Yattee.app */;
|
||||||
@@ -2822,7 +2822,7 @@
|
|||||||
374D11E52943C56300CB4350 /* XCRemoteSwiftPackageReference "Cache" */,
|
374D11E52943C56300CB4350 /* XCRemoteSwiftPackageReference "Cache" */,
|
||||||
371AC0AA294D1A490085989E /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */,
|
371AC0AA294D1A490085989E /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */,
|
||||||
379325D329A265A300181CF1 /* XCRemoteSwiftPackageReference "swift-log" */,
|
379325D329A265A300181CF1 /* XCRemoteSwiftPackageReference "swift-log" */,
|
||||||
379766592C79FA6900C10DBD /* XCRemoteSwiftPackageReference "MPVKit" */,
|
E265D0C02C7D217000D2BB8E /* XCRemoteSwiftPackageReference "MPVKit" */,
|
||||||
);
|
);
|
||||||
productRefGroup = 37D4B0CA2671614900C925CA /* Products */;
|
productRefGroup = 37D4B0CA2671614900C925CA /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@@ -4103,7 +4103,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 190;
|
CURRENT_PROJECT_VERSION = 194;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
|
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
|
||||||
@@ -4134,7 +4134,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 190;
|
CURRENT_PROJECT_VERSION = 194;
|
||||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||||
@@ -4165,7 +4165,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 190;
|
CURRENT_PROJECT_VERSION = 194;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||||
@@ -4185,7 +4185,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 190;
|
CURRENT_PROJECT_VERSION = 194;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||||
@@ -4326,6 +4326,7 @@
|
|||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 3;
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
@@ -4348,7 +4349,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "iOS/Yattee (iOS).entitlements";
|
CODE_SIGN_ENTITLEMENTS = "iOS/Yattee (iOS).entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 190;
|
CURRENT_PROJECT_VERSION = 194;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
"DEBUG=1",
|
"DEBUG=1",
|
||||||
@@ -4400,7 +4401,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 190;
|
CURRENT_PROJECT_VERSION = 194;
|
||||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1";
|
GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1";
|
||||||
@@ -4452,7 +4453,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 190;
|
CURRENT_PROJECT_VERSION = 194;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
@@ -4491,7 +4492,7 @@
|
|||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 190;
|
CURRENT_PROJECT_VERSION = 194;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = 78Z5H3M6RJ;
|
"DEVELOPMENT_TEAM[sdk=macosx*]" = 78Z5H3M6RJ;
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
@@ -4525,7 +4526,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 190;
|
CURRENT_PROJECT_VERSION = 194;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -4548,7 +4549,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 190;
|
CURRENT_PROJECT_VERSION = 194;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -4573,7 +4574,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 190;
|
CURRENT_PROJECT_VERSION = 194;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -4597,7 +4598,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 190;
|
CURRENT_PROJECT_VERSION = 194;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -4623,7 +4624,7 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 190;
|
CURRENT_PROJECT_VERSION = 194;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -4663,7 +4664,7 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Distribution";
|
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Distribution";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 190;
|
CURRENT_PROJECT_VERSION = 194;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
"DEVELOPMENT_TEAM[sdk=appletvos*]" = 78Z5H3M6RJ;
|
"DEVELOPMENT_TEAM[sdk=appletvos*]" = 78Z5H3M6RJ;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -4703,7 +4704,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 190;
|
CURRENT_PROJECT_VERSION = 194;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -4726,7 +4727,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 190;
|
CURRENT_PROJECT_VERSION = 194;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -4960,14 +4961,6 @@
|
|||||||
minimumVersion = 2.1.0;
|
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" */ = {
|
3797757B268922D100DD52A8 /* XCRemoteSwiftPackageReference "siesta" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/bustoutsolutions/siesta";
|
repositoryURL = "https://github.com/bustoutsolutions/siesta";
|
||||||
@@ -5040,6 +5033,14 @@
|
|||||||
minimumVersion = 0.3.0;
|
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 */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
@@ -5228,21 +5229,6 @@
|
|||||||
package = 3797104728D3D10600D5F53C /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */;
|
package = 3797104728D3D10600D5F53C /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */;
|
||||||
productName = 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 */ = {
|
3797757C268922D100DD52A8 /* Siesta */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = 3797757B268922D100DD52A8 /* XCRemoteSwiftPackageReference "siesta" */;
|
package = 3797757B268922D100DD52A8 /* XCRemoteSwiftPackageReference "siesta" */;
|
||||||
@@ -5328,6 +5314,21 @@
|
|||||||
package = 37FB285227220D8400A57617 /* XCRemoteSwiftPackageReference "SDWebImagePINPlugin" */;
|
package = 37FB285227220D8400A57617 /* XCRemoteSwiftPackageReference "SDWebImagePINPlugin" */;
|
||||||
productName = 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 */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
|
|
||||||
/* Begin XCVersionGroup section */
|
/* Begin XCVersionGroup section */
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "1f99971d9d21cffe56d0033bc5c38e9fcd5ff46ca5f7d19c76f5ba0a268ce4b6",
|
"originHash" : "515d8e68c4a31658288fb3f94789ee539399b042082c08c39f4c03c27fd8860c",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "activelabel.swift",
|
"identity" : "activelabel.swift",
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"location" : "https://github.com/hyperoslo/Cache.git",
|
"location" : "https://github.com/hyperoslo/Cache.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"branch" : "master",
|
"branch" : "master",
|
||||||
"revision" : "d2e8f5a53c601b43371fdc90277d7f64b0e89a25"
|
"revision" : "81a0277cbc6b63f4e0cd6f42c4abefa1011bbfa9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -58,10 +58,10 @@
|
|||||||
{
|
{
|
||||||
"identity" : "mpvkit",
|
"identity" : "mpvkit",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/cxfksword/MPVKit",
|
"location" : "https://github.com/mpvkit/MPVKit.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "f646e4b625e9c8a2ff22a7e0bb5557306300be5d",
|
"revision" : "ee72059235566df8b455bff15e3f83a1c9053e78",
|
||||||
"version" : "0.38.0"
|
"version" : "0.38.0-fix"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -66,6 +66,11 @@
|
|||||||
value = "Yes"
|
value = "Yes"
|
||||||
isEnabled = "YES">
|
isEnabled = "YES">
|
||||||
</EnvironmentVariable>
|
</EnvironmentVariable>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "IDELogRedirectionPolicy"
|
||||||
|
value = "oslogToStdio"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
</EnvironmentVariables>
|
</EnvironmentVariables>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import AVFoundation
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Logging
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
final class AppDelegate: UIResponder, UIApplicationDelegate {
|
final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
var orientationLock = UIInterfaceOrientationMask.all
|
var orientationLock = UIInterfaceOrientationMask.all
|
||||||
|
|
||||||
|
private var logger = Logger(label: "stream.yattee.app.delegalate")
|
||||||
private(set) static var instance: AppDelegate!
|
private(set) static var instance: AppDelegate!
|
||||||
|
|
||||||
func application(_: UIApplication, supportedInterfaceOrientationsFor _: UIWindow?) -> UIInterfaceOrientationMask {
|
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
|
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { // swiftlint:disable:this discouraged_optional_collection
|
||||||
Self.instance = self
|
Self.instance = self
|
||||||
#if os(iOS)
|
|
||||||
UIViewController.swizzleHomeIndicatorProperty()
|
|
||||||
|
|
||||||
|
#if !os(macOS)
|
||||||
|
UIViewController.swizzleHomeIndicatorProperty()
|
||||||
OrientationTracker.shared.startDeviceOrientationTracking()
|
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
|
#endif
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
import MPVKit
|
import Libmpv
|
||||||
import OpenGL.GL
|
import OpenGL.GL
|
||||||
import OpenGL.GL3
|
import OpenGL.GL3
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user