1
0
mirror of https://github.com/yattee/yattee.git synced 2025-04-29 08:20:33 +05:30

Compare commits

..

No commits in common. "main" and "1.5.2-192" have entirely different histories.

78 changed files with 1197 additions and 3781 deletions

View File

@ -27,9 +27,9 @@ jobs:
# lane: ['mac beta', 'ios beta', 'tvos beta'] # lane: ['mac beta', 'ios beta', 'tvos beta']
lane: ['ios beta', 'tvos beta'] lane: ['ios beta', 'tvos beta']
name: Releasing ${{ matrix.lane }} version to TestFlight name: Releasing ${{ matrix.lane }} version to TestFlight
runs-on: macos-latest runs-on: macos-13
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1 - uses: ruby/setup-ruby@v1
with: with:
ruby-version: '3.0' ruby-version: '3.0'
@ -44,16 +44,16 @@ jobs:
- uses: maierj/fastlane-action@v3.0.0 - uses: maierj/fastlane-action@v3.0.0
with: with:
lane: ${{ matrix.lane }} lane: ${{ matrix.lane }}
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v3
with: with:
name: ${{ matrix.lane }} build name: ${{ matrix.lane }} build
path: fastlane/builds/**/*.ipa path: fastlane/builds/**/*.ipa
if-no-files-found: ignore if-no-files-found: ignore
mac_notarized: mac_notarized:
name: Build and notarize macOS app name: Build and notarize macOS app
runs-on: macos-latest runs-on: macos-13
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1 - uses: ruby/setup-ruby@v1
with: with:
ruby-version: '3.0' ruby-version: '3.0'
@ -76,7 +76,7 @@ jobs:
echo "ZIP_PATH=fastlane/builds/${{ env.VERSION_NUMBER }}-${{ env.BUILD_NUMBER }}/macOS/Yattee-${{ env.VERSION_NUMBER }}-macOS.zip" >> $GITHUB_ENV echo "ZIP_PATH=fastlane/builds/${{ env.VERSION_NUMBER }}-${{ env.BUILD_NUMBER }}/macOS/Yattee-${{ env.VERSION_NUMBER }}-macOS.zip" >> $GITHUB_ENV
- name: ZIP build - name: ZIP build
run: /usr/bin/ditto -c -k --keepParent ${{ env.APP_PATH }} ${{ env.ZIP_PATH }} run: /usr/bin/ditto -c -k --keepParent ${{ env.APP_PATH }} ${{ env.ZIP_PATH }}
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v3
with: with:
name: mac notarized build name: mac notarized build
path: ${{ env.ZIP_PATH }} path: ${{ env.ZIP_PATH }}
@ -86,10 +86,10 @@ jobs:
name: Create GitHub release name: Create GitHub release
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- run: echo "BUILD_NUMBER=$(cat Yattee.xcodeproj/project.pbxproj | grep -m 1 CURRENT_PROJECT_VERSION | cut -d' ' -f3 | sed 's/;//g')" >> $GITHUB_ENV - run: echo "BUILD_NUMBER=$(cat Yattee.xcodeproj/project.pbxproj | grep -m 1 CURRENT_PROJECT_VERSION | cut -d' ' -f3 | sed 's/;//g')" >> $GITHUB_ENV
- run: echo "VERSION_NUMBER=$(cat Yattee.xcodeproj/project.pbxproj | grep -m 1 MARKETING_VERSION | cut -d' ' -f3 | sed 's/;//g')" >> $GITHUB_ENV - run: echo "VERSION_NUMBER=$(cat Yattee.xcodeproj/project.pbxproj | grep -m 1 MARKETING_VERSION | cut -d' ' -f3 | sed 's/;//g')" >> $GITHUB_ENV
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v3
with: with:
path: artifacts path: artifacts
- uses: ncipollo/release-action@v1 - uses: ncipollo/release-action@v1

View File

@ -1,8 +1,10 @@
## Build 199 ## Build 192
* Fix mpv crashing on macOS by @stonerl in https://github.com/yattee/yattee/pull/754
## What's Changed * Refreshed icons for iOS and macOS by @stonerl in https://github.com/yattee/yattee/pull/752
* Add support for invidious companion by @lifo9 in https://github.com/yattee/yattee/pull/863 * Add new MPVKit repo by @stonerl in https://github.com/yattee/yattee/pull/753
* Translations update from Hosted Weblate by @weblate in https://github.com/yattee/yattee/pull/851 * 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
## 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,50 +23,6 @@
* 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
* Allow users to disable fullscreen swipe gesture by @stonerl in https://github.com/yattee/yattee/pull/814
* Proper audio interrupt and route change handling by @stonerl in https://github.com/yattee/yattee/pull/815
* Improved subtitle handling by @stonerl in https://github.com/yattee/yattee/pull/817
* Improvements to MPVGLView by @stonerl in https://github.com/yattee/yattee/pull/818
* Add drag gestures to video details by @stonerl in https://github.com/yattee/yattee/pull/820
* Fix uneven playback when using MPV and not syncing refreshrate by @blennster in https://github.com/yattee/yattee/pull/833
* Norwegian Language by @mmaalo in https://github.com/yattee/yattee/pull/834
* Translations update from Hosted Weblate by @weblate in https://github.com/yattee/yattee/pull/836
* Update MPVKit to v0.39.0 by @stonerl in https://github.com/yattee/yattee/pull/824
* Update SwiftUI-Introspect by @stonerl in https://github.com/yattee/yattee/pull/813
* Orientation/Fullscreen fixes and cleanup by @stonerl in https://github.com/yattee/yattee/pull/806
* More robust resolution handling by @stonerl in https://github.com/yattee/yattee/pull/807
* MPV: improved A/V sync by @stonerl in https://github.com/yattee/yattee/pull/805
* Retry loading video before presenting error by @stonerl in https://github.com/yattee/yattee/pull/810
* Refactor Search by @stonerl in https://github.com/yattee/yattee/pull/809
* iOS: Simplified fullscreen and orientation by @stonerl in https://github.com/yattee/yattee/pull/786
* macOS: only apply player shortcuts when window is active by @stonerl in https://github.com/yattee/yattee/pull/802
* player controls: add background opacity selection by @stonerl in https://github.com/yattee/yattee/pull/799
* add missing Shorts resolutions by @stonerl in https://github.com/yattee/yattee/pull/797
* use -O1 on macOS by @stonerl in https://github.com/yattee/yattee/pull/801
* Gestures: swipe up toggles fullscreen by @stonerl in https://github.com/yattee/yattee/pull/778
* dont open video when dismissing context menu by @stonerl in https://github.com/yattee/yattee/pull/780
* mpv: remove video layer when entering background by @stonerl in https://github.com/yattee/yattee/pull/793
* hi-res invidious logos by @stonerl in https://github.com/yattee/yattee/pull/791
* enable -O3 by @stonerl in https://github.com/yattee/yattee/pull/794
* Better audio ducking by @stonerl in https://github.com/yattee/yattee/pull/779
* fix picture in picture by @stonerl in https://github.com/yattee/yattee/pull/789
* 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
* Stop making videos with unknown length shorts. by @derspyy in https://github.com/yattee/yattee/pull/849
* Add Hungarian to locales list
* 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 * 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 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 * Fix regression and improve curentChapter handling by @stonerl in https://github.com/yattee/yattee/pull/749

View File

@ -9,22 +9,21 @@ GEM
public_suffix (>= 2.0.2, < 7.0) public_suffix (>= 2.0.2, < 7.0)
artifactory (3.0.17) artifactory (3.0.17)
atomos (0.1.3) atomos (0.1.3)
aws-eventstream (1.3.2) aws-eventstream (1.3.0)
aws-partitions (1.1072.0) aws-partitions (1.968.0)
aws-sdk-core (3.220.2) aws-sdk-core (3.201.5)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.9) aws-sigv4 (~> 1.9)
base64
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.99.0) aws-sdk-kms (1.88.0)
aws-sdk-core (~> 3, >= 3.216.0) aws-sdk-core (~> 3, >= 3.201.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.182.0) aws-sdk-s3 (1.159.0)
aws-sdk-core (~> 3, >= 3.216.0) aws-sdk-core (~> 3, >= 3.201.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0) aws-sigv4 (1.9.1)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4) babosa (1.0.4)
base64 (0.2.0) base64 (0.2.0)
@ -34,13 +33,13 @@ GEM
commander (4.6.0) commander (4.6.0)
highline (~> 2.0.0) highline (~> 2.0.0)
declarative (0.0.20) declarative (0.0.20)
digest-crc (0.7.0) digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107) domain_name (0.6.20240107)
dotenv (2.8.1) dotenv (2.8.1)
emoji_regex (3.2.3) emoji_regex (3.2.3)
excon (0.112.0) excon (0.111.0)
faraday (1.10.4) faraday (1.10.3)
faraday-em_http (~> 1.0) faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0) faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1) faraday-excon (~> 1.1)
@ -59,17 +58,17 @@ GEM
faraday-em_synchrony (1.0.0) faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0) faraday-excon (1.1.0)
faraday-httpclient (1.0.1) faraday-httpclient (1.0.1)
faraday-multipart (1.1.0) faraday-multipart (1.0.4)
multipart-post (~> 2.0) multipart-post (~> 2)
faraday-net_http (1.0.2) faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0) faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0) faraday-patron (1.0.0)
faraday-rack (1.0.0) faraday-rack (1.0.0)
faraday-retry (1.0.3) faraday-retry (1.0.3)
faraday_middleware (1.2.1) faraday_middleware (1.2.0)
faraday (~> 1.0) faraday (~> 1.0)
fastimage (2.4.0) fastimage (2.3.1)
fastlane (2.227.0) fastlane (2.222.0)
CFPropertyList (>= 2.3, < 4.0.0) CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0) addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0) artifactory (~> 3.0)
@ -85,7 +84,6 @@ GEM
faraday-cookie_jar (~> 0.0.6) faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0) faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0) fastimage (>= 2.1.0, < 3.0.0)
fastlane-sirp (>= 1.0.0)
gh_inspector (>= 1.1.2, < 2.0.0) gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3) google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1) google-apis-playcustomapp_v1 (~> 0.1)
@ -109,10 +107,8 @@ GEM
tty-spinner (>= 0.8.0, < 1.0.0) tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0) word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0) xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.4.0) xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
gh_inspector (1.1.3) gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.54.0) google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a) google-apis-core (>= 0.11.0, < 2.a)
@ -130,12 +126,12 @@ GEM
google-apis-core (>= 0.11.0, < 2.a) google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.31.0) google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a) google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.8.0) google-cloud-core (1.7.1)
google-cloud-env (>= 1.0, < 3.a) google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0) google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0) faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.5.0) google-cloud-errors (1.4.0)
google-cloud-storage (1.47.0) google-cloud-storage (1.47.0)
addressable (~> 2.8) addressable (~> 2.8)
digest-crc (~> 0.4) digest-crc (~> 0.4)
@ -151,25 +147,23 @@ GEM
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a) signet (>= 0.16, < 2.a)
highline (2.0.3) highline (2.0.3)
http-cookie (1.0.8) http-cookie (1.0.7)
domain_name (~> 0.5) domain_name (~> 0.5)
httpclient (2.9.0) httpclient (2.8.3)
mutex_m
jmespath (1.6.2) jmespath (1.6.2)
json (2.10.2) json (2.7.2)
jwt (2.10.1) jwt (2.8.2)
base64 base64
mini_magick (4.13.2) mini_magick (4.13.2)
mini_mime (1.1.5) mini_mime (1.1.5)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.4.1) multipart-post (2.4.1)
mutex_m (0.3.0) nanaimo (0.3.0)
nanaimo (0.4.0)
naturally (2.2.1) naturally (2.2.1)
nkf (0.2.0) nkf (0.2.0)
optparse (0.6.0) optparse (0.5.0)
os (1.1.4) os (1.1.4)
plist (3.7.2) plist (3.7.1)
public_suffix (6.0.1) public_suffix (6.0.1)
rake (13.2.1) rake (13.2.1)
representable (3.2.0) representable (3.2.0)
@ -177,10 +171,11 @@ 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.4.1) rexml (3.3.6)
rouge (3.28.0) strscan
rouge (2.0.7)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyzip (2.4.1) rubyzip (2.3.2)
security (0.1.5) security (0.1.5)
signet (0.19.0) signet (0.19.0)
addressable (~> 2.8) addressable (~> 2.8)
@ -190,7 +185,7 @@ GEM
simctl (1.6.10) simctl (1.6.10)
CFPropertyList CFPropertyList
naturally naturally
sysrandom (1.0.5) 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)
@ -200,27 +195,24 @@ GEM
tty-spinner (0.9.3) tty-spinner (0.9.3)
tty-cursor (~> 0.7) tty-cursor (~> 0.7)
uber (0.1.0) uber (0.1.0)
unicode-display_width (2.6.0) unicode-display_width (2.5.0)
word_wrap (1.0.0) word_wrap (1.0.0)
xcodeproj (1.27.0) xcodeproj (1.25.0)
CFPropertyList (>= 2.3.3, < 4.0) CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3) atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0) claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1) colored2 (~> 3.1)
nanaimo (~> 0.4.0) nanaimo (~> 0.3.0)
rexml (>= 3.3.6, < 4.0) rexml (>= 3.3.2, < 4.0)
xcpretty (0.4.0) xcpretty (0.3.0)
rouge (~> 3.28.0) rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1) xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7) xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS PLATFORMS
arm64-darwin-21 arm64-darwin-21
arm64-darwin-23
arm64-darwin-24
x86_64-darwin-19 x86_64-darwin-19
x86_64-darwin-20 x86_64-darwin-20
x86_64-darwin-21
x86_64-linux x86_64-linux
DEPENDENCIES DEPENDENCIES

View File

@ -10,28 +10,11 @@ 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": sanitizedUrlString, "apiURL": value.urlString,
"username": value.username, "username": value.username,
"password": value.password ?? "" "password": value.password ?? ""
] ]

View File

@ -10,16 +10,14 @@ struct Instance: Defaults.Serializable, Hashable, Identifiable {
let apiURLString: String let apiURLString: String
var frontendURL: String? var frontendURL: String?
var proxiesVideos: Bool var proxiesVideos: Bool
var invidiousCompanion: Bool
init(app: VideosApp, id: String? = nil, name: String? = nil, apiURLString: String, frontendURL: String? = nil, proxiesVideos: Bool = false, invidiousCompanion: Bool = false) { init(app: VideosApp, id: String? = nil, name: String? = nil, apiURLString: String, frontendURL: String? = nil, proxiesVideos: Bool = false) {
self.app = app self.app = app
self.id = id ?? UUID().uuidString self.id = id ?? UUID().uuidString
self.name = name ?? app.rawValue self.name = name ?? app.rawValue
self.apiURLString = apiURLString self.apiURLString = apiURLString
self.frontendURL = frontendURL self.frontendURL = frontendURL
self.proxiesVideos = proxiesVideos self.proxiesVideos = proxiesVideos
self.invidiousCompanion = invidiousCompanion
} }
var apiURL: URL! { var apiURL: URL! {

View File

@ -16,8 +16,7 @@ struct InstancesBridge: Defaults.Bridge {
"name": value.name, "name": value.name,
"apiURL": value.apiURLString, "apiURL": value.apiURLString,
"frontendURL": value.frontendURL ?? "", "frontendURL": value.frontendURL ?? "",
"proxiesVideos": value.proxiesVideos ? "true" : "false", "proxiesVideos": value.proxiesVideos ? "true" : "false"
"invidiousCompanion": value.invidiousCompanion ? "true" : "false"
] ]
} }
@ -34,8 +33,7 @@ struct InstancesBridge: Defaults.Bridge {
let name = object["name"] ?? "" let name = object["name"] ?? ""
let frontendURL: String? = object["frontendURL"]!.isEmpty ? nil : object["frontendURL"] let frontendURL: String? = object["frontendURL"]!.isEmpty ? nil : object["frontendURL"]
let proxiesVideos = object["proxiesVideos"] == "true" let proxiesVideos = object["proxiesVideos"] == "true"
let invidiousCompanion = object["invidiousCompanion"] == "true"
return Instance(app: app, id: id, name: name, apiURLString: apiURL, frontendURL: frontendURL, proxiesVideos: proxiesVideos, invidiousCompanion: invidiousCompanion) return Instance(app: app, id: id, name: name, apiURLString: apiURL, frontendURL: frontendURL, proxiesVideos: proxiesVideos)
} }
} }

View File

@ -79,17 +79,6 @@ final class InstancesModel: ObservableObject {
Defaults[.instances][index] = instance Defaults[.instances][index] = instance
} }
func setInvidiousCompanion(_ instance: Instance, _ invidiousCompanion: Bool) {
guard let index = Defaults[.instances].firstIndex(where: { $0.id == instance.id }) else {
return
}
var instance = Defaults[.instances][index]
instance.invidiousCompanion = invidiousCompanion
Defaults[.instances][index] = instance
}
func remove(_ instance: Instance) { func remove(_ instance: Instance) {
let accounts = accounts(instance.id) let accounts = accounts(instance.id)
if let index = Defaults[.instances].firstIndex(where: { $0.id == instance.id }) { if let index = Defaults[.instances].firstIndex(where: { $0.id == instance.id }) {

View File

@ -247,27 +247,27 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
} }
func feed(_ page: Int?) -> Resource? { func feed(_ page: Int?) -> Resource? {
resourceWithAuthCheck(baseURL: account.url, path: "\(Self.basePath)/auth/feed") resource(baseURL: account.url, path: "\(Self.basePath)/auth/feed")
.withParam("page", String(page ?? 1)) .withParam("page", String(page ?? 1))
} }
var feed: Resource? { var feed: Resource? {
resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/feed")) resource(baseURL: account.url, path: basePathAppending("auth/feed"))
} }
var subscriptions: Resource? { var subscriptions: Resource? {
resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/subscriptions")) resource(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
} }
func subscribe(_ channelID: String, onCompletion: @escaping () -> Void = {}) { func subscribe(_ channelID: String, onCompletion: @escaping () -> Void = {}) {
resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/subscriptions")) resource(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) {
resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/subscriptions")) resource(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 resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/playlists")) return resource(baseURL: account.url, path: basePathAppending("auth/playlists"))
} }
func playlist(_ id: String) -> Resource? { func playlist(_ id: String) -> Resource? {
resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/playlists/\(id)")) resource(baseURL: account.url, path: basePathAppending("auth/playlists/\(id)"))
} }
func playlistVideos(_ id: String) -> Resource? { func playlistVideos(_ id: String) -> Resource? {
@ -445,9 +445,6 @@ 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
@ -498,7 +495,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
indexID: indexID, indexID: indexID,
live: json["liveNow"].boolValue, live: json["liveNow"].boolValue,
upcoming: json["isUpcoming"].boolValue, upcoming: json["isUpcoming"].boolValue,
short: length <= Video.shortLength && length != 0.0, short: length <= Video.shortLength,
publishedAt: publishedAt, publishedAt: publishedAt,
likes: json["likeCount"].int, likes: json["likeCount"].int,
dislikes: json["dislikeCount"].int, dislikes: json["dislikeCount"].int,
@ -556,30 +553,6 @@ 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,
@ -590,20 +563,13 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
return nil return nil
} }
// Some instances are not configured properly and return thumbnail links // some of instances are not configured properly and return thumbnails links
// with an incorrect scheme or a missing port. // with incorrect scheme
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)!)
} }
@ -655,29 +621,21 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
if json["liveNow"].boolValue { if json["liveNow"].boolValue {
return hls return hls
} }
let videoId = json["videoId"].stringValue
return extractFormatStreams(from: json["formatStreams"].arrayValue, videoId: videoId) + return extractFormatStreams(from: json["formatStreams"].arrayValue) +
extractAdaptiveFormats(from: json["adaptiveFormats"].arrayValue, videoId: videoId) + extractAdaptiveFormats(from: json["adaptiveFormats"].arrayValue) +
hls hls
} }
private func extractFormatStreams(from streams: [JSON], videoId: String?) -> [Stream] { private func extractFormatStreams(from streams: [JSON]) -> [Stream] {
streams.compactMap { stream in streams.compactMap { stream in
guard let streamURL = stream["url"].url else { guard let streamURL = stream["url"].url else {
return nil return nil
} }
let finalURL: URL
if let videoId, let itag = stream["itag"].string, account.instance.invidiousCompanion {
let companionURLString = "\(account.instance.apiURLString)/latest_version?id=\(videoId)&itag=\(itag)"
finalURL = URL(string: companionURLString) ?? streamURL
} else {
finalURL = streamURL
}
return SingleAssetStream( return SingleAssetStream(
instance: account.instance, instance: account.instance,
avAsset: AVURLAsset(url: finalURL), avAsset: AVURLAsset(url: streamURL),
resolution: Stream.Resolution.from(resolution: stream["resolution"].string ?? ""), resolution: Stream.Resolution.from(resolution: stream["resolution"].string ?? ""),
kind: .stream, kind: .stream,
encoding: stream["encoding"].string ?? "" encoding: stream["encoding"].string ?? ""
@ -685,7 +643,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
} }
} }
private func extractAdaptiveFormats(from streams: [JSON], videoId: String?) -> [Stream] { private func extractAdaptiveFormats(from streams: [JSON]) -> [Stream] {
let audioStreams = streams let audioStreams = streams
.filter { $0["type"].stringValue.starts(with: "audio/mp4") } .filter { $0["type"].stringValue.starts(with: "audio/mp4") }
.sorted { .sorted {
@ -700,29 +658,15 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
return videoStreams.compactMap { videoStream in return videoStreams.compactMap { videoStream in
guard let audioAssetURL = audioStream["url"].url, guard let audioAssetURL = audioStream["url"].url,
let videoAssetURL = videoStream["url"].url, let videoAssetURL = videoStream["url"].url
let audioItag = audioStream["itag"].string,
let videoItag = videoStream["itag"].string
else { else {
return nil return nil
} }
let finalAudioURL: URL
let finalVideoURL: URL
if let videoId, account.instance.invidiousCompanion {
let audioCompanionURLString = "\(account.instance.apiURLString)/latest_version?id=\(videoId)&itag=\(audioItag)"
let videoCompanionURLString = "\(account.instance.apiURLString)/latest_version?id=\(videoId)&itag=\(videoItag)"
finalAudioURL = URL(string: audioCompanionURLString) ?? audioAssetURL
finalVideoURL = URL(string: videoCompanionURLString) ?? videoAssetURL
} else {
finalAudioURL = audioAssetURL
finalVideoURL = videoAssetURL
}
return Stream( return Stream(
instance: account.instance, instance: account.instance,
audioAsset: AVURLAsset(url: finalAudioURL), audioAsset: AVURLAsset(url: audioAssetURL),
videoAsset: AVURLAsset(url: finalVideoURL), videoAsset: AVURLAsset(url: videoAssetURL),
resolution: Stream.Resolution.from(resolution: videoStream["resolution"].stringValue), resolution: Stream.Resolution.from(resolution: videoStream["resolution"].stringValue),
kind: .adaptive, kind: .adaptive,
encoding: videoStream["encoding"].string, encoding: videoStream["encoding"].string,

View File

@ -515,8 +515,7 @@ final class PeerTubeAPI: Service, ObservableObject, VideosAPI {
.dictionaryValue["files"]?.arrayValue.first? .dictionaryValue["files"]?.arrayValue.first?
.dictionaryValue["fileUrl"]?.url .dictionaryValue["fileUrl"]?.url
{ {
let resolution = Stream.Resolution.predefined(.hd720p30) streams.append(SingleAssetStream(instance: account.instance, avAsset: AVURLAsset(url: fileURL), resolution: .hd720p30, kind: .stream))
streams.append(SingleAssetStream(instance: account.instance, avAsset: AVURLAsset(url: fileURL), resolution: resolution, kind: .stream))
} }
return streams return streams

View File

@ -5,7 +5,6 @@ final class AdvancedSettingsGroupExporter: SettingsGroupExporter {
override var globalJSON: JSON { override var globalJSON: JSON {
[ [
"showPlayNowInBackendContextMenu": Defaults[.showPlayNowInBackendContextMenu], "showPlayNowInBackendContextMenu": Defaults[.showPlayNowInBackendContextMenu],
"videoLoadingRetryCount": Defaults[.videoLoadingRetryCount],
"showMPVPlaybackStats": Defaults[.showMPVPlaybackStats], "showMPVPlaybackStats": Defaults[.showMPVPlaybackStats],
"mpvEnableLogging": Defaults[.mpvEnableLogging], "mpvEnableLogging": Defaults[.mpvEnableLogging],
"mpvCacheSecs": Defaults[.mpvCacheSecs], "mpvCacheSecs": Defaults[.mpvCacheSecs],
@ -14,7 +13,6 @@ final class AdvancedSettingsGroupExporter: SettingsGroupExporter {
"mpvDeinterlace": Defaults[.mpvDeinterlace], "mpvDeinterlace": Defaults[.mpvDeinterlace],
"mpvHWdec": Defaults[.mpvHWdec], "mpvHWdec": Defaults[.mpvHWdec],
"mpvDemuxerLavfProbeInfo": Defaults[.mpvDemuxerLavfProbeInfo], "mpvDemuxerLavfProbeInfo": Defaults[.mpvDemuxerLavfProbeInfo],
"mpvSetRefreshToContentFPS": Defaults[.mpvSetRefreshToContentFPS],
"mpvInitialAudioSync": Defaults[.mpvInitialAudioSync], "mpvInitialAudioSync": Defaults[.mpvInitialAudioSync],
"showCacheStatus": Defaults[.showCacheStatus], "showCacheStatus": Defaults[.showCacheStatus],
"feedCacheSize": Defaults[.feedCacheSize] "feedCacheSize": Defaults[.feedCacheSize]

View File

@ -11,7 +11,6 @@ final class BrowsingSettingsGroupExporter: SettingsGroupExporter {
"favorites": Defaults[.favorites].compactMap { jsonFromString(FavoriteItem.bridge.serialize($0)) }, "favorites": Defaults[.favorites].compactMap { jsonFromString(FavoriteItem.bridge.serialize($0)) },
"widgetsSettings": Defaults[.widgetsSettings].compactMap { widgetSettingsJSON($0) }, "widgetsSettings": Defaults[.widgetsSettings].compactMap { widgetSettingsJSON($0) },
"startupSection": Defaults[.startupSection].rawValue, "startupSection": Defaults[.startupSection].rawValue,
"showSearchSuggestions": Defaults[.showSearchSuggestions],
"visibleSections": Defaults[.visibleSections].compactMap { $0.rawValue }, "visibleSections": Defaults[.visibleSections].compactMap { $0.rawValue },
"showOpenActionsToolbarItem": Defaults[.showOpenActionsToolbarItem], "showOpenActionsToolbarItem": Defaults[.showOpenActionsToolbarItem],
"accountPickerDisplaysAnonymousAccounts": Defaults[.accountPickerDisplaysAnonymousAccounts], "accountPickerDisplaysAnonymousAccounts": Defaults[.accountPickerDisplaysAnonymousAccounts],

View File

@ -5,13 +5,11 @@ final class ConstrolsSettingsGroupExporter: SettingsGroupExporter {
override var globalJSON: JSON { override var globalJSON: JSON {
[ [
"avPlayerUsesSystemControls": Defaults[.avPlayerUsesSystemControls], "avPlayerUsesSystemControls": Defaults[.avPlayerUsesSystemControls],
"fullscreenPlayerGestureEnabled": Defaults[.fullscreenPlayerGestureEnabled],
"horizontalPlayerGestureEnabled": Defaults[.horizontalPlayerGestureEnabled], "horizontalPlayerGestureEnabled": Defaults[.horizontalPlayerGestureEnabled],
"seekGestureSensitivity": Defaults[.seekGestureSensitivity], "seekGestureSensitivity": Defaults[.seekGestureSensitivity],
"seekGestureSpeed": Defaults[.seekGestureSpeed], "seekGestureSpeed": Defaults[.seekGestureSpeed],
"playerControlsLayout": Defaults[.playerControlsLayout].rawValue, "playerControlsLayout": Defaults[.playerControlsLayout].rawValue,
"fullScreenPlayerControlsLayout": Defaults[.fullScreenPlayerControlsLayout].rawValue, "fullScreenPlayerControlsLayout": Defaults[.fullScreenPlayerControlsLayout].rawValue,
"playerControlsBackgroundOpacity": Defaults[.playerControlsBackgroundOpacity],
"systemControlsCommands": Defaults[.systemControlsCommands].rawValue, "systemControlsCommands": Defaults[.systemControlsCommands].rawValue,
"buttonBackwardSeekDuration": Defaults[.buttonBackwardSeekDuration], "buttonBackwardSeekDuration": Defaults[.buttonBackwardSeekDuration],
"buttonForwardSeekDuration": Defaults[.buttonForwardSeekDuration], "buttonForwardSeekDuration": Defaults[.buttonForwardSeekDuration],

View File

@ -44,7 +44,7 @@ final class PlayerSettingsGroupExporter: SettingsGroupExporter {
#endif #endif
#if os(iOS) #if os(iOS)
export["isOrientationLocked"].bool = Defaults[.isOrientationLocked] export["honorSystemOrientationLock"].bool = Defaults[.honorSystemOrientationLock]
export["enterFullscreenInLandscape"].bool = Defaults[.enterFullscreenInLandscape] export["enterFullscreenInLandscape"].bool = Defaults[.enterFullscreenInLandscape]
export["rotateToLandscapeOnEnterFullScreen"].string = Defaults[.rotateToLandscapeOnEnterFullScreen].rawValue export["rotateToLandscapeOnEnterFullScreen"].string = Defaults[.rotateToLandscapeOnEnterFullScreen].rawValue
#endif #endif

View File

@ -9,10 +9,6 @@ struct AdvancedSettingsGroupImporter {
Defaults[.showPlayNowInBackendContextMenu] = showPlayNowInBackendContextMenu Defaults[.showPlayNowInBackendContextMenu] = showPlayNowInBackendContextMenu
} }
if let videoLoadingRetryCount = json["videoLoadingRetryCount"].int {
Defaults[.videoLoadingRetryCount] = videoLoadingRetryCount
}
if let showMPVPlaybackStats = json["showMPVPlaybackStats"].bool { if let showMPVPlaybackStats = json["showMPVPlaybackStats"].bool {
Defaults[.showMPVPlaybackStats] = showMPVPlaybackStats Defaults[.showMPVPlaybackStats] = showMPVPlaybackStats
} }
@ -45,10 +41,6 @@ struct AdvancedSettingsGroupImporter {
Defaults[.mpvDemuxerLavfProbeInfo] = mpvDemuxerLavfProbeInfo Defaults[.mpvDemuxerLavfProbeInfo] = mpvDemuxerLavfProbeInfo
} }
if let mpvSetRefreshToContentFPS = json["mpvSetRefreshToContentFPS"].bool {
Defaults[.mpvSetRefreshToContentFPS] = mpvSetRefreshToContentFPS
}
if let mpvInitialAudioSync = json["mpvInitialAudioSync"].bool { if let mpvInitialAudioSync = json["mpvInitialAudioSync"].bool {
Defaults[.mpvInitialAudioSync] = mpvInitialAudioSync Defaults[.mpvInitialAudioSync] = mpvInitialAudioSync
} }

View File

@ -46,10 +46,6 @@ struct BrowsingSettingsGroupImporter {
Defaults[.startupSection] = startupSection Defaults[.startupSection] = startupSection
} }
if let showSearchSuggestions = json["showSearchSuggestions"].bool {
Defaults[.showSearchSuggestions] = showSearchSuggestions
}
if let visibleSections = json["visibleSections"].array { if let visibleSections = json["visibleSections"].array {
let sections = visibleSections.compactMap { visibleSectionJSON in let sections = visibleSections.compactMap { visibleSectionJSON in
if let visibleSectionString = visibleSectionJSON.rawString(options: []), if let visibleSectionString = visibleSectionJSON.rawString(options: []),

View File

@ -9,10 +9,6 @@ struct ConstrolsSettingsGroupImporter {
Defaults[.avPlayerUsesSystemControls] = avPlayerUsesSystemControls Defaults[.avPlayerUsesSystemControls] = avPlayerUsesSystemControls
} }
if let fullscreenPlayerGestureEnabled = json["fullscreenPlayerGestureEnabled"].bool {
Defaults[.fullscreenPlayerGestureEnabled] = fullscreenPlayerGestureEnabled
}
if let horizontalPlayerGestureEnabled = json["horizontalPlayerGestureEnabled"].bool { if let horizontalPlayerGestureEnabled = json["horizontalPlayerGestureEnabled"].bool {
Defaults[.horizontalPlayerGestureEnabled] = horizontalPlayerGestureEnabled Defaults[.horizontalPlayerGestureEnabled] = horizontalPlayerGestureEnabled
} }
@ -37,10 +33,6 @@ struct ConstrolsSettingsGroupImporter {
Defaults[.fullScreenPlayerControlsLayout] = fullScreenPlayerControlsLayout Defaults[.fullScreenPlayerControlsLayout] = fullScreenPlayerControlsLayout
} }
if let playerControlsBackgroundOpacity = json["playerControlsBackgroundOpacity"].double {
Defaults[.playerControlsBackgroundOpacity] = playerControlsBackgroundOpacity
}
if let systemControlsCommandsString = json["systemControlsCommands"].string, if let systemControlsCommandsString = json["systemControlsCommands"].string,
let systemControlsCommands = SystemControlsCommands(rawValue: systemControlsCommandsString) let systemControlsCommands = SystemControlsCommands(rawValue: systemControlsCommandsString)
{ {

View File

@ -97,8 +97,8 @@ struct PlayerSettingsGroupImporter {
#endif #endif
#if os(iOS) #if os(iOS)
if let isOrientationLocked = json["isOrientationLocked"].bool { if let honorSystemOrientationLock = json["honorSystemOrientationLock"].bool {
Defaults[.isOrientationLocked] = isOrientationLocked Defaults[.honorSystemOrientationLock] = honorSystemOrientationLock
} }
if let enterFullscreenInLandscape = json["enterFullscreenInLandscape"].bool { if let enterFullscreenInLandscape = json["enterFullscreenInLandscape"].bool {

View File

@ -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: NSKeyValueObservation? private var playerTimeControlStatusObserver: Any?
private var statusObservation: NSKeyValueObservation? private var statusObservation: NSKeyValueObservation?
@ -119,30 +119,10 @@ 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 == .hls || stream.kind == .stream || (stream.kind == .adaptive && stream.format == .mp4)
} }
func playStream( func playStream(
@ -181,9 +161,7 @@ final class AVPlayerBackend: PlayerBackend {
{ {
seek(to: 0, seekType: .loopRestart) seek(to: 0, seekType: .loopRestart)
} }
#if !os(macOS)
model.setAudioSessionActive(true)
#endif
avPlayer.play() avPlayer.play()
// Setting hasStarted to true the first time player started // Setting hasStarted to true the first time player started
@ -198,9 +176,7 @@ final class AVPlayerBackend: PlayerBackend {
guard avPlayer.timeControlStatus != .paused else { guard avPlayer.timeControlStatus != .paused else {
return return
} }
#if !os(macOS)
model.setAudioSessionActive(false)
#endif
avPlayer.pause() avPlayer.pause()
model.objectWillChange.send() model.objectWillChange.send()
} }
@ -214,9 +190,6 @@ final class AVPlayerBackend: PlayerBackend {
} }
func stop() { func stop() {
#if !os(macOS)
model.setAudioSessionActive(false)
#endif
avPlayer.replaceCurrentItem(with: nil) avPlayer.replaceCurrentItem(with: nil)
hasStarted = false hasStarted = false
} }
@ -371,7 +344,7 @@ final class AVPlayerBackend: PlayerBackend {
let startPlaying = { let startPlaying = {
#if !os(macOS) #if !os(macOS)
self.model.setAudioSessionActive(true) try? AVAudioSession.sharedInstance().setActive(true)
#endif #endif
self.setRate(self.model.currentRate) self.setRate(self.model.currentRate)
@ -806,7 +779,7 @@ final class AVPlayerBackend: PlayerBackend {
opened = true opened = true
controller.startPictureInPicture() controller.startPictureInPicture()
} else { } else {
self.logger.info("PiP not possible, waited \(delay) seconds") print("PiP not possible, waited \(delay) seconds")
} }
} }
} }

View File

@ -11,7 +11,6 @@ import SwiftUI
final class MPVBackend: PlayerBackend { final class MPVBackend: PlayerBackend {
static var timeUpdateInterval = 0.5 static var timeUpdateInterval = 0.5
static var networkStateUpdateInterval = 0.1 static var networkStateUpdateInterval = 0.1
static var refreshRateUpdateInterval = 0.5
private var logger = Logger(label: "mpv-backend") private var logger = Logger(label: "mpv-backend")
@ -23,14 +22,13 @@ final class MPVBackend: PlayerBackend {
var stream: Stream? var stream: Stream?
var video: Video? var video: Video?
var captions: Captions? { var captions: Captions? { didSet {
didSet { guard let captions else {
Task { client?.removeSubs()
await handleCaptionsChange() return
} }
} addSubTrack(captions.url)
} }}
var currentTime: CMTime? var currentTime: CMTime?
var loadedVideo = false var loadedVideo = false
@ -91,7 +89,6 @@ final class MPVBackend: PlayerBackend {
private var clientTimer: Repeater! private var clientTimer: Repeater!
private var networkStateTimer: Repeater! private var networkStateTimer: Repeater!
private var refreshRateTimer: Repeater!
private var onFileLoaded: (() -> Void)? private var onFileLoaded: (() -> Void)?
@ -187,30 +184,27 @@ final class MPVBackend: PlayerBackend {
} }
init() { init() {
// swiftlint:disable shorthand_optional_binding
clientTimer = .init(interval: .seconds(Self.timeUpdateInterval), mode: .infinite) { [weak self] _ in clientTimer = .init(interval: .seconds(Self.timeUpdateInterval), mode: .infinite) { [weak self] _ in
guard let self, self.model.activeBackend == .mpv else { guard let self = self, self.model.activeBackend == .mpv else {
return return
} }
self.getTimeUpdates() self.getTimeUpdates()
} }
networkStateTimer = .init(interval: .seconds(Self.networkStateUpdateInterval), mode: .infinite) { [weak self] _ in networkStateTimer = .init(interval: .seconds(Self.networkStateUpdateInterval), mode: .infinite) { [weak self] _ in
guard let self, self.model.activeBackend == .mpv else { guard let self = self, self.model.activeBackend == .mpv else {
return return
} }
self.updateNetworkState() self.updateNetworkState()
} }
// swiftlint:enable shorthand_optional_binding
refreshRateTimer = .init(interval: .seconds(Self.refreshRateUpdateInterval), mode: .infinite) { [weak self] _ in
guard let self, self.model.activeBackend == .mpv else { return }
self.checkAndUpdateRefreshRate()
}
} }
typealias AreInIncreasingOrder = (Stream, Stream) -> Bool typealias AreInIncreasingOrder = (Stream, Stream) -> Bool
func canPlay(_ stream: Stream) -> Bool { func canPlay(_ stream: Stream) -> Bool {
stream.format != .av1 stream.resolution != .unknown && stream.format != .av1
} }
func playStream(_ stream: Stream, of video: Video, preservingTime: Bool, upgrading: Bool) { func playStream(_ stream: Stream, of video: Video, preservingTime: Bool, upgrading: Bool) {
@ -252,7 +246,18 @@ final class MPVBackend: PlayerBackend {
let startPlaying = { let startPlaying = {
#if !os(macOS) #if !os(macOS)
self.model.setAudioSessionActive(true) do {
try AVAudioSession.sharedInstance().setActive(true)
NotificationCenter.default.addObserver(
self,
selector: #selector(self.handleAudioSessionInterruption(_:)),
name: AVAudioSession.interruptionNotification,
object: nil
)
} catch {
self.logger.error("Error setting up audio session: \(error)")
}
#endif #endif
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
@ -345,20 +350,8 @@ final class MPVBackend: PlayerBackend {
startClientUpdates() startClientUpdates()
} }
func startRefreshRateUpdates() {
refreshRateTimer.start()
}
func stopRefreshRateUpdates() {
refreshRateTimer.pause()
}
func play() { func play() {
#if !os(macOS)
model.setAudioSessionActive(true)
#endif
startClientUpdates() startClientUpdates()
startRefreshRateUpdates()
if controls.presentingControls { if controls.presentingControls {
startControlsUpdates() startControlsUpdates()
@ -385,11 +378,7 @@ final class MPVBackend: PlayerBackend {
} }
func pause() { func pause() {
#if !os(macOS)
model.setAudioSessionActive(false)
#endif
stopClientUpdates() stopClientUpdates()
stopRefreshRateUpdates()
client?.pause() client?.pause()
isPaused = true isPaused = true
@ -409,11 +398,6 @@ final class MPVBackend: PlayerBackend {
} }
func stop() { func stop() {
#if !os(macOS)
model.setAudioSessionActive(false)
#endif
stopClientUpdates()
stopRefreshRateUpdates()
client?.stop() client?.stop()
isPlaying = false isPlaying = false
isPaused = false isPaused = false
@ -495,52 +479,6 @@ final class MPVBackend: PlayerBackend {
} }
} }
private func checkAndUpdateRefreshRate() {
guard let screenRefreshRate = client?.getScreenRefreshRate() else {
logger.warning("Failed to get screen refresh rate.")
return
}
let contentFps = client?.currentContainerFps ?? screenRefreshRate
guard Defaults[.mpvSetRefreshToContentFPS] else {
// If the current refresh rate doesn't match the screen refresh rate, reset it
if client?.currentRefreshRate != screenRefreshRate {
client?.updateRefreshRate(to: screenRefreshRate)
client?.currentRefreshRate = screenRefreshRate
#if !os(macOS)
notifyViewToUpdateDisplayLink(with: screenRefreshRate)
#endif
logger.info("Reset refresh rate to screen's rate: \(screenRefreshRate) Hz")
}
return
}
// Adjust the refresh rate to match the content if it differs
if screenRefreshRate != contentFps {
client?.updateRefreshRate(to: contentFps)
client?.currentRefreshRate = contentFps
#if !os(macOS)
notifyViewToUpdateDisplayLink(with: contentFps)
#endif
logger.info("Adjusted screen refresh rate to match content: \(contentFps) Hz")
} else if client?.currentRefreshRate != screenRefreshRate {
// Ensure the refresh rate is set back to the screen's rate if no adjustment is needed
client?.updateRefreshRate(to: screenRefreshRate)
client?.currentRefreshRate = screenRefreshRate
#if !os(macOS)
notifyViewToUpdateDisplayLink(with: screenRefreshRate)
#endif
logger.info("Checked and reset refresh rate to screen's rate: \(screenRefreshRate) Hz")
}
}
#if !os(macOS)
private func notifyViewToUpdateDisplayLink(with refreshRate: Int) {
NotificationCenter.default.post(name: .updateDisplayLinkFrameRate, object: nil, userInfo: ["refreshRate": refreshRate])
}
#endif
func handle(_ event: UnsafePointer<mpv_event>!) { func handle(_ event: UnsafePointer<mpv_event>!) {
logger.info(.init(stringLiteral: "RECEIVED event: \(String(cString: mpv_event_name(event.pointee.event_id)))")) logger.info(.init(stringLiteral: "RECEIVED event: \(String(cString: mpv_event_name(event.pointee.event_id)))"))
@ -621,14 +559,8 @@ final class MPVBackend: PlayerBackend {
} }
func addSubTrack(_ url: URL) { func addSubTrack(_ url: URL) {
Task { client?.removeSubs()
if let areSubtitlesAdded = client?.areSubtitlesAdded { client?.addSubTrack(url)
if await areSubtitlesAdded() {
await client?.removeSubs()
}
}
await client?.addSubTrack(url)
}
} }
func setVideoToAuto() { func setVideoToAuto() {
@ -692,17 +624,6 @@ final class MPVBackend: PlayerBackend {
} }
} }
private func handleCaptionsChange() async {
guard let captions else {
if let isSubtitlesAdded = client?.areSubtitlesAdded, await isSubtitlesAdded() {
await client?.removeSubs()
}
return
}
addSubTrack(captions.url)
}
private func handlePropertyChange(_ name: String, _ property: mpv_event_property) { private func handlePropertyChange(_ name: String, _ property: mpv_event_property) {
switch name { switch name {
case "pause": case "pause":
@ -728,4 +649,33 @@ 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
} }

View File

@ -6,8 +6,6 @@ import Logging
#if !os(macOS) #if !os(macOS)
import Siesta import Siesta
import UIKit import UIKit
#else
import AppKit
#endif #endif
final class MPVClient: ObservableObject { final class MPVClient: ObservableObject {
@ -16,8 +14,6 @@ final class MPVClient: ObservableObject {
} }
private var logger = Logger(label: "mpv-client") private var logger = Logger(label: "mpv-client")
private var needsDrawingCooldown = false
private var needsDrawingWorkItem: DispatchWorkItem?
var mpv: OpaquePointer! var mpv: OpaquePointer!
var mpvGL: OpaquePointer! var mpvGL: OpaquePointer!
@ -31,7 +27,6 @@ final class MPVClient: ObservableObject {
var backend: MPVBackend! var backend: MPVBackend!
var seeking = false var seeking = false
var currentRefreshRate = 60
func create(frame: CGRect? = nil) { func create(frame: CGRect? = nil) {
#if !os(macOS) #if !os(macOS)
@ -42,7 +37,7 @@ final class MPVClient: ObservableObject {
mpv = mpv_create() mpv = mpv_create()
if mpv == nil { if mpv == nil {
logger.critical("failed creating context\n") print("failed creating context\n")
exit(1) exit(1)
} }
@ -79,29 +74,6 @@ final class MPVClient: ObservableObject {
checkError(mpv_set_option_string(mpv, "user-agent", UserAgentManager.shared.userAgent)) checkError(mpv_set_option_string(mpv, "user-agent", UserAgentManager.shared.userAgent))
checkError(mpv_set_option_string(mpv, "initial-audio-sync", Defaults[.mpvInitialAudioSync] ? "yes" : "no")) checkError(mpv_set_option_string(mpv, "initial-audio-sync", Defaults[.mpvInitialAudioSync] ? "yes" : "no"))
// Enable VSYNC needed for `video-sync`
if Defaults[.mpvSetRefreshToContentFPS] {
checkError(mpv_set_option_string(mpv, "opengl-swapinterval", "1"))
checkError(mpv_set_option_string(mpv, "video-sync", "display-resample"))
checkError(mpv_set_option_string(mpv, "interpolation", "yes"))
checkError(mpv_set_option_string(mpv, "tscale", "mitchell"))
checkError(mpv_set_option_string(mpv, "tscale-window", "blackman"))
checkError(mpv_set_option_string(mpv, "vd-lavc-framedrop", "nonref"))
checkError(mpv_set_option_string(mpv, "display-fps-override", "\(String(getScreenRefreshRate()))"))
}
// CPU //
// Determine number of threads based on system core count
let numberOfCores = ProcessInfo.processInfo.processorCount
let threads = numberOfCores * 2
// Log the number of cores and threads
logger.info("Number of CPU cores: \(numberOfCores)")
// Set the number of threads dynamically
checkError(mpv_set_option_string(mpv, "vd-lavc-threads", "\(threads)"))
// GPU // // GPU //
checkError(mpv_set_option_string(mpv, "hwdec", Defaults[.mpvHWdec])) checkError(mpv_set_option_string(mpv, "hwdec", Defaults[.mpvHWdec]))
@ -109,6 +81,7 @@ final class MPVClient: ObservableObject {
// We set set everything to OpenGL so MPV doesn't have to probe for other APIs. // We set set everything to OpenGL so MPV doesn't have to probe for other APIs.
checkError(mpv_set_option_string(mpv, "gpu-api", "opengl")) checkError(mpv_set_option_string(mpv, "gpu-api", "opengl"))
checkError(mpv_set_option_string(mpv, "opengl-swapinterval", "0"))
#if !os(macOS) #if !os(macOS)
checkError(mpv_set_option_string(mpv, "opengl-es", "yes")) checkError(mpv_set_option_string(mpv, "opengl-es", "yes"))
@ -139,7 +112,7 @@ final class MPVClient: ObservableObject {
get_proc_address_ctx: nil get_proc_address_ctx: nil
) )
queue = DispatchQueue(label: "mpv", qos: .userInteractive, attributes: [.concurrent]) queue = DispatchQueue(label: "mpv")
withUnsafeMutablePointer(to: &initParams) { initParams in withUnsafeMutablePointer(to: &initParams) { initParams in
var params = [ var params = [
@ -149,7 +122,7 @@ final class MPVClient: ObservableObject {
] ]
if mpv_render_context_create(&mpvGL, mpv, &params) < 0 { if mpv_render_context_create(&mpvGL, mpv, &params) < 0 {
logger.critical("failed to initialize mpv GL context") print("failed to initialize mpv GL context")
exit(1) exit(1)
} }
@ -345,31 +318,6 @@ final class MPVClient: ObservableObject {
mpv.isNil ? false : getFlag("eof-reached") mpv.isNil ? false : getFlag("eof-reached")
} }
var currentContainerFps: Int {
guard !mpv.isNil else { return 30 }
let fps = getDouble("container-fps")
return Int(fps.rounded())
}
func areSubtitlesAdded() async -> Bool {
guard !mpv.isNil else { return false }
let trackCount = await Task(operation: { getInt("track-list/count") }).value
guard trackCount > 0 else { return false }
for index in 0 ..< trackCount {
if let trackType = await Task(operation: { getString("track-list/\(index)/type") }).value, trackType == "sub" {
return true
}
}
return false
}
func logCurrentFps() {
let fps = currentContainerFps
logger.info("Current container FPS: \(fps)")
}
func seek(relative time: CMTime, completionHandler: ((Bool) -> Void)? = nil) { func seek(relative time: CMTime, completionHandler: ((Bool) -> Void)? = nil) {
guard !seeking else { guard !seeking else {
logger.warning("ignoring seek, another in progress") logger.warning("ignoring seek, another in progress")
@ -413,7 +361,7 @@ final class MPVClient: ObservableObject {
return return
} }
DispatchQueue.main.async(qos: .userInteractive) { [weak self] in DispatchQueue.main.async { [weak self] in
guard let self else { return } guard let self else { return }
let model = self.backend.model let model = self.backend.model
let aspectRatio = self.aspectRatio > 0 && self.aspectRatio < VideoPlayerView.defaultAspectRatio ? self.aspectRatio : VideoPlayerView.defaultAspectRatio let aspectRatio = self.aspectRatio > 0 && self.aspectRatio < VideoPlayerView.defaultAspectRatio ? self.aspectRatio : VideoPlayerView.defaultAspectRatio
@ -441,30 +389,10 @@ final class MPVClient: ObservableObject {
} }
func setNeedsDrawing(_ needsDrawing: Bool) { func setNeedsDrawing(_ needsDrawing: Bool) {
// Check if we are currently in a cooldown period
guard !needsDrawingCooldown else {
logger.info("Not drawing, cooldown in progress")
return
}
logger.info("needs drawing: \(needsDrawing)") logger.info("needs drawing: \(needsDrawing)")
// Set the cooldown flag to true and cancel any existing work item
needsDrawingCooldown = true
needsDrawingWorkItem?.cancel()
#if !os(macOS) #if !os(macOS)
glView?.needsDrawing = needsDrawing glView?.needsDrawing = needsDrawing
#endif #endif
// Create a new DispatchWorkItem to reset the cooldown flag after 0.1 seconds
let workItem = DispatchWorkItem { [weak self] in
self?.needsDrawingCooldown = false
}
needsDrawingWorkItem = workItem
// Schedule the cooldown reset after 0.1 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: workItem)
} }
func command( func command(
@ -492,59 +420,16 @@ final class MPVClient: ObservableObject {
} }
} }
func updateRefreshRate(to refreshRate: Int) {
setString("display-fps-override", "\(String(refreshRate))")
logger.info("Updated refresh rate during playback to: \(refreshRate) Hz")
}
// Retrieve the screen's current refresh rate dynamically.
func getScreenRefreshRate() -> Int {
var refreshRate = 60 // Default to 60 Hz in case of failure
#if os(macOS)
// macOS implementation using NSScreen
if let screen = NSScreen.main,
let displayID = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? CGDirectDisplayID,
let mode = CGDisplayCopyDisplayMode(displayID),
mode.refreshRate > 0
{
refreshRate = Int(mode.refreshRate)
logger.info("Screen refresh rate: \(refreshRate) Hz")
} else {
logger.warning("Failed to get refresh rate from NSScreen.")
}
#else
// iOS implementation using UIScreen with a failover
let mainScreen = UIScreen.main
refreshRate = mainScreen.maximumFramesPerSecond
// Failover: if maximumFramesPerSecond is 0 or an unexpected value
if refreshRate <= 0 {
refreshRate = 60 // Fallback to 60 Hz
logger.warning("Failed to get refresh rate from UIScreen, falling back to 60 Hz.")
} else {
logger.info("Screen refresh rate: \(refreshRate) Hz")
}
#endif
currentRefreshRate = refreshRate
return refreshRate
}
func addVideoTrack(_ url: URL) { func addVideoTrack(_ url: URL) {
command("video-add", args: [url.absoluteString]) command("video-add", args: [url.absoluteString])
} }
func addSubTrack(_ url: URL) async { func addSubTrack(_ url: URL) {
await Task {
command("sub-add", args: [url.absoluteString]) command("sub-add", args: [url.absoluteString])
}.value
} }
func removeSubs() async { func removeSubs() {
await Task {
command("sub-remove") command("sub-remove")
}.value
} }
func setVideoToAuto() { func setVideoToAuto() {

View File

@ -153,9 +153,7 @@ 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
// Check if the stream's resolution is within the maximum allowed resolution let isWithinResolution = $0.resolution <= maxResolution.value
let isWithinResolution = $0.resolution.map { $0 <= 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
@ -189,7 +187,6 @@ extension PlayerBackend {
} }
let filteredStreams = adjustedStreams.filter { stream in let filteredStreams = adjustedStreams.filter { stream in
// Check if the stream's resolution is within the maximum allowed resolution
let isWithinResolution = stream.resolution <= maxResolution.value let isWithinResolution = stream.resolution <= maxResolution.value
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

View File

@ -47,7 +47,7 @@ final class PlayerModel: ObservableObject {
static var shared = PlayerModel() static var shared = PlayerModel()
let logger = Logger(label: "stream.yattee.player.model") let logger = Logger(label: "stream.yattee.app")
var playerItem: AVPlayerItem? var playerItem: AVPlayerItem?
@ -130,15 +130,7 @@ final class PlayerModel: ObservableObject {
#if os(iOS) #if os(iOS)
@Published var lockedOrientation: UIInterfaceOrientationMask? @Published var lockedOrientation: UIInterfaceOrientationMask?
@Published var isOrientationLocked: Bool { @Default(.rotateToLandscapeOnEnterFullScreen) private var rotateToLandscapeOnEnterFullScreen
didSet {
Defaults[.isOrientationLocked] = isOrientationLocked
}
}
@Default(.rotateToLandscapeOnEnterFullScreen) var rotateToLandscapeOnEnterFullScreen
@Default(.lockPortraitWhenBrowsing) var lockPortraitWhenBrowsing
var fullscreenInitiatedByButton = false
#endif #endif
@Published var currentChapterIndex: Int? @Published var currentChapterIndex: Int?
@ -203,43 +195,14 @@ final class PlayerModel: ObservableObject {
var rateToRestore: Float? var rateToRestore: Float?
private var remoteCommandCenterConfigured = false private var remoteCommandCenterConfigured = false
// Used in the PlayerModel extension in PlayerQueue
var retryAttempts = [String: Int]()
#if os(macOS) #if os(macOS)
var keyPressMonitor: Any? var keyPressMonitor: Any?
#endif #endif
init() { init() {
#if os(iOS)
isOrientationLocked = Defaults[.isOrientationLocked]
if isOrientationLocked, lockPortraitWhenBrowsing {
lockedOrientation = UIInterfaceOrientationMask.portrait
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
} else if isOrientationLocked {
lockOrientationAction()
}
#endif
#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
)
// Register for audio session route change notifications
NotificationCenter.default.addObserver(
self,
selector: #selector(handleRouteChange(_:)),
name: AVAudioSession.routeChangeNotification,
object: AVAudioSession.sharedInstance()
)
#endif #endif
playbackMode = Defaults[.playbackMode] playbackMode = Defaults[.playbackMode]
@ -256,20 +219,6 @@ final class PlayerModel: ObservableObject {
currentRate = playerRate currentRate = playerRate
} }
#if !os(macOS)
deinit {
NotificationCenter.default.removeObserver(
self, name: AVAudioSession.interruptionNotification, object: nil
)
NotificationCenter.default.removeObserver(
self,
name: AVAudioSession.routeChangeNotification,
object: AVAudioSession.sharedInstance()
)
}
#endif
func show() { func show() {
#if os(macOS) #if os(macOS)
if presentingPlayer { if presentingPlayer {
@ -553,10 +502,7 @@ final class PlayerModel: ObservableObject {
} }
private func handlePresentationChange() { private func handlePresentationChange() {
#if os(macOS)
// TODO: Check whether this is needed on macOS
backend.setNeedsDrawing(presentingPlayer) backend.setNeedsDrawing(presentingPlayer)
#endif
#if os(iOS) #if os(iOS)
if presentingPlayer, activeBackend == .appleAVPlayer, avPlayerUsesSystemControls, Constants.isIPhone { if presentingPlayer, activeBackend == .appleAVPlayer, avPlayerUsesSystemControls, Constants.isIPhone {
@ -585,11 +531,13 @@ final class PlayerModel: ObservableObject {
if !presentingPlayer { if !presentingPlayer {
#if os(iOS) #if os(iOS)
if lockPortraitWhenBrowsing { if Defaults[.lockPortraitWhenBrowsing] {
Orientation.lockOrientation(.portrait, andRotateTo: .portrait) Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
} else { } else {
Orientation.lockOrientation(.all) Orientation.lockOrientation(.allButUpsideDown)
} }
OrientationModel.shared.stopOrientationUpdates()
#endif #endif
} }
} }
@ -696,37 +644,32 @@ final class PlayerModel: ObservableObject {
} }
func closeCurrentItem(finished: Bool = false) { func closeCurrentItem(finished: Bool = false) {
guard !closing else { return }
closing = true
if playingFullScreen { exitFullScreen() }
Delay.by(0.3) { [weak self] in
guard let self else { return }
pause() pause()
videoBeingOpened = nil videoBeingOpened = nil
advancing = false advancing = false
forceBackendOnPlay = nil forceBackendOnPlay = nil
closing = true
controls.presentingControls = false controls.presentingControls = false
self.prepareCurrentItemForHistory(finished: finished) self.prepareCurrentItemForHistory(finished: finished)
self.hide() self.hide()
Delay.by(0.7) { [weak self] in Delay.by(0.8) { [weak self] in
guard let self else { return } guard let self else { return }
if playingInPictureInPicture { self.closePiP() } self.closePiP()
withAnimation { withAnimation {
self.currentItem = nil self.currentItem = nil
} }
self.updateNowPlayingInfo() self.updateNowPlayingInfo()
self.backend.closeItem() self.backend.closeItem()
self.aspectRatio = VideoPlayerView.defaultAspectRatio self.aspectRatio = VideoPlayerView.defaultAspectRatio
self.resetAutoplay() self.resetAutoplay()
self.closing = false self.closing = false
} self.playingFullScreen = false
} }
} }
@ -735,24 +678,38 @@ final class PlayerModel: ObservableObject {
avPlayerBackend.startPictureInPictureOnPlay = false avPlayerBackend.startPictureInPictureOnPlay = false
avPlayerBackend.startPictureInPictureOnSwitch = false avPlayerBackend.startPictureInPictureOnSwitch = false
guard activeBackend != .appleAVPlayer else { if activeBackend == .appleAVPlayer {
avPlayerBackend.tryStartingPictureInPicture() avPlayerBackend.tryStartingPictureInPicture()
return return
} }
avPlayerBackend.startPictureInPictureOnSwitch = true // First, we need to create an array with supported formats.
let formatOrderPiP: [QualityProfile.Format] = [.hls, .stream, .mp4]
saveTime { guard let video = currentVideo else { return }
self.changeActiveBackend(from: .mpv, to: .appleAVPlayer) guard let stream = avPlayerBackend.bestPlayable(availableStreams, maxResolution: .hd720p30, formatOrder: formatOrderPiP) else { return }
if avPlayerBackend.video == video {
if activeBackend != .appleAVPlayer {
avPlayerBackend.startPictureInPictureOnSwitch = true
}
changeActiveBackend(from: activeBackend, to: .appleAVPlayer)
} else {
avPlayerBackend.startPictureInPictureOnPlay = true
playStream(stream, of: video, preservingTime: true, upgrading: true, withBackend: avPlayerBackend)
}
var retryCount = 0
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in _ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
if let pipController = self?.pipController, pipController.isPictureInPictureActive, self?.avPlayerBackend.isPlaying == true { if let pipController = self?.pipController, pipController.isPictureInPictureActive, self?.avPlayerBackend.isPlaying == true {
self?.exitFullScreen() self?.exitFullScreen()
self?.controls.objectWillChange.send() self?.controls.objectWillChange.send()
timer.invalidate() timer.invalidate()
} else if self?.activeBackend == .appleAVPlayer, self?.avPlayerBackend.startPictureInPictureOnSwitch == false { } 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.startPictureInPictureOnSwitch = true
self?.avPlayerBackend.tryStartingPictureInPicture() self?.avPlayerBackend.tryStartingPictureInPicture()
} retryCount += 1
} }
} }
} }
@ -782,27 +739,19 @@ final class PlayerModel: ObservableObject {
show() show()
#endif #endif
avPlayerBackend.closePiP() if previousActiveBackend == .mpv {
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
if self?.activeBackend == .appleAVPlayer, self?.avPlayerBackend.isPlaying == true, self?.playingInPictureInPicture == false {
timer.invalidate()
}
}
guard previousActiveBackend == .mpv else { return }
saveTime { saveTime {
self.changeActiveBackend(from: .appleAVPlayer, to: .mpv, isInClosePip: true) self.changeActiveBackend(from: self.activeBackend, to: .mpv, isInClosePip: true)
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in _ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
if self?.activeBackend == .mpv, self?.mpvBackend.isPlaying == true { if self?.activeBackend == .mpv, self?.mpvBackend.isPlaying == true {
self?.backend.closePiP()
self?.controls.resetTimer()
timer.invalidate() timer.invalidate()
} }
} }
} }
} else {
// We need to remove the itme from the player, if not it will be displayed when next video goe to PiP. backend.closePiP()
Delay.by(1.0) {
self.avPlayerBackend.closeItem()
} }
} }
@ -815,7 +764,7 @@ final class PlayerModel: ObservableObject {
} }
func toggleFullScreenAction() { func toggleFullScreenAction() {
toggleFullscreen(playingFullScreen, showControls: false, initiatedByButton: true) toggleFullscreen(playingFullScreen, showControls: false)
} }
func togglePiPAction() { func togglePiPAction() {
@ -828,21 +777,20 @@ final class PlayerModel: ObservableObject {
#if os(iOS) #if os(iOS)
var lockOrientationImage: String { var lockOrientationImage: String {
isOrientationLocked ? "lock.rotation" : "lock.rotation.open" lockedOrientation.isNil ? "lock.rotation.open" : "lock.rotation"
} }
func lockOrientationAction() { func lockOrientationAction() {
// This makes toggling orientation lock more robust if lockedOrientation.isNil {
if lockedOrientation.isNil || !isOrientationLocked {
isOrientationLocked = true
let orientationMask = OrientationTracker.shared.currentInterfaceOrientationMask let orientationMask = OrientationTracker.shared.currentInterfaceOrientationMask
lockedOrientation = orientationMask lockedOrientation = orientationMask
let orientation = OrientationTracker.shared.currentInterfaceOrientation let orientation = OrientationTracker.shared.currentInterfaceOrientation
Orientation.lockOrientation(orientationMask, andRotateTo: playingFullScreen ? nil : orientation) Orientation.lockOrientation(orientationMask, andRotateTo: .landscapeLeft)
// iOS 16 workaround
Orientation.lockOrientation(orientationMask, andRotateTo: orientation)
} else { } else {
isOrientationLocked = false
lockedOrientation = nil lockedOrientation = nil
Orientation.lockOrientation(.all) Orientation.lockOrientation(.allButUpsideDown, andRotateTo: OrientationTracker.shared.currentInterfaceOrientation)
} }
} }
#endif #endif
@ -932,29 +880,26 @@ final class PlayerModel: ObservableObject {
} }
func updateRemoteCommandCenter() { func updateRemoteCommandCenter() {
let commandCenter = MPRemoteCommandCenter.shared() let skipForwardCommand = MPRemoteCommandCenter.shared().skipForwardCommand
let skipForwardCommand = commandCenter.skipForwardCommand let skipBackwardCommand = MPRemoteCommandCenter.shared().skipBackwardCommand
let skipBackwardCommand = commandCenter.skipBackwardCommand let previousTrackCommand = MPRemoteCommandCenter.shared().previousTrackCommand
let previousTrackCommand = commandCenter.previousTrackCommand let nextTrackCommand = MPRemoteCommandCenter.shared().nextTrackCommand
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
@ -978,22 +923,22 @@ final class PlayerModel: ObservableObject {
return .success return .success
} }
commandCenter.playCommand.addTarget { [weak self] _ in MPRemoteCommandCenter.shared().playCommand.addTarget { [weak self] _ in
self?.play() self?.play()
return .success return .success
} }
commandCenter.pauseCommand.addTarget { [weak self] _ in MPRemoteCommandCenter.shared().pauseCommand.addTarget { [weak self] _ in
self?.pause() self?.pause()
return .success return .success
} }
commandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in MPRemoteCommandCenter.shared().togglePlayPauseCommand.addTarget { [weak self] _ in
self?.togglePlay() self?.togglePlay()
return .success return .success
} }
commandCenter.changePlaybackPositionCommand.addTarget { [weak self] remoteEvent in MPRemoteCommandCenter.shared().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)
@ -1028,43 +973,25 @@ final class PlayerModel: ObservableObject {
} }
#else #else
func handleEnterForeground() { func handleEnterForeground() {
DispatchQueue.global(qos: .userInteractive).async { [weak self] in setNeedsDrawing(presentingPlayer)
guard let self = self else { return }
if !self.musicMode, self.activeBackend == .mpv { if !musicMode, activeBackend == .appleAVPlayer {
self.mpvBackend.addVideoTrackFromStream() avPlayerBackend.bindPlayerToLayer()
self.mpvBackend.setVideoToAuto()
self.mpvBackend.controls.resetTimer()
} else if !self.musicMode, self.activeBackend == .appleAVPlayer {
self.avPlayerBackend.bindPlayerToLayer()
} }
}
#if os(iOS)
OrientationTracker.shared.startDeviceOrientationTracking()
#endif
guard closePiPAndOpenPlayerOnEnteringForeground, playingInPictureInPicture else { guard closePiPAndOpenPlayerOnEnteringForeground, playingInPictureInPicture else {
return return
} }
show() show()
// Needs to be delayed a bit, otherwise the PiP windows stays open closePiP()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
self?.closePiP()
}
} }
func handleEnterBackground() { func handleEnterBackground() {
#if os(iOS)
OrientationTracker.shared.stopDeviceOrientationTracking()
#endif
if Defaults[.pauseOnEnteringBackground], !playingInPictureInPicture, !musicMode { if Defaults[.pauseOnEnteringBackground], !playingInPictureInPicture, !musicMode {
pause() pause()
} else if !playingInPictureInPicture, activeBackend == .appleAVPlayer { } else if !playingInPictureInPicture {
avPlayerBackend.removePlayerFromLayer() avPlayerBackend.removePlayerFromLayer()
} else if activeBackend == .mpv, !musicMode {
mpvBackend.setVideoToNo()
} }
} }
#endif #endif
@ -1090,22 +1017,18 @@ 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,
@ -1113,7 +1036,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: mediaType MPMediaItemPropertyMediaType: MPMediaType.anyVideo.rawValue as AnyObject
] ]
if !currentArtwork.isNil { if !currentArtwork.isNil {
@ -1134,7 +1057,7 @@ final class PlayerModel: ObservableObject {
func updateCurrentArtwork() { func updateCurrentArtwork() {
guard let video = currentVideo, guard let video = currentVideo,
let thumbnailURL = video.thumbnailURL(quality: Constants.isIPhone ? .medium : .maxres) let thumbnailURL = video.thumbnailURL(quality: .medium)
else { else {
return return
} }
@ -1156,7 +1079,7 @@ final class PlayerModel: ObservableObject {
task.resume() task.resume()
} }
func toggleFullscreen(_ isFullScreen: Bool, showControls: Bool = true, initiatedByButton: Bool = false) { func toggleFullscreen(_ isFullScreen: Bool, showControls: Bool = true) {
controls.presentingControls = showControls && isFullScreen controls.presentingControls = showControls && isFullScreen
#if os(macOS) #if os(macOS)
@ -1168,27 +1091,18 @@ final class PlayerModel: ObservableObject {
#if os(iOS) #if os(iOS)
if playingFullScreen { if playingFullScreen {
if activeBackend == .appleAVPlayer, avPlayerUsesSystemControls { if activeBackend == .appleAVPlayer, avPlayerUsesSystemControls {
fullscreenInitiatedByButton = initiatedByButton
avPlayerBackend.controller.enterFullScreen(animated: true) avPlayerBackend.controller.enterFullScreen(animated: true)
return return
} }
let lockOrientation = rotateToLandscapeOnEnterFullScreen.interfaceOrientation guard rotateToLandscapeOnEnterFullScreen.isRotating else { return }
if currentVideoIsLandscape { if currentVideoIsLandscape {
if initiatedByButton { let delay = activeBackend == .appleAVPlayer && avPlayerUsesSystemControls ? 0.8 : 0
Orientation.lockOrientation(isOrientationLocked // not sure why but first rotation call is ignore so doing rotate to same orientation first
? (lockOrientation == .landscapeRight ? .landscapeRight : .landscapeLeft) Delay.by(delay) {
: .landscape) let orientation = OrientationTracker.shared.currentDeviceOrientation.isLandscape ? OrientationTracker.shared.currentInterfaceOrientation : self.rotateToLandscapeOnEnterFullScreen.interaceOrientation
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: OrientationTracker.shared.currentInterfaceOrientation)
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: orientation)
} }
let orientation = OrientationTracker.shared.currentDeviceOrientation.isLandscape
? OrientationTracker.shared.currentInterfaceOrientation
: rotateToLandscapeOnEnterFullScreen.interfaceOrientation
Orientation.lockOrientation(
isOrientationLocked
? (lockOrientation == .landscapeRight ? .landscapeRight : .landscapeLeft)
: .all,
andRotateTo: orientation
)
} }
} else { } else {
if activeBackend == .appleAVPlayer, avPlayerUsesSystemControls { if activeBackend == .appleAVPlayer, avPlayerUsesSystemControls {
@ -1196,12 +1110,10 @@ final class PlayerModel: ObservableObject {
avPlayerBackend.controller.dismiss(animated: true) avPlayerBackend.controller.dismiss(animated: true)
return return
} }
if lockPortraitWhenBrowsing { let rotationOrientation = Constants.isIPhone ? UIInterfaceOrientation.portrait : nil
lockedOrientation = UIInterfaceOrientationMask.portrait Orientation.lockOrientation(.allButUpsideDown, andRotateTo: rotationOrientation)
}
let rotationOrientation = lockPortraitWhenBrowsing ? UIInterfaceOrientation.portrait : nil
Orientation.lockOrientation(lockPortraitWhenBrowsing ? .portrait : .all, andRotateTo: rotationOrientation)
} }
#endif #endif
} }
@ -1291,143 +1203,9 @@ final class PlayerModel: ObservableObject {
return nil return nil
} }
#if !os(macOS)
func setAudioSessionActive(_ setActive: Bool) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
do {
try AVAudioSession.sharedInstance().setActive(setActive)
} catch {
self.logger.error("Error setting up audio session: \(error)")
}
}
}
@objc func handleAudioSessionInterruption(_ notification: Notification) {
logger.info("Audio session interruption received.")
logger.info("Notification object: \(String(describing: notification.object))")
guard let info = notification.userInfo else {
logger.info("userInfo is missing in the notification.")
return
}
// Extract the interruption type
guard 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)")
// Check availability for iOS 14.5 or newer to handle interruption reason
// Currently only for debugging purpose
#if os(iOS)
if #available(iOS 14.5, *) {
// Extract the interruption reason, if available
if let reasonValue = info[AVAudioSessionInterruptionReasonKey] as? UInt,
let reason = AVAudioSession.InterruptionReason(rawValue: reasonValue)
{
logger.info("Interruption reason received: \(reason)")
switch reason {
case .default:
logger.info("Interruption reason: Default or unspecified interruption occurred.")
case .appWasSuspended:
logger.info("Interruption reason: The app was suspended during the interruption.")
@unknown default:
logger.info("Unknown interruption reason received.")
}
} else {
logger.info("AVAudioSessionInterruptionReasonKey is missing or not a UInt in userInfo.")
}
} else {
logger.info("Interruption reason handling is not available on this iOS version.")
}
#endif
// Handle the specific interruption type
switch type {
case .began:
pause()
logger.info("Audio session interrupted (began).")
case .ended:
// Extract any interruption options, if available
if let optionsValue = info[AVAudioSessionInterruptionOptionKey] as? UInt {
logger.info("Interruption options received: \(optionsValue)")
if optionsValue & AVAudioSession.InterruptionOptions.shouldResume.rawValue != 0 {
play()
logger.info("Interruption option indicates playback should resume automatically.")
} else {
logger.info("Interruption option indicates playback should not resume automatically.")
}
} else {
logger.info("AVAudioSessionInterruptionOptionKey is missing or not a UInt in userInfo.")
}
logger.info("Audio session interruption ended.")
// Check if audio was resumed or if there's any indication of ducking
let currentVolume = AVAudioSession.sharedInstance().outputVolume
logger.info("Current output volume: \(currentVolume)")
default:
logger.info("Unknown interruption type received.")
}
}
@objc func handleRouteChange(_ notification: Notification) {
logger.info("Audio route change received.")
guard let info = notification.userInfo else {
logger.info("userInfo is missing in the notification.")
return
}
guard let reasonValue = info[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue)
else {
logger.info("AVAudioSessionRouteChangeReasonKey is missing or not a UInt in userInfo.")
return
}
logger.info("Route change reason received: \(reason)")
let currentCategory = AVAudioSession.sharedInstance().category
logger.info("Current audio session category before change: \(currentCategory)")
switch reason {
case .categoryChange:
logger.info("Audio session category changed.")
let newCategory = AVAudioSession.sharedInstance().category
logger.info("New audio session category: \(newCategory)")
case .oldDeviceUnavailable, .newDeviceAvailable:
logger.info("Audio route change may indicate ducking or device change.")
let currentRoute = AVAudioSession.sharedInstance().currentRoute
logger.info("Current audio route: \(currentRoute)")
for output in currentRoute.outputs {
logger.info("Output port type: \(output.portType), UID: \(output.uid)")
switch output.portType {
case .headphones, .bluetoothA2DP:
logger.info("Detected port type \(output.portType). Executing play().")
play()
default:
logger.info("Detected port type \(output.portType). Executing pause().")
pause()
}
}
case .noSuitableRouteForCategory:
logger.info("No suitable route for the current category.")
default:
logger.info("Unhandled route change reason: \(reason)")
}
}
#endif
#if os(macOS) #if os(macOS)
private func assignKeyPressMonitor() { private func assignKeyPressMonitor() {
keyPressMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] keyEvent -> NSEvent? in keyPressMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { keyEvent -> NSEvent? in
// Check if the player window is the key window
guard let self, let window = Windows.playerWindow, window.isKeyWindow else { return keyEvent }
switch keyEvent.keyCode { switch keyEvent.keyCode {
case 124: case 124:
if !self.liveStreamInAVPlayer { if !self.liveStreamInAVPlayer {
@ -1462,7 +1240,7 @@ final class PlayerModel: ObservableObject {
} }
private func destroyKeyPressMonitor() { private func destroyKeyPressMonitor() {
if let keyPressMonitor { if let keyPressMonitor = keyPressMonitor {
NSEvent.removeMonitor(keyPressMonitor) NSEvent.removeMonitor(keyPressMonitor)
} }
} }

View File

@ -359,31 +359,6 @@ extension PlayerModel {
} }
private func videoLoadFailureHandler(_ error: RequestError, video: Video? = nil) { private func videoLoadFailureHandler(_ error: RequestError, video: Video? = nil) {
guard let video else {
presentErrorAlert(error)
return
}
let videoID = video.videoID
let currentRetry = retryAttempts[videoID] ?? 0
if currentRetry < Defaults[.videoLoadingRetryCount] {
retryAttempts[videoID] = currentRetry + 1
logger.info("Retry attempt \(currentRetry + 1) for video \(videoID) due to error: \(error)")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
guard let self else { return }
self.enqueueVideo(video, play: true, prepending: true, loadDetails: true)
}
return
}
retryAttempts[videoID] = 0
presentErrorAlert(error, video: video)
}
private func presentErrorAlert(_ error: RequestError, video: Video? = nil) {
var message = error.userMessage var message = error.userMessage
if let errorDictionary = error.json.dictionaryObject, if let errorDictionary = error.json.dictionaryObject,
let errorMessage = errorDictionary["message"] ?? errorDictionary["error"], let errorMessage = errorDictionary["message"] ?? errorDictionary["error"],

View File

@ -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 avc1 case hls
case stream case stream
case webm case avc1
case mp4 case mp4
case av1 case av1
case hls case webm
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 .avc1: case .hls:
return .avc1 return nil
case .stream: case .stream:
return nil return nil
case .webm: case .avc1:
return .webm return .avc1
case .mp4: case .mp4:
return .mp4 return .mp4
case .av1: case .av1:
return .av1 return .av1
case .hls: case .webm:
return nil return .webm
} }
} }
} }
@ -59,16 +59,14 @@ struct QualityProfile: Hashable, Identifiable, Defaults.Serializable {
} }
var formatsDescription: String { var formatsDescription: String {
switch formats.count { if formats.count == Format.allCases.count {
case Format.allCases.count:
return "Any format".localized() return "Any format".localized()
case 0:
return "No format selected".localized()
case 1 ... 3:
return formats.map(\.description).joined(separator: ", ")
default:
return String(format: "%@ formats".localized(), String(formats.count))
} }
if formats.count <= 3 {
return formats.map(\.description).joined(separator: ", ")
}
return String(format: "%@ formats".localized(), String(formats.count))
} }
func isPreferred(_ stream: Stream) -> Bool { func isPreferred(_ stream: Stream) -> Bool {
@ -76,8 +74,7 @@ struct QualityProfile: Hashable, Identifiable, Defaults.Serializable {
return true return true
} }
let defaultResolution = Stream.Resolution.custom(height: 720, refreshRate: 30) let resolutionMatch = !stream.resolution.isNil && resolution.value >= stream.resolution
let resolutionMatch = resolution.value ?? defaultResolution >= stream.resolution
if resolutionMatch, formats.contains(.stream), stream.kind == .stream { if resolutionMatch, formats.contains(.stream), stream.kind == .stream {
return true return true

View File

@ -18,8 +18,6 @@ final class SearchModel: ObservableObject {
@Published var focused = false @Published var focused = false
@Default(.showSearchSuggestions) private var showSearchSuggestions
#if os(iOS) #if os(iOS)
var textField: UITextField! var textField: UITextField!
#elseif os(macOS) #elseif os(macOS)
@ -104,7 +102,7 @@ final class SearchModel: ObservableObject {
}} }}
func loadSuggestions(_ query: String) { func loadSuggestions(_ query: String) {
guard accounts.app.supportsSearchSuggestions, showSearchSuggestions else { guard accounts.app.supportsSearchSuggestions else {
querySuggestions.removeAll() querySuggestions.removeAll()
return return
} }

View File

@ -4,126 +4,288 @@ import Foundation
// swiftlint:disable:next final_class // swiftlint:disable:next final_class
class Stream: Equatable, Hashable, Identifiable { class Stream: Equatable, Hashable, Identifiable {
enum Resolution: Comparable, Codable, Defaults.Serializable { enum Resolution: String, CaseIterable, Comparable, Defaults.Serializable {
case predefined(PredefinedResolution) // Some 16:19 and 16:10 resolutions are also used in 2:1 videos
case custom(height: Int, refreshRate: Int)
enum PredefinedResolution: String, CaseIterable, Codable {
// 8K UHD (16:9) Resolutions // 8K UHD (16:9) Resolutions
case hd4320p60, hd4320p30 case hd4320p60
case hd4320p50
case hd4320p48
case hd4320p30
case hd4320p25
case hd4320p24
// 4K UHD (16:9) Resolutions // 5K (16:9) Resolutions
case hd2160p60, hd2160p30 case hd2560p60
case hd2560p50
case hd2560p48
case hd2560p30
case hd2560p25
case hd2560p24
// 1440p (16:9) Resolutions // 2:1 Aspect Ratio (Univisium) Resolutions
case hd1440p60, hd1440p30 case hd2880p60
case hd2880p50
case hd2880p48
case hd2880p30
case hd2880p25
case hd2880p24
// 1080p (Full HD, 16:9) Resolutions // 16:10 Resolutions
case hd1080p60, hd1080p30 case hd2400p60
case hd2400p50
case hd2400p48
case hd2400p30
case hd2400p25
case hd2400p24
// 720p (HD, 16:9) Resolutions // 16:9 Resolutions
case hd720p60, hd720p30 case hd2160p60
case hd2160p50
case hd2160p48
case hd2160p30
case hd2160p25
case hd2160p24
// 16:10 Resolutions
case hd1600p60
case hd1600p50
case hd1600p48
case hd1600p30
case hd1600p25
case hd1600p24
// 16:9 Resolutions
case hd1440p60
case hd1440p50
case hd1440p48
case hd1440p30
case hd1440p25
case hd1440p24
// 16:10 Resolutions
case hd1280p60
case hd1280p50
case hd1280p48
case hd1280p30
case hd1280p25
case hd1280p24
// 16:10 Resolutions
case hd1200p60
case hd1200p50
case hd1200p48
case hd1200p30
case hd1200p25
case hd1200p24
// 16:9 Resolutions
case hd1080p60
case hd1080p50
case hd1080p48
case hd1080p30
case hd1080p25
case hd1080p24
// 16:10 Resolutions
case hd1050p60
case hd1050p50
case hd1050p48
case hd1050p30
case hd1050p25
case hd1050p24
// 16:9 Resolutions
case hd960p60
case hd960p50
case hd960p48
case hd960p30
case hd960p25
case hd960p24
// 16:10 Resolutions
case hd900p60
case hd900p50
case hd900p48
case hd900p30
case hd900p25
case hd900p24
// 16:10 Resolutions
case hd800p60
case hd800p50
case hd800p48
case hd800p30
case hd800p25
case hd800p24
// 16:9 Resolutions
case hd720p60
case hd720p50
case hd720p48
case hd720p30
case hd720p25
case hd720p24
// Standard Definition (SD) Resolutions // Standard Definition (SD) Resolutions
case sd854p30
case sd854p25
case sd768p30
case sd768p25
case sd640p30
case sd640p25
case sd480p30 case sd480p30
case sd480p25
case sd428p30
case sd428p25
case sd360p30 case sd360p30
case sd360p25
case sd320p30
case sd320p25
case sd240p30 case sd240p30
case sd240p25
case sd214p30
case sd214p25
case sd144p30 case sd144p30
} case sd144p25
case sd128p30
case sd128p25
case unknown
var name: String { var name: String {
switch self { "\(height)p\(refreshRate != -1 && refreshRate != 30 ? ", \(refreshRate) fps" : "")"
case let .predefined(predefined):
return predefined.rawValue
case let .custom(height, refreshRate):
return "\(height)p\(refreshRate != 30 ? ", \(refreshRate) fps" : "")"
}
} }
var height: Int { var height: Int {
switch self { if self == .unknown {
case let .predefined(predefined): return -1
return predefined.height
case let .custom(height, _):
return height
} }
let resolutionPart = rawValue.components(separatedBy: "p").first!
return Int(resolutionPart.components(separatedBy: CharacterSet.decimalDigits.inverted).joined())!
} }
var refreshRate: Int { var refreshRate: Int {
switch self { if self == .unknown {
case let .predefined(predefined): return -1
return predefined.refreshRate
case let .custom(_, refreshRate):
return refreshRate
} }
let refreshRatePart = rawValue.components(separatedBy: "p")[1]
if refreshRatePart.isEmpty {
return 30
} }
return Int(refreshRatePart.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()) ?? -1
}
// These values are an approximation.
// https://support.google.com/youtube/answer/1722171?hl=en#zippy=%2Cbitrate
var bitrate: Int { var bitrate: Int {
switch self { switch self {
case let .predefined(predefined): // 8K UHD (16:9) Resolutions
return predefined.bitrate case .hd4320p60, .hd4320p50, .hd4320p48, .hd4320p30, .hd4320p25, .hd4320p24:
case let .custom(height, refreshRate): return 85_000_000 // 85 Mbit/s
// Find the closest predefined resolution based on height and refresh rate
let closestPredefined = Stream.Resolution.PredefinedResolution.allCases.min { // 5K (16:9) Resolutions
abs($0.height - height) + abs($0.refreshRate - refreshRate) < case .hd2880p60, .hd2880p50, .hd2880p48, .hd2880p30, .hd2880p25, .hd2880p24:
abs($1.height - height) + abs($1.refreshRate - refreshRate) return 45_000_000 // 45 Mbit/s
}
// Return the bitrate of the closest predefined resolution or a default bitrate if no close match is found // 2:1 Aspect Ratio (Univisium) Resolutions
return closestPredefined?.bitrate ?? 5_000_000 case .hd2560p60, .hd2560p50, .hd2560p48, .hd2560p30, .hd2560p25, .hd2560p24:
return 30_000_000 // 30 Mbit/s
// 16:10 Resolutions
case .hd2400p60, .hd2400p50, .hd2400p48, .hd2400p30, .hd2400p25, .hd2400p24:
return 35_000_000 // 35 Mbit/s
// 4K UHD (16:9) Resolutions
case .hd2160p60, .hd2160p50, .hd2160p48, .hd2160p30, .hd2160p25, .hd2160p24:
return 56_000_000 // 56 Mbit/s
// 16:10 Resolutions
case .hd1600p60, .hd1600p50, .hd1600p48, .hd1600p30, .hd1600p25, .hd1600p24:
return 20_000_000 // 20 Mbit/s
// 1440p (16:9) Resolutions
case .hd1440p60, .hd1440p50, .hd1440p48, .hd1440p30, .hd1440p25, .hd1440p24:
return 24_000_000 // 24 Mbit/s
// 1280p (16:10) Resolutions
case .hd1280p60, .hd1280p50, .hd1280p48, .hd1280p30, .hd1280p25, .hd1280p24:
return 15_000_000 // 15 Mbit/s
// 1200p (16:10) Resolutions
case .hd1200p60, .hd1200p50, .hd1200p48, .hd1200p30, .hd1200p25, .hd1200p24:
return 18_000_000 // 18 Mbit/s
// 1080p (16:9) Resolutions
case .hd1080p60, .hd1080p50, .hd1080p48, .hd1080p30, .hd1080p25, .hd1080p24:
return 12_000_000 // 12 Mbit/s
// 1050p (16:10) Resolutions
case .hd1050p60, .hd1050p50, .hd1050p48, .hd1050p30, .hd1050p25, .hd1050p24:
return 10_000_000 // 10 Mbit/s
// 960p Resolutions
case .hd960p60, .hd960p50, .hd960p48, .hd960p30, .hd960p25, .hd960p24:
return 8_000_000 // 8 Mbit/s
// 900p (16:10) Resolutions
case .hd900p60, .hd900p50, .hd900p48, .hd900p30, .hd900p25, .hd900p24:
return 7_000_000 // 7 Mbit/s
// 800p (16:10) Resolutions
case .hd800p60, .hd800p50, .hd800p48, .hd800p30, .hd800p25, .hd800p24:
return 6_000_000 // 6 Mbit/s
// 720p (16:9) Resolutions
case .hd720p60, .hd720p50, .hd720p48, .hd720p30, .hd720p25, .hd720p24:
return 9_500_000 // 9.5 Mbit/s
// Standard Definition (SD) Resolutions
case .sd854p30, .sd854p25, .sd768p30, .sd768p25, .sd640p30, .sd640p25:
return 4_000_000 // 4 Mbit/s
case .sd480p30, .sd480p25:
return 2_500_000 // 2.5 Mbit/s
case .sd428p30, .sd428p25:
return 2_000_000 // 2 Mbit/s
case .sd360p30, .sd360p25:
return 1_500_000 // 1.5 Mbit/s
case .sd320p30, .sd320p25:
return 1_200_000 // 1.2 Mbit/s
case .sd240p30, .sd240p25:
return 1_000_000 // 1 Mbit/s
case .sd214p30, .sd214p25:
return 800_000 // 0.8 Mbit/s
case .sd144p30, .sd144p25:
return 600_000 // 0.6 Mbit/s
case .sd128p30, .sd128p25:
return 400_000 // 0.4 Mbit/s
case .unknown:
return 0
} }
} }
static func from(resolution: String, fps: Int? = nil) -> Self { static func from(resolution: String, fps: Int? = nil) -> Self {
if let predefined = PredefinedResolution(rawValue: resolution) { allCases.first { $0.rawValue.contains(resolution) && $0.refreshRate == (fps ?? 30) } ?? .unknown
return .predefined(predefined)
}
// Attempt to parse height and refresh rate
if let height = Int(resolution.components(separatedBy: "p").first ?? ""), height > 0 {
let refreshRate = fps ?? 30
return .custom(height: height, refreshRate: refreshRate)
}
// Default behavior if parsing fails
return .custom(height: 720, refreshRate: 30)
} }
static func < (lhs: Self, rhs: Self) -> Bool { static func < (lhs: Self, rhs: Self) -> Bool {
lhs.height == rhs.height ? (lhs.refreshRate < rhs.refreshRate) : (lhs.height < rhs.height) lhs.height == rhs.height ? (lhs.refreshRate < rhs.refreshRate) : (lhs.height < rhs.height)
} }
enum CodingKeys: String, CodingKey {
case predefined
case custom
case height
case refreshRate
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let predefinedValue = try? container.decode(PredefinedResolution.self, forKey: .predefined) {
self = .predefined(predefinedValue)
} else if let height = try? container.decode(Int.self, forKey: .height),
let refreshRate = try? container.decode(Int.self, forKey: .refreshRate)
{
self = .custom(height: height, refreshRate: refreshRate)
} else {
// Set default resolution to 720p 30 if decoding fails
self = .custom(height: 720, refreshRate: 30)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .predefined(predefinedValue):
try container.encode(predefinedValue, forKey: .predefined)
case let .custom(height, refreshRate):
try container.encode(height, forKey: .height)
try container.encode(refreshRate, forKey: .refreshRate)
}
}
} }
enum Kind: String, Comparable { enum Kind: String, Comparable {
@ -316,97 +478,3 @@ class Stream: Equatable, Hashable, Identifiable {
} }
} }
} }
extension Stream.Resolution.PredefinedResolution {
var height: Int {
switch self {
// 8K UHD (16:9) Resolutions
case .hd4320p60, .hd4320p30:
return 4320
// 4K UHD (16:9) Resolutions
case .hd2160p60, .hd2160p30:
return 2160
// 1440p (16:9) Resolutions
case .hd1440p60, .hd1440p30:
return 1440
// 1080p (Full HD, 16:9) Resolutions
case .hd1080p60, .hd1080p30:
return 1080
// 720p (HD, 16:9) Resolutions
case .hd720p60, .hd720p30:
return 720
// Standard Definition (SD) Resolutions
case .sd480p30:
return 480
case .sd360p30:
return 360
case .sd240p30:
return 240
case .sd144p30:
return 144
}
}
var refreshRate: Int {
switch self {
// 60 fps Resolutions
case .hd4320p60, .hd2160p60, .hd1440p60, .hd1080p60, .hd720p60:
return 60
// 30 fps Resolutions
case .hd4320p30, .hd2160p30, .hd1440p30, .hd1080p30, .hd720p30,
.sd480p30, .sd360p30, .sd240p30, .sd144p30:
return 30
}
}
// These values are an approximation.
// https://support.google.com/youtube/answer/1722171?hl=en#zippy=%2Cbitrate
var bitrate: Int {
switch self {
// 8K UHD (16:9) Resolutions
case .hd4320p60:
return 180_000_000 // Midpoint between 120 Mbps and 240 Mbps
case .hd4320p30:
return 120_000_000 // Midpoint between 80 Mbps and 160 Mbps
// 4K UHD (16:9) Resolutions
case .hd2160p60:
return 60_500_000 // Midpoint between 53 Mbps and 68 Mbps
case .hd2160p30:
return 40_000_000 // Midpoint between 35 Mbps and 45 Mbps
// 1440p (2K) Resolutions
case .hd1440p60:
return 24_000_000 // 24 Mbps
case .hd1440p30:
return 16_000_000 // 16 Mbps
// 1080p (Full HD, 16:9) Resolutions
case .hd1080p60:
return 12_000_000 // 12 Mbps
case .hd1080p30:
return 8_000_000 // 8 Mbps
// 720p (HD, 16:9) Resolutions
case .hd720p60:
return 7_500_000 // 7.5 Mbps
case .hd720p30:
return 5_000_000 // 5 Mbps
// Standard Definition (SD) Resolutions
case .sd480p30:
return 2_500_000 // 2.5 Mbps
case .sd360p30:
return 1_000_000 // 1 Mbps
case .sd240p30:
return 1_000_000 // 1 Mbps
case .sd144p30:
return 600_000 // 0.6 Mbps
}
}
}

View File

@ -5,27 +5,10 @@ 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) {
let retries = (retryCounts[url] ?? 0) + 1
if retries >= maxRetries {
DispatchQueue.main.async { DispatchQueue.main.async {
self.unloadable.insert(url) 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
}
}
} }
} }

View File

@ -1,17 +1,15 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "Invidious_512x512@1x.png", "filename" : "Invidious.svg",
"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"
} }

View File

@ -0,0 +1,2 @@
<?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>

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

View File

@ -1,38 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.110",
"green" : "0.110",
"red" : "0.118"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -39,38 +39,6 @@ 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 detailsVisibility: Bool {
#if os(iOS)
false
#else
true
#endif
}
static var progressViewScale: Double { static var progressViewScale: Double {
#if os(macOS) #if os(macOS)
0.4 0.4
@ -103,11 +71,11 @@ enum Constants {
#endif #endif
} }
static var contentViewMinWidth: Double { static var detailsVisibility: Bool {
#if os(macOS) #if os(iOS)
835 false
#else #else
0 true
#endif #endif
} }

View File

@ -15,20 +15,19 @@ extension Defaults.Keys {
static let favorites = Key<[FavoriteItem]>("favorites", default: []) static let favorites = Key<[FavoriteItem]>("favorites", default: [])
static let widgetsSettings = Key<[WidgetSettings]>("widgetsSettings", default: []) static let widgetsSettings = Key<[WidgetSettings]>("widgetsSettings", default: [])
static let startupSection = Key<StartupSection>("startupSection", default: .home) static let startupSection = Key<StartupSection>("startupSection", default: .home)
static let showSearchSuggestions = Key<Bool>("showSearchSuggestions", default: true)
static let visibleSections = Key<Set<VisibleSection>>("visibleSections", default: [.subscriptions, .trending, .playlists]) static let visibleSections = Key<Set<VisibleSection>>("visibleSections", default: [.subscriptions, .trending, .playlists])
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: Constants.isIPhone) static let lockPortraitWhenBrowsing = Key<Bool>("lockPortraitWhenBrowsing", default: UIDevice.current.userInterfaceIdiom == .phone)
#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 = Constants.isIPad static let accountPickerDisplaysUsernameDefault = UIDevice.current.userInterfaceIdiom == .pad
#endif #endif
static let accountPickerDisplaysUsername = Key<Bool>("accountPickerDisplaysUsername", default: accountPickerDisplaysUsernameDefault) static let accountPickerDisplaysUsername = Key<Bool>("accountPickerDisplaysUsername", default: accountPickerDisplaysUsernameDefault)
#endif #endif
@ -42,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: .togglePlayerVisibility) static let playerButtonDoubleTapGesture = Key<PlayerTapGestureAction>("playerButtonDoubleTapGesture", default: .nothing)
static let playerButtonShowsControlButtonsWhenMinimized = Key<Bool>("playerButtonShowsControlButtonsWhenMinimized", default: true) static let playerButtonShowsControlButtonsWhenMinimized = Key<Bool>("playerButtonShowsControlButtonsWhenMinimized", default: false)
static let playerButtonIsExpanded = Key<Bool>("playerButtonIsExpanded", default: true) static let playerButtonIsExpanded = Key<Bool>("playerButtonIsExpanded", default: false)
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)
@ -65,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: false) static let pauseOnEnteringBackground = Key<Bool>("pauseOnEnteringBackground", default: true)
#endif #endif
#if os(iOS) #if os(iOS)
@ -80,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: false) static let showChapterThumbnailsOnlyWhenDifferent = Key<Bool>("showChapterThumbnailsOnlyWhenDifferent", default: true)
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,9 +93,12 @@ extension Defaults.Keys {
static let enableReturnYouTubeDislike = Key<Bool>("enableReturnYouTubeDislike", default: false) static let enableReturnYouTubeDislike = Key<Bool>("enableReturnYouTubeDislike", default: false)
#if os(iOS) #if os(iOS)
static let isOrientationLocked = Key<Bool>("isOrientationLocked", default: Constants.isIPhone) static let honorSystemOrientationLock = Key<Bool>("honorSystemOrientationLock", default: true)
static let enterFullscreenInLandscape = Key<Bool>("enterFullscreenInLandscape", default: Constants.isIPhone) static let enterFullscreenInLandscape = Key<Bool>("enterFullscreenInLandscape", default: UIDevice.current.userInterfaceIdiom == .phone)
static let rotateToLandscapeOnEnterFullScreen = Key<FullScreenRotationSetting>("rotateToLandscapeOnEnterFullScreen", default: .landscapeRight) static let rotateToLandscapeOnEnterFullScreen = Key<FullScreenRotationSetting>(
"rotateToLandscapeOnEnterFullScreen",
default: UIDevice.current.userInterfaceIdiom == .phone ? .landscapeRight : .disabled
)
#endif #endif
static let closePiPOnNavigation = Key<Bool>("closePiPOnNavigation", default: false) static let closePiPOnNavigation = Key<Bool>("closePiPOnNavigation", default: false)
@ -114,15 +116,14 @@ extension Defaults.Keys {
// MARK: GROUP - Controls // MARK: GROUP - Controls
static let avPlayerUsesSystemControls = Key<Bool>("avPlayerUsesSystemControls", default: Constants.isTvOS) static let avPlayerUsesSystemControls = Key<Bool>("avPlayerUsesSystemControls", default: true)
static let horizontalPlayerGestureEnabled = Key<Bool>("horizontalPlayerGestureEnabled", default: true) static let horizontalPlayerGestureEnabled = Key<Bool>("horizontalPlayerGestureEnabled", default: true)
static let fullscreenPlayerGestureEnabled = Key<Bool>("fullscreenPlayerGestureEnabled", 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 = Constants.isIPad ? PlayerControlsLayout.medium : .small static let playerControlsLayoutDefault = UIDevice.current.userInterfaceIdiom == .pad ? PlayerControlsLayout.medium : .small
static let fullScreenPlayerControlsLayoutDefault = Constants.isIPad ? PlayerControlsLayout.medium : .small static let fullScreenPlayerControlsLayoutDefault = UIDevice.current.userInterfaceIdiom == .pad ? 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
@ -133,7 +134,6 @@ extension Defaults.Keys {
static let playerControlsLayout = Key<PlayerControlsLayout>("playerControlsLayout", default: playerControlsLayoutDefault) static let playerControlsLayout = Key<PlayerControlsLayout>("playerControlsLayout", default: playerControlsLayoutDefault)
static let fullScreenPlayerControlsLayout = Key<PlayerControlsLayout>("fullScreenPlayerControlsLayout", default: fullScreenPlayerControlsLayoutDefault) static let fullScreenPlayerControlsLayout = Key<PlayerControlsLayout>("fullScreenPlayerControlsLayout", default: fullScreenPlayerControlsLayoutDefault)
static let playerControlsBackgroundOpacity = Key<Double>("playerControlsBackgroundOpacity", default: 0.2)
static let systemControlsCommands = Key<SystemControlsCommands>("systemControlsCommands", default: .restartAndAdvanceToNext) static let systemControlsCommands = Key<SystemControlsCommands>("systemControlsCommands", default: .restartAndAdvanceToNext)
@ -175,152 +175,61 @@ extension Defaults.Keys {
// MARK: GROUP - Quality // MARK: GROUP - Quality
static let hd2160p60MPVProfile = QualityProfile(id: "hd2160p60MPVProfile", backend: .mpv, resolution: .hd2160p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices)) static let hd2160pMPVProfile = QualityProfile(id: "hd2160pMPVProfile", backend: .mpv, resolution: .hd2160p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
static let hd1080p60MPVProfile = QualityProfile(id: "hd1080p60MPVProfile", backend: .mpv, resolution: .hd1080p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices)) static let hd1080pMPVProfile = QualityProfile(id: "hd1080pMPVProfile", backend: .mpv, resolution: .hd1080p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
static let hd1080pMPVProfile = QualityProfile(id: "hd1080pMPVProfile", backend: .mpv, resolution: .hd1080p30, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices)) static let hd720pMPVProfile = QualityProfile(id: "hd720pMPVProfile", backend: .mpv, resolution: .hd720p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
static let hd720p60MPVProfile = QualityProfile(id: "hd720p60MPVProfile", backend: .mpv, resolution: .hd720p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices)) static let hd720pAVPlayerProfile = QualityProfile(id: "hd720pAVPlayerProfile", backend: .appleAVPlayer, resolution: .hd720p30, 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 sd360pAVPlayerProfile = QualityProfile(id: "sd360pAVPlayerProfile", backend: .appleAVPlayer, resolution: .sd360p30, formats: [.hls, .stream], 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)
enum QualityProfiles { static let qualityProfilesDefault = UIDevice.current.userInterfaceIdiom == .pad ? [
// iPad-specific settings hd2160pMPVProfile,
enum iPad {
static let qualityProfilesDefault = [
hd1080p60MPVProfile,
hd1080pMPVProfile, hd1080pMPVProfile,
hd720p60MPVProfile,
hd720pMPVProfile
]
static let batteryCellularProfileDefault = hd720pMPVProfile.id
static let batteryNonCellularProfileDefault = hd720p60MPVProfile.id
static let chargingCellularProfileDefault = hd1080pMPVProfile.id
static let chargingNonCellularProfileDefault = hd1080p60MPVProfile.id
}
// iPhone-specific settings
enum iPhone {
static let qualityProfilesDefault = [
hd1080p60MPVProfile,
hd1080pMPVProfile,
hd720p60MPVProfile,
hd720pMPVProfile, hd720pMPVProfile,
sd360pMPVProfile hd720pAVPlayerProfile,
sd360pAVPlayerProfile
] : [
hd1080pMPVProfile,
hd720pMPVProfile,
hd720pAVPlayerProfile,
sd360pAVPlayerProfile
] ]
static let batteryCellularProfileDefault = sd360pMPVProfile.id static let batteryCellularProfileDefault = hd720pAVPlayerProfile.id
static let batteryNonCellularProfileDefault = hd720p60MPVProfile.id static let batteryNonCellularProfileDefault = hd720pAVPlayerProfile.id
static let chargingCellularProfileDefault = hd720pMPVProfile.id static let chargingCellularProfileDefault = hd720pAVPlayerProfile.id
static let chargingNonCellularProfileDefault = hd1080p60MPVProfile.id static let chargingNonCellularProfileDefault = hd1080pMPVProfile.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
)
}
}
#elseif os(tvOS) #elseif os(tvOS)
enum QualityProfiles {
// tvOS-specific settings
enum tvOS {
static let qualityProfilesDefault = [ static let qualityProfilesDefault = [
hd2160p60MPVProfile, hd2160pMPVProfile,
hd1080p60MPVProfile, hd1080pMPVProfile,
hd720p60MPVProfile, hd720pMPVProfile,
hd720pAVPlayerProfile hd720pAVPlayerProfile
] ]
static let batteryCellularProfileDefault = hd1080pMPVProfile.id
static let batteryCellularProfileDefault = hd1080p60MPVProfile.id static let batteryNonCellularProfileDefault = hd1080pMPVProfile.id
static let batteryNonCellularProfileDefault = hd1080p60MPVProfile.id static let chargingCellularProfileDefault = hd1080pMPVProfile.id
static let chargingCellularProfileDefault = hd1080p60MPVProfile.id static let chargingNonCellularProfileDefault = hd1080pMPVProfile.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
enum QualityProfiles {
// macOS-specific settings
enum macOS {
static let qualityProfilesDefault = [ static let qualityProfilesDefault = [
hd2160p60MPVProfile, hd2160pMPVProfile,
hd1080p60MPVProfile,
hd1080pMPVProfile, hd1080pMPVProfile,
hd720p60MPVProfile hd720pMPVProfile,
hd720pAVPlayerProfile
] ]
static let batteryCellularProfileDefault = hd1080pMPVProfile.id
static let batteryCellularProfileDefault = hd1080p60MPVProfile.id static let batteryNonCellularProfileDefault = hd1080pMPVProfile.id
static let batteryNonCellularProfileDefault = hd1080p60MPVProfile.id static let chargingCellularProfileDefault = hd1080pMPVProfile.id
static let chargingCellularProfileDefault = hd1080p60MPVProfile.id static let chargingNonCellularProfileDefault = hd1080pMPVProfile.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>( static let batteryCellularProfile = Key<QualityProfile.ID>("batteryCellularProfile", default: batteryCellularProfileDefault)
"batteryCellularProfile", static let batteryNonCellularProfile = Key<QualityProfile.ID>("batteryNonCellularProfile", default: batteryNonCellularProfileDefault)
default: QualityProfiles.currentProfile.batteryCellularProfileDefault static let chargingCellularProfile = Key<QualityProfile.ID>("chargingCellularProfile", default: chargingCellularProfileDefault)
) static let chargingNonCellularProfile = Key<QualityProfile.ID>("chargingNonCellularProfile", default: chargingNonCellularProfileDefault)
static let batteryNonCellularProfile = Key<QualityProfile.ID>( static let forceAVPlayerForLiveStreams = Key<Bool>("forceAVPlayerForLiveStreams", default: true)
"batteryNonCellularProfile",
default: QualityProfiles.currentProfile.batteryNonCellularProfileDefault static let qualityProfiles = Key<[QualityProfile]>("qualityProfiles", default: qualityProfilesDefault)
)
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
@ -360,7 +269,6 @@ extension Defaults.Keys {
// MARK: Group - Advanced // MARK: Group - Advanced
static let showPlayNowInBackendContextMenu = Key<Bool>("showPlayNowInBackendContextMenu", default: false) static let showPlayNowInBackendContextMenu = Key<Bool>("showPlayNowInBackendContextMenu", default: false)
static let videoLoadingRetryCount = Key<Int>("videoLoadingRetryCount", default: 10)
static let showMPVPlaybackStats = Key<Bool>("showMPVPlaybackStats", default: false) static let showMPVPlaybackStats = Key<Bool>("showMPVPlaybackStats", default: false)
static let mpvEnableLogging = Key<Bool>("mpvEnableLogging", default: false) static let mpvEnableLogging = Key<Bool>("mpvEnableLogging", default: false)
@ -371,7 +279,6 @@ extension Defaults.Keys {
static let mpvHWdec = Key<String>("hwdec", default: "auto-safe") static let mpvHWdec = Key<String>("hwdec", default: "auto-safe")
static let mpvDemuxerLavfProbeInfo = Key<String>("mpvDemuxerLavfProbeInfo", default: "no") static let mpvDemuxerLavfProbeInfo = Key<String>("mpvDemuxerLavfProbeInfo", default: "no")
static let mpvInitialAudioSync = Key<Bool>("mpvInitialAudioSync", default: true) static let mpvInitialAudioSync = Key<Bool>("mpvInitialAudioSync", default: true)
static let mpvSetRefreshToContentFPS = Key<Bool>("mpvSetRefreshToContentFPS", default: false)
static let showCacheStatus = Key<Bool>("showCacheStatus", default: false) static let showCacheStatus = Key<Bool>("showCacheStatus", default: false)
static let feedCacheSize = Key<String>("feedCacheSize", default: "50") static let feedCacheSize = Key<String>("feedCacheSize", default: "50")
@ -428,34 +335,18 @@ enum ResolutionSetting: String, CaseIterable, Defaults.Serializable {
case sd240p30 case sd240p30
case sd144p30 case sd144p30
var value: Stream.Resolution { var value: Stream.Resolution! {
if let predefined = Stream.Resolution.PredefinedResolution(rawValue: rawValue) { .init(rawValue: rawValue)
return .predefined(predefined)
}
// Provide a default value of 720p 30
return .custom(height: 720, refreshRate: 30)
} }
var description: String { var description: String {
let resolution = value switch self {
let height = resolution.height case .hd2160p60:
let refreshRate = resolution.refreshRate return "4K, 60fps"
case .hd2160p30:
// Superscript labels return "4K"
let superscript4K = "⁴ᴷ"
let superscriptHD = "ᴴᴰ"
// Special handling for specific resolutions
switch height {
case 2160:
// 4K superscript after the refresh rate
return refreshRate == 30 ? "2160p \(superscript4K)" : "2160p\(refreshRate) \(superscript4K)"
case 1440, 1080:
// HD superscript after the refresh rate
return refreshRate == 30 ? "\(height)p \(superscriptHD)" : "\(height)p\(refreshRate) \(superscriptHD)"
default: default:
// Default formatting for other resolutions return value.name
return refreshRate == 30 ? "\(height)p" : "\(height)p\(refreshRate)"
} }
} }
} }
@ -630,19 +521,26 @@ enum PlayerTapGestureAction: String, CaseIterable, Defaults.Serializable {
} }
enum FullScreenRotationSetting: String, CaseIterable, Defaults.Serializable { enum FullScreenRotationSetting: String, CaseIterable, Defaults.Serializable {
case disabled
case landscapeLeft case landscapeLeft
case landscapeRight case landscapeRight
#if os(iOS) #if os(iOS)
var interfaceOrientation: UIInterfaceOrientation { var interaceOrientation: UIInterfaceOrientation {
switch self { switch self {
case .landscapeLeft: case .landscapeLeft:
return .landscapeLeft return .landscapeLeft
case .landscapeRight: case .landscapeRight:
return .landscapeRight return .landscapeRight
default:
return .portrait
} }
} }
#endif #endif
var isRotating: Bool {
self != .disabled
}
} }
struct WidgetSettings: Defaults.Serializable { struct WidgetSettings: Defaults.Serializable {

View File

@ -152,7 +152,7 @@ struct HomeView: View {
#endif #endif
#if os(macOS) #if os(macOS)
.background(Color.secondaryBackground) .background(Color.secondaryBackground)
.frame(minWidth: Constants.contentViewMinWidth) .frame(minWidth: 360)
.toolbar { .toolbar {
ToolbarItemGroup(placement: .automatic) { ToolbarItemGroup(placement: .automatic) {
HideWatchedButtons() HideWatchedButtons()

View File

@ -15,7 +15,7 @@ struct AppSidebarNavigation: View {
var body: some View { var body: some View {
#if os(iOS) #if os(iOS)
content.introspect(.viewController, on: .iOS(.v15, .v16, .v17, .v18)) { viewController in content.introspect(.viewController, on: .iOS(.v15, .v16, .v17)) { viewController in
// workaround for an empty supplementary view on launch // workaround for an empty supplementary view on launch
// the supplementary view is determined by the default selection inside the // the supplementary view is determined by the default selection inside the
// primary view, but the primary view is not loaded so its selection is not read // primary view, but the primary view is not loaded so its selection is not read

View File

@ -169,7 +169,7 @@ struct ContentView: View {
.statusBarHidden(player.playingFullScreen) .statusBarHidden(player.playingFullScreen)
#endif #endif
#if os(macOS) #if os(macOS)
.frame(minWidth: 1200, minHeight: 600) .frame(minWidth: 1200)
#endif #endif
} }

View File

@ -4,6 +4,11 @@ import SwiftUI
#if !os(macOS) #if !os(macOS)
final class AppleAVPlayerViewControllerDelegate: NSObject, AVPlayerViewControllerDelegate { final class AppleAVPlayerViewControllerDelegate: NSObject, AVPlayerViewControllerDelegate {
#if os(iOS)
@Default(.rotateToLandscapeOnEnterFullScreen) private var rotateToLandscapeOnEnterFullScreen
@Default(.avPlayerUsesSystemControls) private var avPlayerUsesSystemControls
#endif
var player: PlayerModel { .shared } var player: PlayerModel { .shared }
func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_: AVPlayerViewController) -> Bool { func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_: AVPlayerViewController) -> Bool {
@ -12,23 +17,15 @@ import SwiftUI
#if os(iOS) #if os(iOS)
func playerViewController(_: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator) { func playerViewController(_: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator) {
let lockOrientation = player.rotateToLandscapeOnEnterFullScreen.interfaceOrientation guard rotateToLandscapeOnEnterFullScreen.isRotating else { return }
if player.currentVideoIsLandscape { if PlayerModel.shared.currentVideoIsLandscape {
if player.fullscreenInitiatedByButton { let delay = PlayerModel.shared.activeBackend == .appleAVPlayer && avPlayerUsesSystemControls ? 0.8 : 0
Orientation.lockOrientation(player.isOrientationLocked // not sure why but first rotation call is ignore so doing rotate to same orientation first
? (lockOrientation == .landscapeRight ? .landscapeRight : .landscapeLeft) Delay.by(delay) {
: .landscape) let orientation = OrientationTracker.shared.currentDeviceOrientation.isLandscape ? OrientationTracker.shared.currentInterfaceOrientation : self.rotateToLandscapeOnEnterFullScreen.interaceOrientation
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: OrientationTracker.shared.currentInterfaceOrientation)
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: orientation)
} }
let orientation = OrientationTracker.shared.currentDeviceOrientation.isLandscape
? OrientationTracker.shared.currentInterfaceOrientation
: player.rotateToLandscapeOnEnterFullScreen.interfaceOrientation
Orientation.lockOrientation(
player.isOrientationLocked
? (lockOrientation == .landscapeRight ? .landscapeRight : .landscapeLeft)
: .all,
andRotateTo: orientation
)
} }
} }
@ -40,11 +37,11 @@ import SwiftUI
} }
if !context.isCancelled { if !context.isCancelled {
#if os(iOS) #if os(iOS)
if self.player.lockPortraitWhenBrowsing { self.player.lockedOrientation = nil
self.player.lockedOrientation = UIInterfaceOrientationMask.portrait
if Constants.isIPhone {
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
} }
let rotationOrientation = self.player.lockPortraitWhenBrowsing ? UIInterfaceOrientation.portrait : nil
Orientation.lockOrientation(self.player.lockPortraitWhenBrowsing ? .portrait : .all, andRotateTo: rotationOrientation)
if wasPlaying { if wasPlaying {
self.player.play() self.player.play()

View File

@ -5,8 +5,6 @@ struct ControlsOverlay: View {
@ObservedObject private var player = PlayerModel.shared @ObservedObject private var player = PlayerModel.shared
private var model = PlayerControlsModel.shared private var model = PlayerControlsModel.shared
@State private var availableCaptions: [Captions] = []
@State private var isLoadingCaptions = true
@State private var contentSize: CGSize = .zero @State private var contentSize: CGSize = .zero
@Default(.showMPVPlaybackStats) private var showMPVPlaybackStats @Default(.showMPVPlaybackStats) private var showMPVPlaybackStats
@ -337,6 +335,7 @@ struct ControlsOverlay: View {
Image(systemName: "text.bubble") Image(systemName: "text.bubble")
if let captions = captionsBinding.wrappedValue, if let captions = captionsBinding.wrappedValue,
let language = LanguageCodes(rawValue: captions.code) let language = LanguageCodes(rawValue: captions.code)
{ {
Text("\(language.description.capitalized) (\(language.rawValue))") Text("\(language.description.capitalized) (\(language.rawValue))")
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
@ -381,16 +380,17 @@ struct ControlsOverlay: View {
.contextMenu { .contextMenu {
Button("Disabled") { captionsBinding.wrappedValue = nil } Button("Disabled") { captionsBinding.wrappedValue = nil }
ForEach(availableCaptions) { caption in ForEach(player.currentVideo?.captions ?? []) { caption in
Button(caption.description) { captionsBinding.wrappedValue = caption } Button(caption.description) { captionsBinding.wrappedValue = caption }
} }
Button("Cancel", role: .cancel) {} Button("Cancel", role: .cancel) {}
} }
#endif #endif
} }
@ViewBuilder private var captionsPicker: some View { @ViewBuilder private var captionsPicker: some View {
let captions = availableCaptions let captions = player.currentVideo?.captions ?? []
Picker("Captions", selection: captionsBinding) { Picker("Captions", selection: captionsBinding) {
if captions.isEmpty { if captions.isEmpty {
Text("Not available").tag(Captions?.none) Text("Not available").tag(Captions?.none)
@ -402,31 +402,6 @@ struct ControlsOverlay: View {
} }
} }
.disabled(captions.isEmpty) .disabled(captions.isEmpty)
.onAppear {
loadCaptions()
}
}
private func loadCaptions() {
isLoadingCaptions = true
// Fetch captions asynchronously
Task {
let fetchedCaptions = await fetchCaptions()
await MainActor.run {
// Update state on the main thread
self.availableCaptions = fetchedCaptions
self.isLoadingCaptions = false
}
}
}
private func fetchCaptions() async -> [Captions] {
// Access currentVideo from the main actor context
await MainActor.run {
// Safely access the main actor-isolated currentVideo property
player.currentVideo?.captions ?? []
}
} }
private var captionsBinding: Binding<Captions?> { private var captionsBinding: Binding<Captions?> {

View File

@ -29,7 +29,6 @@ struct PlayerControls: View {
@Default(.playerControlsLayout) private var regularPlayerControlsLayout @Default(.playerControlsLayout) private var regularPlayerControlsLayout
@Default(.fullScreenPlayerControlsLayout) private var fullScreenPlayerControlsLayout @Default(.fullScreenPlayerControlsLayout) private var fullScreenPlayerControlsLayout
@Default(.playerControlsBackgroundOpacity) private var playerControlsBackgroundOpacity
@Default(.buttonBackwardSeekDuration) private var buttonBackwardSeekDuration @Default(.buttonBackwardSeekDuration) private var buttonBackwardSeekDuration
@Default(.buttonForwardSeekDuration) private var buttonForwardSeekDuration @Default(.buttonForwardSeekDuration) private var buttonForwardSeekDuration
@ -249,34 +248,29 @@ struct PlayerControls: View {
return [player.playerSize.height - inset, 500].min()! return [player.playerSize.height - inset, 500].min()!
} }
@ViewBuilder @ViewBuilder var controlsBackground: some View {
var controlsBackground: some View {
GeometryReader { geometry in
ZStack { ZStack {
if player.musicMode, if player.musicMode,
let video = player.videoForDisplay let url = controlsBackgroundURL
{ {
let thumbnail = thumbnails.best(video)
if let url = thumbnail.url,
let quality = thumbnail.quality
{
let aspectRatio = (quality == .default || quality == .high) ? Constants.aspectRatio4x3 : Constants.aspectRatio16x9
ThumbnailView(url: url) ThumbnailView(url: url)
.aspectRatio(aspectRatio, contentMode: .fill) .frame(maxWidth: .infinity, maxHeight: .infinity)
.frame(width: geometry.size.width, height: geometry.size.height)
.transition(.opacity) .transition(.opacity)
.animation(.default) .animation(.default)
.clipped()
}
} else if player.videoForDisplay == nil { } else if player.videoForDisplay == nil {
Color.black Color.black
} else if model.presentingControls {
Color.black.opacity(playerControlsBackgroundOpacity)
.edgesIgnoringSafeArea(.all)
} }
} }
} }
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 {
@ -387,13 +381,13 @@ struct PlayerControls: View {
} }
private var pipButton: some View { private var pipButton: some View {
button("PiP", systemImage: player.pipImage, active: player.playingInPictureInPicture, action: player.togglePiPAction) button("PiP", systemImage: player.pipImage, action: player.togglePiPAction)
.disabled(!player.pipPossible) .disabled(!player.pipPossible)
} }
#if os(iOS) #if os(iOS)
private var lockOrientationButton: some View { private var lockOrientationButton: some View {
button("Lock Rotation", systemImage: player.lockOrientationImage, active: player.isOrientationLocked, action: player.lockOrientationAction) button("Lock Rotation", systemImage: player.lockOrientationImage, active: !player.lockedOrientation.isNil, action: player.lockOrientationAction)
} }
#endif #endif

View File

@ -6,12 +6,10 @@ import OpenGLES
final class MPVOGLView: GLKView { final class MPVOGLView: GLKView {
private var logger = Logger(label: "stream.yattee.mpv.oglview") private var logger = Logger(label: "stream.yattee.mpv.oglview")
private var defaultFBO: GLint? private var defaultFBO: GLint?
private var displayLink: CADisplayLink?
var mpvGL: UnsafeMutableRawPointer? var mpvGL: UnsafeMutableRawPointer?
var queue = DispatchQueue(label: "stream.yattee.opengl", qos: .userInteractive) var queue = DispatchQueue(label: "stream.yattee.opengl")
var needsDrawing = true var needsDrawing = true
private var dirtyRegion: CGRect?
override init(frame: CGRect) { override init(frame: CGRect) {
guard let context = EAGLContext(api: .openGLES2) else { guard let context = EAGLContext(api: .openGLES2) else {
@ -31,70 +29,6 @@ final class MPVOGLView: GLKView {
enableSetNeedsDisplay = false enableSetNeedsDisplay = false
fillBlack() fillBlack()
setupDisplayLink()
setupNotifications()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupDisplayLink()
setupNotifications()
}
private func setupDisplayLink() {
displayLink = CADisplayLink(target: self, selector: #selector(updateFrame))
displayLink?.add(to: .main, forMode: .common)
}
// Set up observers to detect display changes and custom refresh rate updates.
private func setupNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(updateDisplayLinkFromNotification(_:)), name: .updateDisplayLinkFrameRate, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(screenDidChange), name: UIScreen.didConnectNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(screenDidChange), name: UIScreen.didDisconnectNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(screenDidChange), name: UIScreen.modeDidChangeNotification, object: nil)
}
@objc private func screenDidChange(_: Notification) {
// Update the display link refresh rate when the screen configuration changes
updateDisplayLinkFrameRate()
}
// Update the display link frame rate from the notification.
@objc private func updateDisplayLinkFromNotification(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let refreshRate = userInfo["refreshRate"] as? Int else { return }
displayLink?.preferredFramesPerSecond = refreshRate
logger.info("Updated CADisplayLink frame rate to: \(refreshRate) from backend notification.")
}
// Update the display link's preferred frame rate based on the current screen refresh rate.
private func updateDisplayLinkFrameRate() {
guard let displayLink else { return }
let refreshRate = getScreenRefreshRate()
displayLink.preferredFramesPerSecond = refreshRate
logger.info("Updated CADisplayLink preferred frames per second to: \(refreshRate)")
}
// Retrieve the screen's current refresh rate dynamically.
private func getScreenRefreshRate() -> Int {
// Use the main screen's maximumFramesPerSecond property
let refreshRate = UIScreen.main.maximumFramesPerSecond
logger.info("Screen refresh rate: \(refreshRate) Hz")
return refreshRate
}
@objc private func updateFrame() {
// Trigger the drawing process if needed
if needsDrawing {
markRegionAsDirty(bounds)
setNeedsDisplay()
}
}
deinit {
// Invalidate the display link and remove observers to avoid memory leaks
displayLink?.invalidate()
NotificationCenter.default.removeObserver(self)
} }
func fillBlack() { func fillBlack() {
@ -102,99 +36,36 @@ final class MPVOGLView: GLKView {
glClear(UInt32(GL_COLOR_BUFFER_BIT)) glClear(UInt32(GL_COLOR_BUFFER_BIT))
} }
// Function to set a dirty region when a part of the screen changes
func markRegionAsDirty(_ region: CGRect) {
if dirtyRegion == nil {
dirtyRegion = region
} else {
// Expand the dirty region to include the new region
dirtyRegion = dirtyRegion!.union(region)
}
}
// Logic to decide if only part of the screen needs updating
private func needsPartialUpdate() -> Bool {
// Check if there is a defined dirty region that needs updating
if let dirtyRegion, !dirtyRegion.isEmpty {
// Set up glScissor based on dirtyRegion coordinates
glScissor(GLint(dirtyRegion.origin.x), GLint(dirtyRegion.origin.y), GLsizei(dirtyRegion.width), GLsizei(dirtyRegion.height))
return true
}
return false
}
// Call this function when you know the entire screen needs updating
private func clearDirtyRegion() {
dirtyRegion = nil
}
override func draw(_: CGRect) { override func draw(_: CGRect) {
guard needsDrawing, let mpvGL else { return } guard needsDrawing, let mpvGL else {
// Ensure the correct context is set
guard EAGLContext.setCurrent(context) else {
logger.error("Failed to set current OpenGL context.")
return return
} }
// Bind the default framebuffer
glGetIntegerv(UInt32(GL_FRAMEBUFFER_BINDING), &defaultFBO!) glGetIntegerv(UInt32(GL_FRAMEBUFFER_BINDING), &defaultFBO!)
// Ensure the framebuffer is valid
guard defaultFBO != nil && defaultFBO! != 0 else {
logger.error("Invalid framebuffer ID.")
return
}
// Get the current viewport dimensions
var dims: [GLint] = [0, 0, 0, 0] var dims: [GLint] = [0, 0, 0, 0]
glGetIntegerv(GLenum(GL_VIEWPORT), &dims) glGetIntegerv(GLenum(GL_VIEWPORT), &dims)
// Check if we need partial updates
if needsPartialUpdate() {
logger.info("Performing partial update with scissor test.")
glEnable(GLenum(GL_SCISSOR_TEST))
}
// Set up the OpenGL FBO data
var data = mpv_opengl_fbo( var data = mpv_opengl_fbo(
fbo: Int32(defaultFBO!), fbo: Int32(defaultFBO!),
w: Int32(dims[2]), w: Int32(dims[2]),
h: Int32(dims[3]), h: Int32(dims[3]),
internal_format: 0 internal_format: 0
) )
// Flip Y coordinate for proper rendering
var flip: CInt = 1 var flip: CInt = 1
withUnsafeMutablePointer(to: &flip) { flip in
// Render with the provided OpenGL FBO parameters withUnsafeMutablePointer(to: &data) { data in
withUnsafeMutablePointer(to: &flip) { flipPtr in
withUnsafeMutablePointer(to: &data) { dataPtr in
var params = [ var params = [
mpv_render_param(type: MPV_RENDER_PARAM_OPENGL_FBO, data: dataPtr), mpv_render_param(type: MPV_RENDER_PARAM_OPENGL_FBO, data: data),
mpv_render_param(type: MPV_RENDER_PARAM_FLIP_Y, data: flipPtr), mpv_render_param(type: MPV_RENDER_PARAM_FLIP_Y, data: flip),
mpv_render_param() mpv_render_param()
] ]
// Call the render function and check for errors mpv_render_context_render(OpaquePointer(mpvGL), &params)
let result = mpv_render_context_render(OpaquePointer(mpvGL), &params)
if result < 0 {
logger.error("mpv_render_context_render() failed with error code: \(result)")
} else {
logger.info("mpv_render_context_render() called successfully.")
} }
} }
} }
// Disable the scissor test after rendering if it was enabled required init?(coder aDecoder: NSCoder) {
if needsPartialUpdate() { super.init(coder: aDecoder)
glDisable(GLenum(GL_SCISSOR_TEST))
}
// Clear dirty region after drawing
clearDirtyRegion()
} }
} }
extension Notification.Name {
static let updateDisplayLinkFrameRate = Notification.Name("updateDisplayLinkFrameRate")
}

View File

@ -43,7 +43,7 @@ struct PlayerBackendView: View {
Color.clear Color.clear
.onAppear { player.playerSize = proxy.size } .onAppear { player.playerSize = proxy.size }
.onChange(of: proxy.size) { _ in player.playerSize = proxy.size } .onChange(of: proxy.size) { _ in player.playerSize = proxy.size }
.onChange(of: player.currentItem?.id) { _ in player.playerSize = proxy.size } .onChange(of: player.controls.presentingOverlays) { _ in player.playerSize = proxy.size }
}) })
#if !os(tvOS) #if !os(tvOS)

View File

@ -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(-translation.height, translation.height) translation.height = max(0, translation.height)
state = translation state = translation
} }
#endif #endif
@ -18,8 +18,7 @@ extension VideoPlayerView {
.onChanged { value in .onChanged { value in
guard player.presentingPlayer, guard player.presentingPlayer,
!controlsOverlayModel.presenting, !controlsOverlayModel.presenting,
dragGestureState, dragGestureState else { return }
!disableToggleGesture else { return }
if player.controls.presentingControls, !player.musicMode { if player.controls.presentingControls, !player.musicMode {
player.controls.presentingControls = false player.controls.presentingControls = false
@ -56,83 +55,47 @@ extension VideoPlayerView {
player.seek.gestureStart = time player.seek.gestureStart = time
} }
let timeSeek = (time / player.playerSize.width) * horizontalDrag * seekGestureSpeed let timeSeek = (time / player.playerSize.width) * horizontalDrag * seekGestureSpeed
player.seek.gestureSeek = timeSeek player.seek.gestureSeek = timeSeek
} }
return return
} }
// Toggle fullscreen on upward drag only when not disabled guard verticalDrag > 0 else { return }
if fullscreenPlayerGestureEnabled, verticalDrag < -50 {
player.toggleFullScreenAction()
disableGestureTemporarily()
return
}
// Ignore downward swipes when in fullscreen
guard verticalDrag > 0 && !player.playingFullScreen else {
return
}
viewDragOffset = verticalDrag viewDragOffset = verticalDrag
if verticalDrag > 60,
player.playingFullScreen
{
player.exitFullScreen(showControls: false)
#if os(iOS)
if Constants.isIPhone {
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
}
#endif
}
} }
.onEnded { _ in .onEnded { _ in
onPlayerDragGestureEnded() onPlayerDragGestureEnded()
} }
} }
var detailsDragGesture: some Gesture {
DragGesture(minimumDistance: 30)
.onChanged { value in
handleDetailsDragChange(value)
}
.onEnded { value in
handleDetailsDragEnd(value)
}
}
private func handleDetailsDragChange(_ value: DragGesture.Value) {
let maxOffset = -player.playerSize.height
// Continuous drag update for smooth movement of VideoDetails
if fullScreenDetails {
// Allow only downward dragging when in fullscreen
if value.translation.height > 0 {
detailViewDragOffset = min(value.translation.height, abs(maxOffset))
}
} else {
// Allow only upward dragging when not in fullscreen
if value.translation.height < 0 {
detailViewDragOffset = max(value.translation.height, maxOffset)
}
}
}
private func handleDetailsDragEnd(_ value: DragGesture.Value) {
if value.translation.height < -50, !fullScreenDetails {
// Swipe up to enter fullscreen
withAnimation(Constants.overlayAnimation) {
fullScreenDetails = true
detailViewDragOffset = 0
}
} else if value.translation.height > 50, fullScreenDetails {
// Swipe down to exit fullscreen
withAnimation(Constants.overlayAnimation) {
fullScreenDetails = false
detailViewDragOffset = 0
}
} else {
// Reset offset if drag was not significant
withAnimation(Constants.overlayAnimation) {
detailViewDragOffset = 0
}
}
}
func onPlayerDragGestureEnded() { func onPlayerDragGestureEnded() {
if horizontalPlayerGestureEnabled, isHorizontalDrag { if horizontalPlayerGestureEnabled, isHorizontalDrag {
isHorizontalDrag = false isHorizontalDrag = false
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,
@ -154,11 +117,4 @@ extension VideoPlayerView {
} }
} }
} }
private func disableGestureTemporarily() {
disableToggleGesture = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
disableToggleGesture = false
}
}
} }

View File

@ -155,10 +155,10 @@ 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, active: player.playingInPictureInPicture, action: player.togglePiPAction) actionButton("PiP", systemImage: player.pipImage, action: player.togglePiPAction)
#if os(iOS) #if os(iOS)
case .lockOrientation: case .lockOrientation:
actionButton("Lock", systemImage: player.lockOrientationImage, active: player.isOrientationLocked, action: player.lockOrientationAction) actionButton("Lock", systemImage: player.lockOrientationImage, active: player.lockedOrientation != nil, action: player.lockOrientationAction)
#endif #endif
case .restart: case .restart:
actionButton("Replay", systemImage: "backward.end.fill", action: player.replayAction) actionButton("Replay", systemImage: "backward.end.fill", action: player.replayAction)

View File

@ -223,7 +223,7 @@ struct VideoDetails: View {
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle()) .contentShape(Rectangle())
.padding(.horizontal, 16) .padding(.horizontal, 16)
// swiftlint:disable trailing_closure
// TODO: when setting tvOS minimum to 16, the platform modifier can be removed // TODO: when setting tvOS minimum to 16, the platform modifier can be removed
#if !os(tvOS) #if !os(tvOS)
.simultaneousGesture( // Simultaneous gesture to prioritize button tap .simultaneousGesture( // Simultaneous gesture to prioritize button tap
@ -234,7 +234,7 @@ struct VideoDetails: View {
} }
) )
#endif #endif
// swiftlint:enable trailing_closure
if VideoActions().isAnyActionVisible() { if VideoActions().isAnyActionVisible() {
VideoActions(video: player.videoForDisplay) VideoActions(video: player.videoForDisplay)
.padding(.vertical, 5) .padding(.vertical, 5)

View File

@ -24,12 +24,13 @@ struct VideoPlayerView: View {
#if os(macOS) #if os(macOS)
335 335
#else #else
140 200
#endif #endif
} }
@State private var playerSize: CGSize = .zero { didSet { updateSidebarQueue() } } @State private var playerSize: CGSize = .zero { didSet { updateSidebarQueue() } }
@State private var hoveringPlayer = false @State private var hoveringPlayer = false
@State private var fullScreenDetails = false
@State private var sidebarQueue = defaultSidebarQueueValue @State private var sidebarQueue = defaultSidebarQueueValue
@Environment(\.colorScheme) private var colorScheme @Environment(\.colorScheme) private var colorScheme
@ -46,20 +47,11 @@ 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
// swiftlint:disable private_swiftui_state @State var isHorizontalDrag = false // swiftlint:disable:this swiftui_state_private
@State var isHorizontalDrag = false @State var isVerticalDrag = false // swiftlint:disable:this swiftui_state_private
@State var isVerticalDrag = false @State var viewDragOffset = Self.hiddenOffset // swiftlint:disable:this swiftui_state_private
@State var viewDragOffset = Self.hiddenOffset
@State var detailViewDragOffset: Double = 0
// swiftlint:enable private_swiftui_state
#endif #endif
// swiftlint:disable private_swiftui_state
@State var disableToggleGesture = false
@State var fullScreenDetails = 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)
@ -67,7 +59,6 @@ struct VideoPlayerView: View {
#endif #endif
@Default(.horizontalPlayerGestureEnabled) var horizontalPlayerGestureEnabled @Default(.horizontalPlayerGestureEnabled) var horizontalPlayerGestureEnabled
@Default(.fullscreenPlayerGestureEnabled) var fullscreenPlayerGestureEnabled
@Default(.seekGestureSpeed) var seekGestureSpeed @Default(.seekGestureSpeed) var seekGestureSpeed
@Default(.seekGestureSensitivity) var seekGestureSensitivity @Default(.seekGestureSensitivity) var seekGestureSensitivity
@Default(.playerSidebar) var playerSidebar @Default(.playerSidebar) var playerSidebar
@ -113,6 +104,9 @@ struct VideoPlayerView: View {
.onChange(of: geometry.size) { _ in .onChange(of: geometry.size) { _ in
self.playerSize = geometry.size self.playerSize = geometry.size
} }
.onChange(of: fullScreenDetails) { value in
player.backend.setNeedsDrawing(!value)
}
#if os(iOS) #if os(iOS)
.onChange(of: player.presentingPlayer) { newValue in .onChange(of: player.presentingPlayer) { newValue in
if newValue { if newValue {
@ -126,6 +120,19 @@ struct VideoPlayerView: View {
} }
#endif #endif
viewDragOffset = 0 viewDragOffset = 0
Delay.by(0.2) {
orientationModel.configureOrientationUpdatesBasedOnAccelerometer()
if let orientationMask = player.lockedOrientation {
Orientation.lockOrientation(
orientationMask,
andRotateTo: orientationMask == .landscapeLeft ? .landscapeLeft : orientationMask == .landscapeRight ? .landscapeRight : .portrait
)
} else {
Orientation.lockOrientation(.allButUpsideDown)
}
}
} }
.onAnimationCompleted(for: viewDragOffset) { .onAnimationCompleted(for: viewDragOffset) {
guard !dragGestureState else { return } guard !dragGestureState else { return }
@ -299,18 +306,13 @@ struct VideoPlayerView: View {
playerSize: player.playerSize, playerSize: player.playerSize,
fullScreen: fullScreenDetails fullScreen: fullScreenDetails
)) ))
#if os(macOS)
// TODO: Check whether this is needed on macOS.
.onDisappear { .onDisappear {
if player.presentingPlayer { if player.presentingPlayer {
player.setNeedsDrawing(true) player.setNeedsDrawing(true)
} }
} }
#endif
.id(player.currentVideo?.cacheKey) .id(player.currentVideo?.cacheKey)
.transition(.opacity) .transition(.opacity)
.offset(y: detailViewDragOffset)
.gesture(detailsDragGesture)
} else { } else {
VStack {} VStack {}
} }

View File

@ -9,7 +9,7 @@ struct FocusableSearchTextField: View {
var body: some View { var body: some View {
SearchTextField() SearchTextField()
#if os(macOS) #if os(macOS)
.introspect(.textField, on: .macOS(.v12, .v13, .v14, .v15)) { textField in .introspect(.textField, on: .macOS(.v12, .v13, .v14)) { textField in
state.textField = textField state.textField = textField
} }
.onAppear { .onAppear {
@ -18,7 +18,7 @@ struct FocusableSearchTextField: View {
} }
} }
#elseif os(iOS) #elseif os(iOS)
.introspect(.textField, on: .iOS(.v15, .v16, .v17, .v18)) { textField in .introspect(.textField, on: .iOS(.v15, .v16, .v17)) { textField in
state.textField = textField state.textField = textField
} }
.onChange(of: state.focused) { newValue in .onChange(of: state.focused) { newValue in

View File

@ -1,23 +1,25 @@
import Repeat
import SwiftUI import SwiftUI
struct SearchTextField: View { struct SearchTextField: View {
private var navigation = NavigationModel.shared private var navigation = NavigationModel.shared
@ObservedObject private var state = SearchModel.shared @ObservedObject private var state = SearchModel.shared
#if os(macOS)
var body: some View { var body: some View {
ZStack { ZStack {
#if os(macOS)
fieldBorder fieldBorder
#endif
HStack(spacing: 0) { HStack(spacing: 0) {
#if os(macOS)
Image(systemName: "magnifyingglass") Image(systemName: "magnifyingglass")
.resizable() .resizable()
.scaledToFill() .scaledToFill()
.frame(width: 12, height: 12) .frame(width: 12, height: 12)
.padding(.horizontal, 6) .padding(.horizontal, 8)
.opacity(0.8) .opacity(0.8)
#endif
GeometryReader { geometry in
TextField("Search...", text: $state.queryText) { TextField("Search...", text: $state.queryText) {
state.changeQuery { query in state.changeQuery { query in
query.query = state.queryText query.query = state.queryText
@ -26,74 +28,37 @@ struct SearchTextField: View {
RecentsModel.shared.addQuery(state.queryText) RecentsModel.shared.addQuery(state.queryText)
} }
.disableAutocorrection(true) .disableAutocorrection(true)
.frame(maxWidth: geometry.size.width - 5) #if os(macOS)
.frame(maxWidth: 190)
.textFieldStyle(.plain) .textFieldStyle(.plain)
.padding(.vertical, 8) #else
.frame(height: 27, alignment: .center) .frame(minWidth: 200)
} .textFieldStyle(.roundedBorder)
.padding(.horizontal, 5)
.padding(.trailing, state.queryText.isEmpty ? 0 : 10)
#endif
if !state.queryText.isEmpty { if !state.queryText.isEmpty {
clearButton clearButton
} else { } else {
#if os(macOS)
clearButton clearButton
.opacity(0) .opacity(0)
}
}
}
.transaction { t in t.animation = nil }
}
#else
var body: some View {
ZStack {
HStack {
HStack(spacing: 0) {
Image(systemName: "magnifyingglass")
.foregroundColor(.gray)
.padding(.leading, 5)
.padding(.trailing, 5)
.imageScale(.medium)
TextField("Search...", text: $state.queryText) {
state.changeQuery { query in
query.query = state.queryText
navigation.hideKeyboard()
}
RecentsModel.shared.addQuery(state.queryText)
}
.disableAutocorrection(true)
.textFieldStyle(.plain)
.padding(.vertical, 7)
if !state.queryText.isEmpty {
clearButton
.padding(.leading, 5)
.padding(.trailing, 5)
}
}
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color("SearchTextFieldBackground"))
)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color(UIColor.secondarySystemBackground), lineWidth: 1)
)
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(.horizontal, 0)
}
.transaction { t in t.animation = nil }
}
#endif #endif
}
}
}
.transaction { t in t.animation = nil }
}
private var fieldBorder: some View { private var fieldBorder: some View {
RoundedRectangle(cornerRadius: 5, style: .continuous) RoundedRectangle(cornerRadius: 5, style: .continuous)
.fill(Color.background) .fill(Color.background)
.frame(width: 250, height: 27) .frame(width: 250, height: 32)
.overlay( .overlay(
RoundedRectangle(cornerRadius: 5, style: .continuous) RoundedRectangle(cornerRadius: 5, style: .continuous)
.stroke(Color.gray.opacity(0.4), lineWidth: 1) .stroke(Color.gray.opacity(0.4), lineWidth: 1)
.frame(width: 250, height: 27) .frame(width: 250, height: 31)
) )
} }
@ -102,14 +67,15 @@ struct SearchTextField: View {
self.state.queryText = "" self.state.queryText = ""
}) { }) {
Image(systemName: "xmark.circle.fill") Image(systemName: "xmark.circle.fill")
#if os(macOS)
.imageScale(.small)
#else
.imageScale(.medium) .imageScale(.medium)
#endif
} }
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
#if os(macOS) #if os(macOS)
.padding(.trailing, 5) .padding(.trailing, 10)
#elseif os(iOS)
.padding(.trailing, 5)
.foregroundColor(.gray)
#endif #endif
.opacity(0.7) .opacity(0.7)
} }

View File

@ -30,7 +30,6 @@ struct SearchView: View {
@Default(.saveRecents) private var saveRecents @Default(.saveRecents) private var saveRecents
@Default(.showHome) private var showHome @Default(.showHome) private var showHome
@Default(.searchListingStyle) private var searchListingStyle @Default(.searchListingStyle) private var searchListingStyle
@Default(.showSearchSuggestions) private var showSearchSuggestions
private var videos = [Video]() private var videos = [Video]()
@ -39,9 +38,9 @@ struct SearchView: View {
self.videos = videos self.videos = videos
} }
#if os(iOS)
var body: some View { var body: some View {
VStack { VStack {
#if os(iOS)
VStack { VStack {
if accounts.app.supportsSearchSuggestions, state.query.query != state.queryText { if accounts.app.supportsSearchSuggestions, state.query.query != state.queryText {
SearchSuggestions() SearchSuggestions()
@ -52,155 +51,27 @@ struct SearchView: View {
} }
.backport .backport
.scrollDismissesKeyboardInteractively() .scrollDismissesKeyboardInteractively()
} #else
.environment(\.listingStyle, searchListingStyle)
.toolbar {
ToolbarItem(placement: .principal) {
if #available(iOS 15, *) {
FocusableSearchTextField()
} else {
SearchTextField()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
searchMenu
}
}
.navigationBarTitleDisplayMode(.inline)
.ignoresSafeArea(.keyboard, edges: .bottom)
.navigationTitle("Search")
.onAppear {
if let query {
state.queryText = query.query
state.resetQuery(query)
updateFavoriteItem()
}
if !videos.isEmpty {
state.store.replace(ContentItem.array(of: videos))
}
}
.onChange(of: accounts.current) { _ in
state.reloadQuery()
}
.onChange(of: state.queryText) { newQuery in
if newQuery.isEmpty {
favoriteItem = nil
state.resetQuery()
} else {
updateFavoriteItem()
}
state.loadSuggestions(newQuery)
}
.onChange(of: searchSortOrder) { order in
state.changeQuery { query in
query.sortBy = order
updateFavoriteItem()
}
}
.onChange(of: searchDate) { date in
state.changeQuery { query in
query.date = date
updateFavoriteItem()
}
}
.onChange(of: searchDuration) { duration in
state.changeQuery { query in
query.duration = duration
updateFavoriteItem()
}
}
}
#elseif os(tvOS)
var body: some View {
VStack {
ZStack { ZStack {
results results
}
}
.environment(\.listingStyle, searchListingStyle)
.onAppear {
if let query {
state.queryText = query.query
state.resetQuery(query)
updateFavoriteItem()
}
if !videos.isEmpty { #if os(macOS)
state.store.replace(ContentItem.array(of: videos)) if accounts.app.supportsSearchSuggestions, state.query.query != state.queryText {
}
}
.onChange(of: accounts.current) { _ in
state.reloadQuery()
}
.onChange(of: state.queryText) { newQuery in
if newQuery.isEmpty {
favoriteItem = nil
state.resetQuery()
} else {
updateFavoriteItem()
}
if showSearchSuggestions {
state.loadSuggestions(newQuery)
}
searchDebounce.invalidate()
recentsDebounce.invalidate()
searchDebounce.debouncing(2) {
state.changeQuery { query in
query.query = newQuery
}
}
recentsDebounce.debouncing(10) {
recents.addQuery(newQuery)
}
}
.onChange(of: searchSortOrder) { order in
state.changeQuery { query in
query.sortBy = order
updateFavoriteItem()
}
}
.onChange(of: searchDate) { date in
state.changeQuery { query in
query.date = date
updateFavoriteItem()
}
}
.onChange(of: searchDuration) { duration in
state.changeQuery { query in
query.duration = duration
updateFavoriteItem()
}
}
.searchable(text: $state.queryText) {
if !state.queryText.isEmpty {
ForEach(state.querySuggestions, id: \.self) { suggestion in
Text(suggestion)
.searchCompletion(suggestion)
}
}
}
}
#elseif os(macOS)
var body: some View {
ZStack {
results
if accounts.app.supportsSearchSuggestions, state.query.query != state.queryText, showSearchSuggestions {
HStack { HStack {
Spacer() Spacer()
SearchSuggestions() SearchSuggestions()
.borderLeading(width: 1, color: Color("ControlsBorderColor")) .borderLeading(width: 1, color: Color("ControlsBorderColor"))
.frame(maxWidth: 262) .frame(maxWidth: 280)
.opacity(state.queryText.isEmpty ? 0 : 1) .opacity(state.queryText.isEmpty ? 0 : 1)
} }
} }
#endif
}
#endif
} }
.environment(\.listingStyle, searchListingStyle) .environment(\.listingStyle, searchListingStyle)
.toolbar { .toolbar {
#if os(macOS)
ToolbarItemGroup(placement: toolbarPlacement) { ToolbarItemGroup(placement: toolbarPlacement) {
ListingStyleButtons(listingStyle: $searchListingStyle) ListingStyleButtons(listingStyle: $searchListingStyle)
HideWatchedButtons() HideWatchedButtons()
@ -213,6 +84,7 @@ struct SearchView: View {
HStack { HStack {
Text("Sort:") Text("Sort:")
.foregroundColor(.secondary) .foregroundColor(.secondary)
searchSortOrderPicker searchSortOrderPicker
} }
} }
@ -229,6 +101,7 @@ struct SearchView: View {
SearchTextField() SearchTextField()
} }
} }
#endif
} }
.onAppear { .onAppear {
if let query { if let query {
@ -251,7 +124,23 @@ struct SearchView: View {
} else { } else {
updateFavoriteItem() updateFavoriteItem()
} }
state.loadSuggestions(newQuery) state.loadSuggestions(newQuery)
#if os(tvOS)
searchDebounce.invalidate()
recentsDebounce.invalidate()
searchDebounce.debouncing(2) {
state.changeQuery { query in
query.query = newQuery
}
}
recentsDebounce.debouncing(10) {
recents.addQuery(newQuery)
}
#endif
} }
.onChange(of: searchSortOrder) { order in .onChange(of: searchSortOrder) { order in
state.changeQuery { query in state.changeQuery { query in
@ -271,10 +160,35 @@ struct SearchView: View {
updateFavoriteItem() updateFavoriteItem()
} }
} }
.frame(minWidth: Constants.contentViewMinWidth) #if os(tvOS)
.navigationTitle("Search") .searchable(text: $state.queryText) {
if !state.queryText.isEmpty {
ForEach(state.querySuggestions, id: \.self) { suggestion in
Text(suggestion)
.searchCompletion(suggestion)
} }
}
}
#else
.ignoresSafeArea(.keyboard, edges: .bottom)
.navigationTitle("Search")
#endif #endif
#if os(iOS)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
searchMenu
}
ToolbarItem(placement: .principal) {
if #available(iOS 15, *) {
FocusableSearchTextField()
} else {
SearchTextField()
}
}
}
.navigationBarTitleDisplayMode(.inline)
#endif
}
#if os(iOS) #if os(iOS)
var searchMenu: some View { var searchMenu: some View {
@ -316,10 +230,11 @@ struct SearchView: View {
} }
} label: { } label: {
HStack { HStack {
Image(systemName: "magnifyingglass")
Image(systemName: "chevron.down.circle.fill") Image(systemName: "chevron.down.circle.fill")
.foregroundColor(.accentColor)
.imageScale(.large)
} }
.foregroundColor(.accentColor)
.imageScale(.medium)
} }
} }
#endif #endif

View File

@ -11,11 +11,9 @@ struct AdvancedSettings: View {
@Default(.mpvHWdec) private var mpvHWdec @Default(.mpvHWdec) private var mpvHWdec
@Default(.mpvDemuxerLavfProbeInfo) private var mpvDemuxerLavfProbeInfo @Default(.mpvDemuxerLavfProbeInfo) private var mpvDemuxerLavfProbeInfo
@Default(.mpvInitialAudioSync) private var mpvInitialAudioSync @Default(.mpvInitialAudioSync) private var mpvInitialAudioSync
@Default(.mpvSetRefreshToContentFPS) private var mpvSetRefreshToContentFPS
@Default(.showCacheStatus) private var showCacheStatus @Default(.showCacheStatus) private var showCacheStatus
@Default(.feedCacheSize) private var feedCacheSize @Default(.feedCacheSize) private var feedCacheSize
@Default(.showPlayNowInBackendContextMenu) private var showPlayNowInBackendContextMenu @Default(.showPlayNowInBackendContextMenu) private var showPlayNowInBackendContextMenu
@Default(.videoLoadingRetryCount) private var videoLoadingRetryCount
@State private var filesToShare = [MPVClient.logFile] @State private var filesToShare = [MPVClient.logFile]
@State private var presentingShareSheet = false @State private var presentingShareSheet = false
@ -66,7 +64,6 @@ struct AdvancedSettings: View {
@ViewBuilder var advancedSettings: some View { @ViewBuilder var advancedSettings: some View {
Section(header: SettingsHeader(text: "Advanced")) { Section(header: SettingsHeader(text: "Advanced")) {
showPlayNowInBackendButtonsToggle showPlayNowInBackendButtonsToggle
videoLoadingRetryCountField
} }
Section(header: SettingsHeader(text: "MPV"), footer: mpvFooter) { Section(header: SettingsHeader(text: "MPV"), footer: mpvFooter) {
@ -248,12 +245,6 @@ struct AdvancedSettings: View {
#endif #endif
} }
Toggle(isOn: $mpvSetRefreshToContentFPS) {
HStack {
Text("Sync refresh rate with content FPS EXPERIMENTAL")
}
}
if mpvEnableLogging { if mpvEnableLogging {
logButton logButton
} }
@ -290,19 +281,6 @@ struct AdvancedSettings: View {
Toggle("Show video context menu options to force selected backend", isOn: $showPlayNowInBackendContextMenu) Toggle("Show video context menu options to force selected backend", isOn: $showPlayNowInBackendContextMenu)
} }
private var videoLoadingRetryCountField: some View {
HStack {
Text("Maximum retries for video loading")
.frame(minWidth: 200, alignment: .leading)
.multilineTextAlignment(.leading)
TextField("Limit", value: $videoLoadingRetryCount, formatter: NumberFormatter())
.multilineTextAlignment(.trailing)
#if !os(macOS)
.keyboardType(.numberPad)
#endif
}
}
var showMPVPlaybackStatsToggle: some View { var showMPVPlaybackStatsToggle: some View {
Toggle("Show playback statistics", isOn: $showMPVPlaybackStats) Toggle("Show playback statistics", isOn: $showMPVPlaybackStats)
} }

View File

@ -10,7 +10,6 @@ struct BrowsingSettings: View {
@Default(.showUnwatchedFeedBadges) private var showUnwatchedFeedBadges @Default(.showUnwatchedFeedBadges) private var showUnwatchedFeedBadges
@Default(.keepChannelsWithUnwatchedFeedOnTop) private var keepChannelsWithUnwatchedFeedOnTop @Default(.keepChannelsWithUnwatchedFeedOnTop) private var keepChannelsWithUnwatchedFeedOnTop
#if os(iOS) #if os(iOS)
@Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape
@Default(.lockPortraitWhenBrowsing) private var lockPortraitWhenBrowsing @Default(.lockPortraitWhenBrowsing) private var lockPortraitWhenBrowsing
@Default(.showDocuments) private var showDocuments @Default(.showDocuments) private var showDocuments
#endif #endif
@ -20,7 +19,6 @@ struct BrowsingSettings: View {
@Default(.showOpenActionsToolbarItem) private var showOpenActionsToolbarItem @Default(.showOpenActionsToolbarItem) private var showOpenActionsToolbarItem
@Default(.visibleSections) private var visibleSections @Default(.visibleSections) private var visibleSections
@Default(.startupSection) private var startupSection @Default(.startupSection) private var startupSection
@Default(.showSearchSuggestions) private var showSearchSuggestions
@Default(.playerButtonSingleTapGesture) private var playerButtonSingleTapGesture @Default(.playerButtonSingleTapGesture) private var playerButtonSingleTapGesture
@Default(.playerButtonDoubleTapGesture) private var playerButtonDoubleTapGesture @Default(.playerButtonDoubleTapGesture) private var playerButtonDoubleTapGesture
@Default(.playerButtonShowsControlButtonsWhenMinimized) private var playerButtonShowsControlButtonsWhenMinimized @Default(.playerButtonShowsControlButtonsWhenMinimized) private var playerButtonShowsControlButtonsWhenMinimized
@ -68,7 +66,6 @@ struct BrowsingSettings: View {
homeSettings homeSettings
if !accounts.isEmpty { if !accounts.isEmpty {
startupSectionPicker startupSectionPicker
showSearchSuggestionsToggle
visibleSectionsSettings visibleSectionsSettings
} }
let interface = interfaceSettings let interface = interfaceSettings
@ -164,16 +161,12 @@ struct BrowsingSettings: View {
#if os(iOS) #if os(iOS)
Toggle("Show Documents", isOn: $showDocuments) Toggle("Show Documents", isOn: $showDocuments)
if Constants.isIPad {
Toggle("Lock portrait mode", isOn: $lockPortraitWhenBrowsing) Toggle("Lock portrait mode", isOn: $lockPortraitWhenBrowsing)
.onChange(of: lockPortraitWhenBrowsing) { lock in .onChange(of: lockPortraitWhenBrowsing) { lock in
if lock { if lock {
enterFullscreenInLandscape = true
Orientation.lockOrientation(.portrait, andRotateTo: .portrait) Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
} else { } else {
enterFullscreenInLandscape = false Orientation.lockOrientation(.allButUpsideDown)
Orientation.lockOrientation(.all)
}
} }
} }
#endif #endif
@ -248,10 +241,6 @@ struct BrowsingSettings: View {
} }
} }
private var showSearchSuggestionsToggle: some View {
Toggle("Show search suggestions", isOn: $showSearchSuggestions)
}
private func toggleSection(_ section: VisibleSection, value: Bool) { private func toggleSection(_ section: VisibleSection, value: Bool) {
if value { if value {
visibleSections.insert(section) visibleSections.insert(section)

View File

@ -86,7 +86,6 @@ struct InstanceForm: View {
.autocapitalization(.none) .autocapitalization(.none)
.keyboardType(.URL) .keyboardType(.URL)
#endif #endif
.disableAutocorrection(true)
#if os(tvOS) #if os(tvOS)
VStack { VStack {

View File

@ -8,7 +8,6 @@ struct InstanceSettings: View {
@State private var frontendURL = "" @State private var frontendURL = ""
@State private var proxiesVideos = false @State private var proxiesVideos = false
@State private var invidiousCompanion = false
var body: some View { var body: some View {
List { List {
@ -88,16 +87,6 @@ struct InstanceSettings: View {
InstancesModel.shared.setProxiesVideos(instance, newValue) InstancesModel.shared.setProxiesVideos(instance, newValue)
} }
} }
if instance.app == .invidious {
invidiousCompanionToggle
.onAppear {
invidiousCompanion = instance.invidiousCompanion
}
.onChange(of: invidiousCompanion) { newValue in
InstancesModel.shared.setInvidiousCompanion(instance, newValue)
}
}
} }
#if os(tvOS) #if os(tvOS)
.frame(maxWidth: 1000) .frame(maxWidth: 1000)
@ -112,10 +101,6 @@ struct InstanceSettings: View {
Toggle("Proxy videos", isOn: $proxiesVideos) Toggle("Proxy videos", isOn: $proxiesVideos)
} }
private var invidiousCompanionToggle: some View {
Toggle("Invidious companion", isOn: $invidiousCompanion)
}
private func removeAccount(_ account: Account) { private func removeAccount(_ account: Account) {
AccountsModel.remove(account) AccountsModel.remove(account)
accountsChanged.toggle() accountsChanged.toggle()

View File

@ -8,7 +8,6 @@ struct PlayerControlsSettings: View {
@Default(.playerControlsLayout) private var playerControlsLayout @Default(.playerControlsLayout) private var playerControlsLayout
@Default(.fullScreenPlayerControlsLayout) private var fullScreenPlayerControlsLayout @Default(.fullScreenPlayerControlsLayout) private var fullScreenPlayerControlsLayout
@Default(.horizontalPlayerGestureEnabled) private var horizontalPlayerGestureEnabled @Default(.horizontalPlayerGestureEnabled) private var horizontalPlayerGestureEnabled
@Default(.fullscreenPlayerGestureEnabled) private var fullscreenPlayerGestureEnabled
@Default(.seekGestureSpeed) private var seekGestureSpeed @Default(.seekGestureSpeed) private var seekGestureSpeed
@Default(.seekGestureSensitivity) private var seekGestureSensitivity @Default(.seekGestureSensitivity) private var seekGestureSensitivity
@Default(.buttonBackwardSeekDuration) private var buttonBackwardSeekDuration @Default(.buttonBackwardSeekDuration) private var buttonBackwardSeekDuration
@ -39,7 +38,6 @@ struct PlayerControlsSettings: View {
@Default(.playerControlsAdvanceToNextEnabled) private var playerControlsAdvanceToNextEnabled @Default(.playerControlsAdvanceToNextEnabled) private var playerControlsAdvanceToNextEnabled
@Default(.playerControlsPlaybackModeEnabled) private var playerControlsPlaybackModeEnabled @Default(.playerControlsPlaybackModeEnabled) private var playerControlsPlaybackModeEnabled
@Default(.playerControlsMusicModeEnabled) private var playerControlsMusicModeEnabled @Default(.playerControlsMusicModeEnabled) private var playerControlsMusicModeEnabled
@Default(.playerControlsBackgroundOpacity) private var playerControlsBackgroundOpacity
private var player = PlayerModel.shared private var player = PlayerModel.shared
@ -65,10 +63,9 @@ struct PlayerControlsSettings: View {
@ViewBuilder var sections: some View { @ViewBuilder var sections: some View {
#if !os(tvOS) #if !os(tvOS)
Section(header: SettingsHeader(text: "Player Controls".localized()), footer: controlsLayoutFooter) { Section(header: SettingsHeader(text: "Controls".localized()), footer: controlsLayoutFooter) {
#if !os(tvOS)
avPlayerUsesSystemControlsToggle avPlayerUsesSystemControlsToggle
#if os(iOS)
fullscreenPlayerGestureEnabledToggle
#endif #endif
horizontalPlayerGestureEnabledToggle horizontalPlayerGestureEnabledToggle
SettingsHeader(text: "Seek gesture sensitivity".localized(), secondary: true) SettingsHeader(text: "Seek gesture sensitivity".localized(), secondary: true)
@ -79,8 +76,6 @@ struct PlayerControlsSettings: View {
playerControlsLayoutPicker playerControlsLayoutPicker
SettingsHeader(text: "Fullscreen size".localized(), secondary: true) SettingsHeader(text: "Fullscreen size".localized(), secondary: true)
fullScreenPlayerControlsLayoutPicker fullScreenPlayerControlsLayoutPicker
SettingsHeader(text: "Background opacity".localized(), secondary: true)
playerControlsBackgroundOpacityPicker
} }
#endif #endif
@ -114,7 +109,7 @@ struct PlayerControlsSettings: View {
} }
var controlsButtonsSection: some View { var controlsButtonsSection: some View {
Section(header: SettingsHeader(text: "Player Control Buttons".localized())) { Section(header: SettingsHeader(text: "Controls Buttons".localized())) {
controlButtonToggles controlButtonToggles
} }
} }
@ -159,12 +154,8 @@ struct PlayerControlsSettings: View {
#endif #endif
} }
private var fullscreenPlayerGestureEnabledToggle: some View {
Toggle("Swipe up toggles fullscreen", isOn: $fullscreenPlayerGestureEnabled)
}
private var horizontalPlayerGestureEnabledToggle: some View { private var horizontalPlayerGestureEnabledToggle: some View {
Toggle("Seek with horizontal swipe", isOn: $horizontalPlayerGestureEnabled) Toggle("Seek with horizontal swipe on video", isOn: $horizontalPlayerGestureEnabled)
} }
private var avPlayerUsesSystemControlsToggle: some View { private var avPlayerUsesSystemControlsToggle: some View {
@ -211,15 +202,6 @@ struct PlayerControlsSettings: View {
.modifier(SettingsPickerModifier()) .modifier(SettingsPickerModifier())
} }
private var playerControlsBackgroundOpacityPicker: some View {
Picker("Background opacity", selection: $playerControlsBackgroundOpacity) {
ForEach(Array(stride(from: 0.0, through: 1.0, by: 0.1)), id: \.self) { value in
Text("\(Int(value * 100))%").tag(value)
}
}
.modifier(SettingsPickerModifier())
}
@ViewBuilder private var seekingSection: some View { @ViewBuilder private var seekingSection: some View {
seekingDurationSetting("System controls", $systemControlsSeekDuration) seekingDurationSetting("System controls", $systemControlsSeekDuration)
.foregroundColor(systemControlsCommands == .restartAndAdvanceToNext ? .secondary : .primary) .foregroundColor(systemControlsCommands == .restartAndAdvanceToNext ? .secondary : .primary)

View File

@ -18,8 +18,8 @@ struct PlayerSettings: View {
@Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer @Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer
@Default(.closeVideoOnEOF) private var closeVideoOnEOF @Default(.closeVideoOnEOF) private var closeVideoOnEOF
#if os(iOS) #if os(iOS)
@Default(.honorSystemOrientationLock) private var honorSystemOrientationLock
@Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape @Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape
@Default(.lockPortraitWhenBrowsing) private var lockPortraitWhenBrowsing
@Default(.rotateToLandscapeOnEnterFullScreen) private var rotateToLandscapeOnEnterFullScreen @Default(.rotateToLandscapeOnEnterFullScreen) private var rotateToLandscapeOnEnterFullScreen
#endif #endif
@Default(.closePiPOnNavigation) private var closePiPOnNavigation @Default(.closePiPOnNavigation) private var closePiPOnNavigation
@ -87,7 +87,7 @@ struct PlayerSettings: View {
} }
pauseOnHidingPlayerToggle pauseOnHidingPlayerToggle
closeVideoOnEOFToggle closeVideoOnEOFToggle
#if os(macOS) #if !os(tvOS)
exitFullscreenOnEOFToggle exitFullscreenOnEOFToggle
#endif #endif
#if !os(macOS) #if !os(macOS)
@ -202,12 +202,11 @@ struct PlayerSettings: View {
#endif #endif
#if os(iOS) #if os(iOS)
Section(header: SettingsHeader(text: "Fullscreen".localized())) { Section(header: SettingsHeader(text: "Orientation".localized())) {
if Constants.isIPad { if idiom == .pad {
enterFullscreenInLandscapeToggle enterFullscreenInLandscapeToggle
} }
honorSystemOrientationLockToggle
exitFullscreenOnEOFToggle
rotateToLandscapeOnEnterFullScreenPicker rotateToLandscapeOnEnterFullScreenPicker
} }
#endif #endif
@ -319,15 +318,20 @@ struct PlayerSettings: View {
#endif #endif
#if os(iOS) #if os(iOS)
private var honorSystemOrientationLockToggle: some View {
Toggle("Honor orientation lock", isOn: $honorSystemOrientationLock)
.disabled(!enterFullscreenInLandscape)
}
private var enterFullscreenInLandscapeToggle: some View { private var enterFullscreenInLandscapeToggle: some View {
Toggle("Enter fullscreen in landscape orientation", isOn: $enterFullscreenInLandscape) Toggle("Enter fullscreen in landscape", isOn: $enterFullscreenInLandscape)
.disabled(lockPortraitWhenBrowsing)
} }
private var rotateToLandscapeOnEnterFullScreenPicker: some View { private var rotateToLandscapeOnEnterFullScreenPicker: some View {
Picker("Default orientation", selection: $rotateToLandscapeOnEnterFullScreen) { Picker("Rotate when entering fullscreen on landscape video", selection: $rotateToLandscapeOnEnterFullScreen) {
Text("Landscape left").tag(FullScreenRotationSetting.landscapeLeft) Text("Landscape left").tag(FullScreenRotationSetting.landscapeRight)
Text("Landscape right").tag(FullScreenRotationSetting.landscapeRight) Text("Landscape right").tag(FullScreenRotationSetting.landscapeLeft)
Text("No rotation").tag(FullScreenRotationSetting.disabled)
} }
.modifier(SettingsPickerModifier()) .modifier(SettingsPickerModifier())
} }

View File

@ -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 = [.stream, QualityProfile.Format.hls] let avPlayerFormats = [QualityProfile.Format.hls, .stream, .mp4]
return !avPlayerFormats.contains(format) return !avPlayerFormats.contains(format)
} }
@ -315,9 +315,7 @@ struct QualityProfileForm: View {
func isResolutionDisabled(_ resolution: ResolutionSetting) -> Bool { func isResolutionDisabled(_ resolution: ResolutionSetting) -> Bool {
guard backend == .appleAVPlayer else { return false } guard backend == .appleAVPlayer else { return false }
let hd720p30 = Stream.Resolution.predefined(.hd720p30) return resolution.value > .hd720p30
return resolution.value > hd720p30
} }
func initializeForm() { func initializeForm() {

View File

@ -38,14 +38,12 @@ struct SubscriptionsView: View {
} }
.pickerStyle(.segmented) .pickerStyle(.segmented)
.labelStyle(.titleOnly) .labelStyle(.titleOnly)
subscriptionsMenu
} }
.frame(maxWidth: 500) .frame(maxWidth: 500)
} }
ToolbarItem(placement: .navigationBarTrailing) {
subscriptionsMenu
}
ToolbarItem { ToolbarItem {
RequestErrorButton(error: requestError) RequestErrorButton(error: requestError)
} }
@ -90,7 +88,7 @@ struct SubscriptionsView: View {
SettingsButtons() SettingsButtons()
} }
} label: { } label: {
HStack { HStack(spacing: 12) {
Image(systemName: "chevron.down.circle.fill") Image(systemName: "chevron.down.circle.fill")
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
.imageScale(.large) .imageScale(.large)

View File

@ -52,7 +52,7 @@ struct VerticalCells<Header: View>: View {
.edgesIgnoringSafeArea(edgesIgnoringSafeArea) .edgesIgnoringSafeArea(edgesIgnoringSafeArea)
#if os(macOS) #if os(macOS)
.background(Color.secondaryBackground) .background(Color.secondaryBackground)
.frame(minWidth: Constants.contentViewMinWidth) .frame(minWidth: 360)
#endif #endif
} }

View File

@ -24,42 +24,14 @@ 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 {
ZStack {
// Conditional overlay to block taps on underlying views
if isOverlayVisible {
Color.clear
.contentShape(Rectangle())
#if !os(tvOS)
// This is not available on tvOS < 16 so we leave out.
// TODO: remove #if when setting the minimum deployment target to >= 16
.onTapGesture {
// Dismiss overlay without triggering other interactions
isOverlayVisible = false
}
#endif
.ignoresSafeArea() // Ensure overlay covers the entire screen
.accessibilityLabel("Dismiss context menu")
.accessibilityHint("Tap to close the context")
.accessibilityAddTraits(.isButton)
}
if video.videoID != Video.fixtureID { if video.videoID != Video.fixtureID {
contextMenu contextMenu
.onAppear {
isOverlayVisible = true
}
.onDisappear {
isOverlayVisible = false
}
}
} }
} }

View File

@ -204,14 +204,9 @@ struct YatteeApp: App {
} }
#if os(iOS) #if os(iOS)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if Defaults[.lockPortraitWhenBrowsing] { if Defaults[.lockPortraitWhenBrowsing] {
Orientation.lockOrientation(.portrait, andRotateTo: .portrait) Orientation.lockOrientation(.all, andRotateTo: .portrait)
} else {
let rotationOrientation =
OrientationTracker.shared.currentDeviceOrientation.rawValue == 4 ? UIInterfaceOrientation.landscapeRight :
(OrientationTracker.shared.currentDeviceOrientation.rawValue == 3 ? UIInterfaceOrientation.landscapeLeft : UIInterfaceOrientation.portrait)
Orientation.lockOrientation(.all, andRotateTo: rotationOrientation)
} }
} }
#endif #endif
@ -230,17 +225,6 @@ struct YatteeApp: App {
DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.global(qos: .userInitiated).async {
self.migrateQualityProfiles() self.migrateQualityProfiles()
} }
#if os(iOS)
DispatchQueue.global(qos: .userInitiated).async {
self.migrateRotateToLandscapeOnEnterFullScreen()
}
DispatchQueue.global(qos: .userInitiated).async {
self.migrateLockPortraitWhenBrowsing()
}
#endif
} }
} }
@ -269,22 +253,6 @@ struct YatteeApp: App {
} }
} }
#if os(iOS)
func migrateRotateToLandscapeOnEnterFullScreen() {
if Defaults[.rotateToLandscapeOnEnterFullScreen] != .landscapeRight || Defaults[.rotateToLandscapeOnEnterFullScreen] != .landscapeLeft {
Defaults[.rotateToLandscapeOnEnterFullScreen] = .landscapeRight
}
}
func migrateLockPortraitWhenBrowsing() {
if Constants.isIPhone {
Defaults[.lockPortraitWhenBrowsing] = true
} else if Constants.isIPad, Defaults[.lockPortraitWhenBrowsing] {
Defaults[.enterFullscreenInLandscape] = true
}
}
#endif
var navigationStyle: NavigationStyle { var navigationStyle: NavigationStyle {
#if os(iOS) #if os(iOS)
return horizontalSizeClass == .compact ? .tab : .sidebar return horizontalSizeClass == .compact ? .tab : .sidebar

View File

@ -4,7 +4,7 @@
"Accounts" = "Konten"; "Accounts" = "Konten";
"Add Account" = "Konto hinzufügen"; "Add Account" = "Konto hinzufügen";
"Add Location" = "Ort hinzufügen"; "Add Location" = "Ort hinzufügen";
"Add Location..." = "Standort hinzufügen …"; "Add Location..." = "Ort hinzufügen …";
"Add to Playlist" = "Zu Wiedergabeliste hinzufügen"; "Add to Playlist" = "Zu Wiedergabeliste hinzufügen";
"Backend" = "Backend"; "Backend" = "Backend";
"Badge color" = "Markierungsfarbe"; "Badge color" = "Markierungsfarbe";

View File

@ -398,7 +398,7 @@
"Hardware decoder" = "Décodeur matériel"; "Hardware decoder" = "Décodeur matériel";
"Stream FPS" = "IPS du flux"; "Stream FPS" = "IPS du flux";
"Cached time" = "Temps mis en cache"; "Cached time" = "Temps mis en cache";
"Dropped frames" = "Images perdues"; "Dropped frames" = "Images perdus";
"Any format" = "Tout formats"; "Any format" = "Tout formats";
"Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "La liste de lecture est vide\n\nAppuyez longuement sur une vidéo, puis sur\n\"Ajouter à la liste de lecture\""; "Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "La liste de lecture est vide\n\nAppuyez longuement sur une vidéo, puis sur\n\"Ajouter à la liste de lecture\"";
"Comments are disabled" = "Les commentaires sont désactivés"; "Comments are disabled" = "Les commentaires sont désactivés";

View File

@ -1,564 +0,0 @@
" subscribers" = " feliratkozók";
"%@ Channel" = "%@ Csatorna";
"%@ Playlist" = "%@ Lejátszási lista";
"10 seconds forwards/backwards" = "10 másodperc előre/vissza";
"%@ subscribers" = "%@ feliratkozók";
"%lld videos" = "%lld videók";
"No results" = "Nincsenek találatok";
"No Playlists" = "Nincsenek lejátszási listák";
"Mark video as watched after playing" = "Jelölje meg a videót megtekintettként lejátszás után";
"Mark watched videos with" = "Megtekintett videók megjelölése a következővel";
"Matrix Channel" = "Matrix csatorna";
"Find Other" = "Egyebek keresése";
"Hour" = "Óra";
"Month" = "Hónap";
"Save history of played videos" = "Lejátszott videókelőzmények mentése";
"Playlists" = "Lejátszási listák";
"Reset search filters" = "Keresési szűrők visszaállítása";
"Promoting a product or service that is directly related to the creator themselves. This usually includes merchandise or promotion of monetized platforms." = "Olyan termék vagy szolgáltatás népszerűsítése, amely közvetlenül kapcsolódik az alkotóhoz. Ez általában magában foglalja az árucikkeket vagy a pénzzel fizetett platformok reklámozását.";
"Orientation" = "Tájolás";
"Clear Queue before opening" = "Várólista kiürítése megnyitás előtt";
"Remove from the queue" = "Eltávolítás a várólistáról";
"Part of a video promoting a product or service not directly related to the creator. The creator will receive payment or compensation in the form of money or free products." = "A videó egy olyan termék vagy szolgáltatás népszerűsítésére szolgáló része, amely nem kapcsolódik közvetlenül az alkotóhoz. Az alkotó fizetést vagy kompenzációt kap pénz vagy ingyenes termékek formájában.";
"Rotate to portrait when exiting fullscreen" = "Forduljon fekvő módba a teljes képernyőből való kilépéskor";
"Pause" = "Szünet";
"Profiles" = "Profilok";
"Resolution" = "Felbontás";
"Round corners" = "Kerekített sarkok";
"Sign In Required" = "Bejelentkezés szükséges";
"When partially watched video is played" = "Részlegesen megtekintett videó lejátszásakor";
"Segments typically found at the start of a video that include an animation, still frame or clip which are also seen in other videos by the same creator." = "Olyan szegmensek, amelyek jellemzően egy videó elején találhatók, és olyan animációt, állóképet vagy klipet tartalmaznak, amelyek ugyanazon alkotó más videóiban is láthatók.";
"Sort" = "Rendezés";
"Shuffle" = "Keverés";
"Seek gesture speed" = "Keresési gesztus sebessége";
"You can use automatic profile selection based on current device status or switch it in video playback settings controls." = "Használhatja az automatikus profilválasztást az eszköz aktuális állapota alapján, vagy átkapcsolhatja a videólejátszás beállításainak vezérlőelemein.";
"Seek with horizontal swipe on video" = "Keresés vízszintes húzással videón";
"Short" = "Rövid";
"Show keywords" = "Kulcsszavak megjelenítése";
"Welcome" = "Üdvözöljük";
"Sort: %@" = "Rendezés: %@";
"This cannot be reverted. You might need to switch between views or restart the app to see changes." = "Ezt nem lehet visszaállítani. Előfordulhat, hogy váltania kell a nézetek között, vagy újra kell indítania az alkalmazást, hogy láthassa a változásokat.";
"That's nice to hear. It is fun to deliver apps other people want to use. You can consider donating to the project or help by contributing to new features development." = "Ezt jó hallani. Jó móka olyan alkalmazásokat kézbesíteni, amelyeket mások is használni akarnak. Fontolja meg, hogy adományoz a projektnek, vagy közreműködhet az új funkciók fejlesztéséhez való hozzájárulással.";
"Wi-Fi" = "Wi-Fi";
"Accounts" = "Fiókok";
"Accounts are not supported for the application of this instance" = "A fiókok nem támogatottak ennek a példánynak az alkalmazása során";
"Add Account" = "Fiók hozzáadása";
"Add Account..." = "Fiók hozzáadása...";
"Add Location" = "Hely hozzáadása";
"Add Location..." = "Hely hozzáadása..";
"Add profile..." = "Profil hozzáadása...";
"Add Quality Profile" = "Minőségi profil hozzáadása";
"Add to %@" = "Hozzáadás a következőhöz: %@";
"Add to Favorites" = "Hozzáadás a kedvencekhez";
"Add to Playlist" = "Hozzáadás a lejátszási listához";
"Add to Playlist..." = "Hozzáadás a lejátszási listához...";
"Advanced" = "Speciális";
"All" = "Összes";
"Always use AVPlayer for live videos" = "Mindig az a AVPlayer-t használja az élő videókhoz";
"Anonymous" = "Névtelen";
"Any" = "Bármely";
"Apply to all" = "Alkalmazás az összesre";
"Are you sure you want to clear history of watched videos?" = "Biztosan törölni szeretné a megtekintett videók előzményeit?";
"Are you sure you want to clear search history?" = "Biztosan törölni szeretné a keresési előzményeket?";
"Are you sure you want to delete playlist?" = "Biztosan törölni szeretné a lejátszási listát?";
"Are you sure you want to restore default quality profiles?" = "Biztosan vissza szeretné állítani az alapértelmezett minőségi profilokat?";
"Are you sure you want to unsubscribe from %@?" = "Biztosan le szeretne iratkozni a(z) %@ szolgáltatásról?";
"Automatic" = "Automatikus";
"Autoplaying Next" = "Következő videó automatikus lejátszása";
"Backend" = "Háttérszolgáltatás";
"Badge" = "Címke";
"Badge & Decreased opacity" = "Címke és csökkentett átlátszóság";
"Badge color" = "Címke színe";
"Based on system color scheme" = "Rendszer színséma alapján";
"Battery" = "Akkumulátor";
"Blue" = "Kék";
"Browsing" = "Böngészés";
"Buffering stream..." = "Adatfolyam pufferelése...";
"Bugs and great feature ideas can be sent to the GitHub issues tracker. " = "A hibákat és nagyszerű funkcióötleteket a GitHub hibakövető rendszerébe beküldheti. ";
"Button" = "Gomb";
"Cancel" = "Mégse";
"Captions" = "Feliratok";
"Categories to Skip" = "Kihagyni kívánt kategóriák";
"Category" = "Kategória";
"Cellular" = "Mobil";
"Chapters" = "Fejezetek";
"Charging" = "Töltés";
"Clear" = "Kiürítés";
"Clear All" = "Összes kiürítése";
"Clear All Recents" = "Összes legutóbbi törlése";
"Clear History" = "Előzmények törlése";
"Clear Search History" = "Keresési előzmények törlése";
"Clear Search History..." = "Keresési előzmények törlése...";
"Clear the queue" = "Várólista törlése";
"Close" = "Bezárás";
"Close PiP and open player when application enters foreground" = "PiP bezárása és a lejátszó megnyitása, amikor az alkalmazás előtérbe kerül";
"Close PiP when player is opened" = "PiP bezárása a lejátszó megnyitásakor";
"Close PiP when starting playing other video" = "PiP bezárása más videó lejátszásának megkezdésekor";
"Close player when closing video" = "Lejátszó bezárása a videó bezárásakor";
"Close player when starting PiP" = "Lejátszó bezárása PiP indításakor";
"Close Video" = "Videó bezárása";
"Comments" = "Hozzászólások";
"Connected successfully (%@)" = "Sikeres csatlakozás (%@)";
"Connection failed" = "A kapcsolat sikertelen";
"Contact" = "Névjegy";
"Continue" = "Folytatás";
"Close video after playing last in the queue" = "Videó bezárása a várólistában lévő utolsó lejátszás után";
"Continue from %@" = "Folytatás ettől: %@";
"Contributing" = "Közreműködés";
"Controls" = "Vezérlők";
"Copy %@ link" = "%@ hivatkozás másolása";
"Copy %@ link with time" = "%@ hivatkozás másolása idővel";
"Country" = "Ország";
"Country Name or Code" = "Országnév vagy országkód";
"Create Playlist" = "Lejátszási lista létrehozása";
"Current: %@\n%@" = "Jelenlegi: %@\n%@";
"Custom" = "Egyéni";
"Custom Locations" = "Egyéni helyek";
"Date" = "Dátum";
"Decrease rate" = "Arány csökkentése";
"Decreased opacity" = "Csökkentett átlátszatlanság";
"Delete" = "Törlés";
"Disabled" = "Letiltva";
"Discord Server" = "Discord-kiszolgáló";
"Discussions take place in Discord and Matrix. It's a good spot for general questions." = "A beszélgetések a Discordon és a Matrixon zajlanak. Ez egy jó hely az általános kérdésekre.";
"Could not load locations manifest" = "Nem sikerült betölteni a helyek listáját";
"Don't use public locations" = "Ne használjon nyilvános helyeket";
"Donations" = "Adományok";
"Done" = "Kész";
"Duration" = "Időtartam";
"Edit" = "Szerkesztés";
"Edit Playlist" = "Lejátszási lista szerkesztése";
"Edit Quality Profile" = "Minőségi profil szerkesztése";
"Edit..." = "Szerkesztés...";
"Enable logging" = "Naplózás engedélyezése";
"Enable Return YouTube Dislike" = "Youtube Dislike visszahozásának engedélyezése";
"Enter fullscreen in landscape" = "Teljes képernyőre váltás fekvőben";
"Error" = "Hiba";
"Error when accessing playlist" = "Hiba a lejátszási lista elérésekor";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Kifejezett emlékeztetők arra, hogy lájkolják, feliratkozzanak vagy interakcióba lépjenek velük bármely fizetős vagy ingyenes platform(ok)on (pl. kattintsanak egy videóra).";
"Favorites" = "Kedvencek";
"Filter" = "Szűrő";
"Filter: active" = "Szűrő: aktív";
"Finding something to play..." = "Valami lejátszható keresése...";
"For videos which feature music as the primary content." = "Olyan videók esetében, amelyek elsődleges tartalma a zene.";
"Formats will be selected in order as listed.\nHLS is an adaptive format (resolution setting does not apply)." = "A formátumok a felsorolt sorrendben kerülnek kiválasztásra.\nA HLS adaptív formátum (a felbontás beállítására nem érvényes).";
"Frontend URL" = "Előtétprogram webcíme";
"Fullscreen size" = "Teljes képernyő mérete";
"Gaming" = "Játék";
"Help" = "Súgó";
"Hide sidebar" = "Oldalsáv elrejtése";
"High" = "Magas";
"Highest" = "Legmagasabb";
"Highest quality" = "Legmagasabb minőség";
"History" = "Előzmények";
"Honor orientation lock" = "Tájolás zárolása";
"I am lost" = "Elvesztem";
"I found a bug /" = "Találtam egy hibát /";
"I have a feature request" = "Van egy funkció kérésem";
"I like this app!" = "Tetszik ez az alkalmazás!";
"I want to ask a question" = "Szeretnék feltenni egy kérdést";
"If you are interested what's coming in future updates, you can track project Milestones." = "Ha érdekli Önt, hogy mi várható a jövőbeni frissítésekben, akkor nyomon követheti a projekt mérföldköveit.";
"If you are reporting a bug, include all relevant details (especially: app version, used device and system version, steps to reproduce)." = "Ha egy hibát jelent, írjon bele minden lényeges adatot (különösen: az alkalmazás verzióját, a használt eszköz és rendszer verzióját, a reprodukálás lépéseit).";
"Increase rate" = "Arány növelése";
"Info" = "Információ";
"Instance of current account" = "Jelenlegi fiók példánya";
"Interaction" = "Interakció";
"Interface" = "Kezelőfelület";
"Intro" = "Intro";
"Issues Tracker" = "Hibakövető";
"Just watched" = "Megtekintettek";
"Large" = "Nagy";
"Large layout is not suitable for all devices and using it may cause controls not to fit on the screen." = "A nagyméretű elrendezés nem minden eszközön használható, és használatakor előfordulhat, hogy a vezérlőelemek nem férnek el a képernyőn.";
"LIVE" = "ÉLŐ";
"Loading streams…" = "Közvetítések betöltése…";
"Loading..." = "Betöltés...";
"Locations" = "Helyek";
"Lock portrait mode" = "Álló mód rögzítése";
"Long" = "Hosszú";
"Low" = "Alacsony";
"Low quality" = "Alacsony minőség";
"Lowest" = "Legalacsonyabb";
"Mark as watched" = "Jelölés megtekintettnek";
"Matrix Chat" = "Matrix csevegés";
"Medium" = "Közepes";
"Medium quality" = "Közepes minőség";
"Milestones" = "Mérföldkövek";
"More info can be found in:" = "További információ a következő oldalon található:";
"Movies" = "Filmek";
"MPV Documentation" = "MPV dokumentáció";
"Music" = "Zene";
"Name" = "Név";
"New Playlist" = "Új lejátszási lista";
"Next" = "Következő";
"No description" = "Nincs leírás";
"Normal" = "Normál";
"Not available" = "Nem elérhető";
"Not Playing" = "Nincs lejátszás";
"Nothing" = "Semmi";
"Offtopic in Music Videos" = "Nem kapcsolódó tartalom a Zene videókban";
"Only when signed in" = "Csak bejelentkezés esetén";
"Open \"Playlists\" tab to create new one" = "Új lejátszási listák létrehozásához nyissa meg a „Lejátszási listák” lapot";
"Open Settings" = "Beállítások megnyitása";
"Opening %@ stream…" = "%@ közvetítés megnyitása…";
"Opening audio stream…" = "Hangfolyam megnyitása…";
"Outro" = "Outro";
"Password" = "Jelszó";
"Pause when entering background" = "Szünet háttérbe lépéskor";
"Pause when player is closed" = "Szünet a lejátszó bezárásakor";
"Picture in Picture" = "Kép a képben";
"Play" = "Lejátszás";
"Play All" = "Összes lejátszása";
"Play in PiP" = "Lejátszás PiP-ben";
"Play Last" = "Legutóbbi lejátszása";
"Play Music" = "Zene lejátszása";
"Play Next" = "Következő lejátszása";
"Play Now" = "Lejátszás most";
"Playback" = "Visszajátszás";
"Player" = "Lejátszó";
"Playlist" = "Lejátszási lista";
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "A(z) „%@” lejátszási lista törlésre kerül.\nEzt nem lehet visszaállítani.";
"Popular" = "Népszerű";
"Preferred Formats" = "Előnyben részesített formátumok";
"Proxy videos" = "Proxyzott videók";
"Public Locations" = "Nyilvános helyek";
"Public Manifest" = "Nyilvános lista";
"Quality" = "Minőség";
"Quality Profile" = "Minőségi profil";
"Queue" = "Várólista";
"Queue is empty" = "A várólista üres";
"Rate" = "Értékelés";
"Rating" = "Értékelések";
"Recents" = "Újdonságok";
"Red" = "Piros";
"Refresh" = "Frissítés";
"Regular size" = "Szabályos méret";
"Regular Size" = "Szabályos méret";
"Related" = "Kapcsolódó";
"Relevance" = "Releváns";
"Remove" = "Eltávolítás";
"Remove from Favorites" = "Eltávolítás a kedvencekből";
"Remove from history" = "Eltávolítás az előzményekből";
"Remove from Playlist" = "Eltávolítás a lejátszási listáról";
"Replies" = "Válaszok";
"Reset" = "Visszaállítás";
"Reset watched status when playing again" = "Megfigyelt állapot visszaállítása újbóli lejátszáskor";
"Restart" = "Újraindítás";
"Restart the app to apply the settings above." = "A fenti beállítások alkalmazásához indítsa újra az alkalmazást.";
"Restart/Play next" = "Újraindítás/Következő lejátszása";
"Restore default profiles..." = "Alapértelmezett profilok helyreállítása...";
"Save" = "Mentés";
"Save history of searches, channels and playlists" = "Keresések, csatornák és lejátszási listák előzményeinek mentése";
"Search" = "Keresés";
"Search history is empty" = "A keresési előzmények üresek";
"Search..." = "Keresés...";
"Sections" = "Szakaszok";
"Seek gesture sensitivity" = "Keresési gesztus érzékenysége";
"Select location closest to you:" = "Válassza ki az Önhöz legközelebbi helyet:";
"Self-promotion" = "Önreklámozás";
"Settings" = "Beállítások";
"Share %@ link" = "%@ hivatkozás megosztása";
"Share %@ link with time" = "%@ hivatkozás megosztása idővel";
"Share..." = "Megosztás...";
"Show account username" = "Fiók felhasználónév megjelenítése";
"Show anonymous accounts" = "Névtelen fiókok megjelenítése";
"Show channel name" = "Csatorna nevének megjelenítése";
"Show history" = "Előzmények megjelenítése";
"Show playback statistics" = "Lejátszási statisztikák megjelenítése";
"Show progress of watching on thumbnails" = "Megtekintés előrehaladásának megjelenítése a miniatűrökön";
"Show sidebar when space permits" = "Oldalsáv megjelenítése, ha van elengendő hely";
"Show video length" = "Videó hosszának megjelenítése";
"Shuffle All" = "Összes keverése";
"Sidebar" = "Oldalsáv";
"Small" = "Kicsi";
"Smaller" = "Kisebb";
"Source" = "Forrás";
"Sponsor" = "Szponzor";
"SponsorBlock" = "SponsorBlock";
"SponsorBlock API Instance" = "SponsorBlock API példány";
"Subscribe" = "Feliratkozás";
"Subscriptions" = "Feliratkozások";
"Switch to other public location" = "Váltás más nyilvános helyre";
"Switch to public locations" = "Váltás nyilvános helyekre";
"System controls buttons" = "Rendszervezérlő gombok";
"System controls show buttons for %@" = "A rendszervezérlők gombokat jelenítenek meg a következőhöz: %@";
"This cannot be reverted" = "Ezt nem lehet visszaállítani";
"This information will be processed only on your device and used to connect you to the server in the specified country." = "Ez az információ csak az Ön eszközén kerül feldolgozásra, és arra használjuk, hogy Önt a megadott országban lévő kiszolgálóhoz kapcsoljuk.";
"This will remove all your custom profiles and return their default values. This cannot be reverted." = "Ez eltávolítja az összes egyéni profilját, és visszaállítja azok alapértelmezett értékeit. Ezt nem lehet visszaállítani.";
"Thumbnails" = "Miniatűrök";
"Today" = "Ma";
"Trending" = "Felkapott";
"TV" = "TV";
"Typically near or at the end of the video when the credits pop up and/or endcards are shown." = "Általában a videó közelében vagy a végén, amikor a köszönetnyilvánítás felugrik/vagy a végkártyák megjelennek.";
"unknown" = "ismeretlen";
"Unsubscribe" = "Leiratkozás";
"Upload date" = "Feltöltés dátuma";
"URL" = "Webcím";
"Used to create links from videos, channels and playlists" = "Videók, csatornák és lejátszási listák hivatkozásainak létrehozásához használható";
"Username" = "Felhasználónév";
"Very Large" = "Nagyon nagy";
"Videos" = "Videók";
"Views" = "Megtekintések";
"Watched" = "Megtekintett";
"Watched %@" = "Megtekintve ekkor: %@";
"Watching now" = "Megtekintés most";
"Week" = "Hét";
"Wiki" = "Wiki";
"Yattee" = "Yattee";
"Yattee %@ (build %@)" = "Yattee %@ (összeállítási szám: %@)";
"Year" = "Év";
"You can find information about using Yattee in the Wiki pages." = "A Yattee használatával kapcsolatos információkat a Wiki oldalon találhat.";
"You have no Playlists" = "Nincsenek lejátszási listái";
"You have no playlists\n\nTap on \"New Playlist\" to create one" = "Nincsenek lejátszási listái\n\nKoppintson az „Új lejátszási lista” gombra a létrehozásához";
"Playback queue is empty" = "A lejátszási várólista üres";
"Keep last played video in the queue after restart" = "Utoljára lejátszott videó megtartása a várólistában újraindítás után";
"Reload manifest" = "Lista újratöltése";
"Locations Manifest" = "Helyek listája";
"Next in Queue" = "Következő a várólistán";
"Show Next in Queue" = "Következő megjelenítése a várólistán";
"Queue - shuffled" = "Várólista - kevert";
"Unlisted" = "Felsorolatlan";
"Current Location" = "Jelenlegi hely";
"Private" = "Privát";
"Playing Next" = "Következő lejátszása";
"Add Channels, Playlists and Searches to Favorites using" = "Csatornák, lejátszási listák és keresések hozzáadása a kedvencekhez a következő használatával";
"Make default" = "Legyen alapértelmezett";
"Visibility" = "Láthatóság";
"Current Playlist" = "Jelenlegi lejátszási lista";
"Stream & Player" = "Közvetítő és lejátszó";
"Statistics" = "Statisztika";
"Hardware decoder" = "Hardveres dekódoló";
"Stream FPS" = "FPS folyam";
"Rate & Captions" = "Értékelés és feliratok";
"Dropped frames" = "Eldobott keretek";
"Any format" = "Bármilyen formátum";
"It can be changed later in settings. You can use your own locations too." = "Ez később a beállításokban módosítható. Saját helyeket is használhat.";
"Comments are disabled" = "Hozzászólások letiltva";
"No comments" = "Nincsenek hozzászólások";
"No chapters information available" = "Nincsenek elérhető fejezetinformációk";
"Could not refresh Subscriptions" = "Nem sikerült frissíteni a feliratkozásokat";
"Could not load streams" = "Nem sikerült betölteni a közvetítéseket";
"Could not open video" = "Nem sikerült megnyitni a videót";
"Channel could not be found" = "A csatorna nem található";
"Could not extract SID from received cookies: %@" = "Nem sikerült kinyerni az SID-t a kapott sütikből: %@";
"Could not update your token." = "Nem sikerült a token frissítése.";
"Could not refresh Trending" = "Nem sikerült a felkapottak frissítése";
"This URL could not be opened" = "Ez a webcím nem nyitható meg";
"Could not open channel" = "Nem sikerült megnyitni a csatornát";
"Could not refresh Popular" = "Nem sikerült a népszerűek frissítése";
"Could not extract video ID" = "Nem sikerült kinyerni a videó azonosítóját";
"This video could not be opened" = "Ez a videó nem nyitható meg";
"Could not extract playlist ID" = "Nem sikerült kinyerni a lejátszási lista azonosítóját";
"Could not load video" = "Nem sikerült a videót betölteni";
"Translations" = "Fordítások";
"No documents" = "Nincsenek dokumentumok";
"Recent Documents" = "Legutóbbi dokumentumok";
"Home" = "Kezdőlap";
"Show Home" = "Kezdőlap megjelenítése";
"Show Open Videos quick actions" = "Nyitott videók gyors műveleteinek megjelenítése";
"Show Favorites" = "Kedvencek megjelenítése";
"Inspector visibility" = "Felügyelő láthatósága";
"Edit Favorites…" = "Kedvencek szerkesztése…";
"Buttons labels" = "Gombok feliratai";
"Files" = "Fájlok";
"Show Documents" = "Dokumentumok megjelenítése";
"Pages toolbar position" = "Oldalak eszköztár helyzete";
"Video Details" = "Videó részletek";
"Show Inspector" = "Felügyelő megjelenítése";
"Open" = "Megnyitás";
"Video actions buttons" = "Videó műveleti gombok";
"Enter link to open" = "Írja be a megnyitni kívánt hivatkozást";
"URL to Open" = "Megnyitni kívánt webcím";
"Enter links to open, one per line" = "Írja be a megnyitni kívánt hivatkozásokat, soronként egyet";
"Add" = "Hozzáadás";
"Hide" = "Elrejtés";
"Always" = "Mindig";
"Playback Mode" = "Lejátszási mód";
"Left" = "Bal";
"Format" = "Formátum";
"Driver" = "Illesztőprogram";
"Show only icons" = "Csak az ikonok megjelenítése";
"Center" = "Középen";
"Documents" = "Dokumentumok";
"Audio" = "Hang";
"Codec" = "Kodek";
"Size" = "Méret";
"FPS" = "FPS";
"Sample Rate" = "Mintavételi sebesség";
"Could not find any links to open in your clipboard" = "A vágólapon nem találhatók megnyitni kívánt hivatkozások";
"Address" = "Cím";
"Remove…" = "Eltávolítás…";
"Actions buttons" = "Műveleti gombok";
"Show sidebar" = "Oldalsáv megjelenítése";
"Remove Location" = "Hely eltávolítása";
"Open Video" = "Videó megnyitása";
"Default Profile" = "Alapértelmezett profil";
"Share%@link" = "%@ hivatkozás megosztása";
"\"%@\" will be irreversibly removed from this device." = "A(z) „%@” visszavonhatatlanul eltávolításra kerül erről az eszközről.";
"Could not delete document" = "A dokumentum törlése nem sikerült";
"Are you sure you want to remove %@ location?" = "Biztosan törölni szeretné a(z) %@ helyet?";
"Live Streams" = "Élő közvetítések";
"Verified" = "Ellenőrzött";
"Channel" = "Csatorna";
"Open expanded" = "Megnyitás kibontva";
"Mark channel feed as watched" = "Jelölje meg a csatorna hírfolyamot megtekintettként";
"Short videos: visible" = "Rövid videók: láthatóak";
"Player Bar" = "Lejátszó sáv";
"Short videos: hidden" = "Rövid videók: rejtett";
"Double tap gesture" = "Dupla koppintás gesztus";
"Always show controls buttons" = "Mindig jelenítse meg a vezérlőgombokat";
"Single tap gesture" = "Egyszeri koppintás gesztus";
"Maximum width expanded" = "Maximális szélesség kiterjesztve";
"Clear all" = "Összes kiürítése";
"Right click channel thumbnail to open context menu with more actions" = "Jobb kattintás a csatorna miniatűrjére a további műveletekhez tartozó kontextusmenü megnyitásához";
"Show unwatched feed badges" = "Nem megtekintett hírfolyam jelvények megjelenítése";
"Seeking" = "Keresés";
"Controls Buttons" = "Vezérlőgombok";
"System controls" = "Rendszervezérlők";
"Controls button: forwards" = "Vezérlőgomb: előre";
"Gesture: backwards" = "Gesztus: hátra";
"Hide player" = "Lejátszó elrejtése";
"Gesture settings control skipping interval for remote arrow buttons (for 2nd generation Siri Remote or newer). Changing system controls settings requires restart." = "A gesztusbeállítások vezérlik a kihagyási időközt a távirányítón található nyíl gombok számára (a 2. generációs Siri Remote vagy újabb modellek esetén). A rendszervezérlők beállításainak módosítása újraindítást igényel.";
"Actions Buttons" = "Műveleti gombok";
"Play next item" = "Következő elem lejátszása";
"Lock orientation" = "Tájolás zárolása";
"Music Mode" = "Zenemód";
"Close video" = "Videó bezárása";
"Total size: %@" = "Teljes méret: %@";
"Open channels with description expanded" = "Csatornák megnyitása bővített leírással";
"Cache" = "Gyorsítótár";
"Subscribe/Unsubscribe" = "Feliratkozás/Leiratkozás";
"Show cache status" = "Gyorsítótár állapotának megjelenítése";
"Maximum feed items" = "Maximális hírfolyam elemek";
"Are you sure you want to clear cache?" = "Biztosan törölni szeretné a gyorsítótárat?";
"Show toggle watch status button" = "Figyelési állapot váltó gomb megjelenítése";
"List" = "Lista";
"Cells" = "Cellák";
"Toggle size" = "Méret váltása";
"Toggle player" = "Lejátszó váltása";
"Do nothing" = "Ne tegyen semmit";
"Feed" = "Hírfolyam";
"Mark all as unwatched" = "Jelölje meg az összeset nem megtekintettnek";
"Playback Settings" = "Lejátszási beállítások";
"Mark all as watched" = "Jelölje meg az összeset megtekintettnek";
"Replay" = "Újrajátszás";
"Fullscreen" = "Teljes képernyő";
"Lock" = "Zárolás";
"Description" = "Leírás";
"Loop one" = "Ismétlés egyesével";
"Autoplay next" = "Következő automatikus lejátszása";
"Stream" = "Közvetítés";
"Enter location address to connect..." = "Adja meg a hely címét a kapcsolódáshoz...";
"Seek" = "Keresés";
"Opened File" = "Megnyitott fájl";
"File Extension" = "Fájl kiterjesztése";
"Opening file…" = "Fájl megnyitása…";
"Public account" = "Nyilvános fiók";
"Your Accounts" = "Saját fiókok";
"Close video and player on end" = "A videó és a lejátszó bezárása a lejátszás befejeztével";
"Use system controls with AVPlayer" = "A rendszervezérlők használata az AVPlayer-rel";
"Landscape left" = "Fekvő balra";
"Landscape right" = "Fekvő jobbra";
"No rotation" = "Nincs forgatás";
"Startup section" = "Indítási szakasz";
"Home Settings" = "Kezdőlap beállítások";
"(watched and shorts hidden)" = "(megtekintettek és rövidek elrejtve)";
"Watched: hidden" = "Megtekintettek: rejtett";
"No videos to show" = "Nincs megjeleníthető videó";
"(watched hidden)" = "(megtekintettek rejtve)";
"(shorts hidden)" = "(rövidek rejtve)";
"Disable filters" = "Szűrők kikapcsolása";
"You need to create an instance and accounts\nto access %@ section" = "Létre kell hoznia egy példányt és fiókokat\na(z) %@ szakasz eléréséhez";
"You can switch between profiles in playback settings controls." = "A lejátszási beállítások vezérlőiben válthat a profilok között.";
"Share files from Finder on a Mac\nor iTunes on Windows" = "Fájlok megosztása a Finderből Macen\nvagy iTunes használatával Windowson";
"Open logs in Finder" = "Naplók megnyitása a Finderben";
"Could not open playlist" = "Nem sikerült megnyitni a lejátszási listát";
"Now Playing" = "Jelenleg lejátszás alatt";
"Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "A lejátszási lista üres\n\nKoppintson és tartsa lenyomva egy videóra, majd\n„Hozzáadás a lejátszási listához”";
"Could not create share link" = "Nem sikerült létrehozni a megosztási hivatkozást";
"If you want this app to be available in your language, join translation project." = "Ha azt szeretné, hogy ez az alkalmazás a saját nyelvén is elérhető legyen, csatlakozzon a fordítási projekthez.";
"You need to select an account\nto access %@ section" = "Ki kell választania egy fiókot\na(z) %@ szakasz eléréséhez";
"Public" = "Nyilvános";
"%@ formats" = "%@ formátumok";
"Press and hold remote button to open captions and quality menus" = "Nyomja meg és tartsa lenyomva a távvezérlő gombot a feliratok és a minőségi menük megnyitásához";
"Could not extract channel information" = "Nem sikerült csatornainformációkat kinyerni";
"Could not refresh Playlists" = "Nem sikerült frissíteni a lejátszási listákat";
"Cached time" = "Gyorsítótárazott idő";
"No locations available at the moment" = "Jelenleg nincsenek elérhető helyek";
"Share Logs..." = "Naplók megosztása…";
"For custom locations you can configure Frontend URL in Locations settings" = "Egyéni helyekhez az előtétprogram webcímét a Helyek menüpontban konfigurálhatja";
"Shorts" = "Rövidek";
"Mark channel feed as unwatched" = "Jelölje meg a csatorna hírfolyamot nem megtekintettként";
"Tap and hold channel thumbnail to open context menu with more actions" = "Koppintson és tartsa lenyomva a csatorna miniatűrjét a további műveletekhez tartozó kontextusmenü megnyitásához";
"Controls button: backwards" = "Vezérlőgomb: hátra";
"Enter account credentials to connect..." = "Adja meg a fiók hitelesítő adatait a kapcsolódáshoz...";
"Show scroll to top button in comments" = "Görgetés a tetejére gomb megjelenítése a hozzászólásokban";
"Browse without account" = "Böngészés fiók nélkül";
"Watched: visible" = "Megtekintettek: láthatóak";
"Paste" = "Beillesztés";
"Playback history is empty" = "A lejátszási előzmények üresek";
"Right" = "Jobb";
"Gesture: fowards" = "Gesztus: előre";
"Gesture settings control skipping interval for double click on left/right side of the player. Changing system controls settings requires restart." = "A gesztusbeállítások vezérlik a kihagyási időközt a lejátszó bal/jobb oldalán történő dupla kattintás esetén. A rendszervezérlők beállításainak módosítása újraindítást igényel.";
"Open channel" = "Csatorna megnyitása";
"Inspector" = "Felügyelő";
"Copy%@link" = "%@ hivatkozás másolása";
"Recent History" = "Legutóbbi előzmények";
"Show icons and text when space permits" = "Ikonok és szöveg megjelenítése, ha van elegendő hely";
"Show Open Videos toolbar button" = "Nyitott videók eszköztár gombjának megjelenítése";
"Channels" = "Csatornák";
"Video" = "Videó";
"Open video description expanded" = "Videó leírásának bővített megjelenítése";
"Other data include last used playback preferences and listing options" = "Egyéb adatok közé tartoznak az utoljára használt lejátszási beállítások és listázási lehetőségek";
"Could not open Files" = "Nem sikerült a fájlokat megnyitni";
"Open Files" = "Fájlok megnyitása";
"File" = "Fájl";
"Open Videos" = "Videók megnyitása";
"Share" = "Megosztás";
"Play all unwatched" = "Összes nem megtekintettek lejátszása";
"Available" = "Elérhető";
"Gesture settings control skipping interval for double tap gesture on left/right side of the player. Changing system controls settings requires restart." = "A gesztusbeállítások vezérlik a kihagyási időközt a lejátszó bal/jobb oldalán végzett kétszeri érintéses gesztus számára. A rendszervezérlők beállításainak módosítása újraindítást igényel.";
"Pages buttons" = "Oldalak gombjai";
"Only for local files and URLs" = "Csak helyi fájlok és webcímek esetén";
"Are you sure you want to remove this document?" = "Biztosan törölni szeretné ezt a dokumentumot?";
"Rotate when entering fullscreen on landscape video" = "Forduljon el fekvő módba teljes képernyőre váltáskor";
"Limit" = "Limit";
"Are you sure you want to remove %@ from Favorites?" = "Biztosan törölni szeretné a(z) %@-t a „Kedvencek” közül?";
"Keep channels with unwatched videos on top of subscriptions list" = "A nem megtekintett videókkal rendelkező csatornák a feliratkozási lista tetején maradnak";
"Show video context menu options to force selected backend" = "Videó kontextusmenü beállításainak megjelenítése a kiválasztott háttérprogram kikényszerítéséhez";
"Play Now in AVPlayer" = "Lejátszás az AVPlayer-ben";
"Play Now in MPV" = "Lejátszás az MPV-ben";
"Show channel avatars in videos lists" = "Csatorna profilképek megjelenítése a videók listájában";
"Export" = "Exportálás";
"File information" = "Fájlinformáció";
"Import" = "Importálás";
"Platform" = "Platform";
"Action button labels" = "Művelet gombok feliratai";
"Build" = "Összeállítási szám";
"Icon and text" = "Ikon és szöveg";
"Custom Location already exists" = "Az egyéni hely már létezik";
"Account already exists" = "A fiók már létezik";
"Export in progress..." = "Exportálás folyamatban...";
"In progress..." = "Folyamatban…";
"Open vertical chapters expanded" = "Függőleges fejezetek megnyitása kibővítve";
"Icon only" = "Csak ikon";
"Podcasts" = "Podcastok";
"Releases" = "Kiadások";
"Description preview" = "Leírás előnézete";
"Chapters (if available)" = "Fejezetek (ha elérhetőek)";
"Are you sure you want to export unencrypted passwords?" = "Biztosan titkosítatlan jelszavakat szeretne exportálni?";
"No preview" = "Nincs előnézet";
"Accounts passwords (unencrypted)" = "Fiókok jelszavai (titkosítatlanul)";
"Other" = "Egyéb";
"Other data" = "Egyéb adat";
"Do not share this file with anyone or you can lose access to your accounts. If you don't select to export passwords you will be asked to provide them during import" = "Ne ossza meg ezt a fájlt senkivel, különben elveszítheti hozzáférését a fiókjaihoz. Ha nem választja a jelszavak exportálását, akkor az importálás során meg kell adnia azokat";
"Password required to import" = "Jelszó szükséges az importáláshoz";
"Custom Location selected for import" = "Az importáláshoz kiválasztott egyéni hely";
"Password saved in import file" = "Az importfájlba mentett jelszó";
"Export..." = "Exportálás…";
"Show channel avatars in channels lists" = "Csatorna profilképek megjelenítése a csatornák listájában";
"Import Settings..." = "Beállítások importálása...";
"Custom Location not selected for import" = "Nincs kiválasztva egyéni hely az importáláshoz";
"Export Settings" = "Beállítások exportálása";
"Add %@" = "%@ hozzáadása";

View File

@ -1,98 +0,0 @@
"Accounts" = "Imiḍanen";
"Chapters" = "Ixfawen";
"Clear" = "Sfeḍ";
"Close Video" = "Mdel tavidyutt";
"Donations" = "Mudd tawsa";
"Error" = "Tuccḍa";
"Help" = "Tallalt";
"Movies" = "Isura";
"Music" = "Aẓawan";
"Profiles" = "Imaɣnuyen";
"Search" = "Nadi";
"Search..." = "Nadi...";
"Settings" = "Iɣewwaṛen";
"TV" = "Tiliẓri";
"Year" = "Aseggas";
"Size" = "Tiddi";
"Address" = "Tansa";
"Channel" = "Abadu";
"Export..." = "Sifeḍ…";
"Advanced" = "Talqayt";
"All" = "Akk";
"Anonymous" = "Udrig";
"Any" = "Menwala";
"Automatic" = "S wudem awurman";
"Battery" = "Aẓru";
"Blue" = "Azegzaw";
"Button" = "Taqeffalt";
"Cancel" = "Sefsex";
"Category" = "Taggayt";
"Close" = "Mdel";
"Contact" = "Anermes";
"Comments" = "Iwenniten";
"Country" = "Tamurt";
"Continue" = "Ddu";
"Date" = "Azemz";
"Duration" = "Tanzagt";
"Delete" = "Kkes";
"Done" = "Immed";
"Edit" = "Ẓreg";
"Favorites" = "Imenyafen";
"Filter" = "Imzizdig";
"History" = "Amazray";
"Hour" = "Asrag";
"Info" = "Talɣut";
"Interface" = "Agrudem";
"Month" = "Ayyur";
"Name" = "Isem";
"Password" = "Awal n uεeddi";
"Edit..." = "Ẓreg...";
"Quality" = "Taɣara";
"Red" = "Azeggaɣ";
"Remove" = "Kkes";
"Replies" = "Tiririyin";
"Reset" = "Wennez";
"Restart" = "Ales asenker";
"Save" = "Sekles";
"Share..." = "Bḍu…";
"Source" = "Aɣbalu";
"unknown" = "arussin";
"URL" = "URL";
"Username" = "Isem n useqdac";
"Today" = "Ass-a";
"Videos" = "Tividyutin";
"Views" = "Timezriyin";
"Week" = "Amalas";
"Wiki" = "Wiki";
"Wi-Fi" = "Wi-Fi";
"Welcome" = "Ansuf";
"Yattee" = "Yattee";
"Statistics" = "Tidaddanin";
"Home" = "Tazwara";
"Hide" = "Ffer-it";
"Translations" = "Tisuqilin";
"Files" = "Ifuyla";
"Open" = "Ldi";
"Add" = "Rnu";
"Paste" = "Senṭeḍ";
"Channels" = "Ibuda";
"Share" = "Bḍu";
"Video" = "Tavidyutt";
"Documents" = "Isemliyen";
"Codec" = "Akudak";
"Audio" = "Imesli";
"File" = "Afaylu";
"FPS" = "FPS";
"Description" = "Aglam";
"Remove…" = "Kkes…";
"List" = "Tabdart";
"Export" = "Sifeḍ";
"Import" = "Kter";
"Platform" = "Tiɣerɣert";
"Add Account" = "Rnu amiḍan";
"Add Account..." = "Rnu amiḍan…";
"Add profile..." = "Rnu amaɣnu…";
"Clear History" = "Sfeḍ amazray";
"Discord Server" = "Aqeddac Discord";

View File

@ -68,20 +68,3 @@
"Close PiP and open player when application enters foreground" = "애플리케이션이 포그라운드에 진입하면 PiP를 닫고 플레이어를 열기"; "Close PiP and open player when application enters foreground" = "애플리케이션이 포그라운드에 진입하면 PiP를 닫고 플레이어를 열기";
"Close PiP when player is opened" = "플레이어가 열리면 PiP 닫기"; "Close PiP when player is opened" = "플레이어가 열리면 PiP 닫기";
"Close PiP when starting playing other video" = "다른 동영상 재생을 시작하면 PiP 닫기"; "Close PiP when starting playing other video" = "다른 동영상 재생을 시작하면 PiP 닫기";
"Error when accessing playlist" = "플레이리스트 연결 도중 오류 발생";
"Hide sidebar" = "사이드바 숨기기";
"Close video after playing last in the queue" = "마지막 동영상 재생 후 , 영상 닫기";
"Comments" = "댓글";
"Connection failed" = "연결 실패";
"Contact" = "연락처";
"Create Playlist" = "재생목록 생성";
"Donations" = "후원";
"Done" = "완료";
"Don't use public locations" = "공공장소에서 사용하지 마십시오";
"I have a feature request" = "기능 제안하기";
"I like this app!" = "저는 이 앱이 좋습니다!";
"I want to ask a question" = "질문하기";
"Just watched" = "방금 시청한 동영상";
"Mark as watched" = "시청 완료로 표시하기";
"Mark video as watched after playing" = "동영상 시청 후에 시청 완료 표시하기";
"Continue" = "다음";

View File

@ -405,75 +405,75 @@
"No chapters information available" = "Ingen tilgjengelig kapittelinfo"; "No chapters information available" = "Ingen tilgjengelig kapittelinfo";
"Comments are disabled" = "Kommentarer er avskrudd"; "Comments are disabled" = "Kommentarer er avskrudd";
"Press and hold remote button to open captions and quality menus" = "Trykk og hold fjernknappen for å åpne meny for undertekster og kvalitet"; "Press and hold remote button to open captions and quality menus" = "Trykk og hold fjernknappen for å åpne meny for undertekster og kvalitet";
"Paste" = "Lim"; "Paste" = "";
"Codec" = "Kodeks"; "Codec" = "";
"Open Videos" = "Åpne Videoer"; "Open Videos" = "";
"Files" = "Filer"; "Files" = "";
"Open Video" = "Åpne Video"; "Open Video" = "";
"Show only icons" = "Vis kun ikoner"; "Show only icons" = "";
"Show Open Videos toolbar button" = "Vis Åpne Videoer verktøylinje knapp"; "Show Open Videos toolbar button" = "";
"Channels" = "Kanaler"; "Channels" = "";
"Buttons labels" = "Knappe ettiketter"; "Buttons labels" = "";
"Could not open Files" = "Kunne ikke åpne fil"; "Could not open Files" = "";
"Reload manifest" = "Last manifest på nytt"; "Reload manifest" = "";
"Right" = "Høyre"; "Right" = "";
"Show Favorites" = "Vis Favoritter"; "Show Favorites" = "";
"Only for local files and URLs" = "Kun for lokale filer og URLer"; "Only for local files and URLs" = "";
"Enter link to open" = "Gå inn i link for å åpne"; "Enter link to open" = "";
"Left" = "Venstre"; "Left" = "";
"Are you sure you want to remove this document?" = "Ønsker du virkelig å fjerne dette dokumentet"; "Are you sure you want to remove this document?" = "";
"Recent Documents" = "Nylige Dokumenter"; "Recent Documents" = "";
"Share files from Finder on a Mac\nor iTunes on Windows" = "Del filer fra Finder på Mac\neller iTunes på Windows"; "Share files from Finder on a Mac\nor iTunes on Windows" = "";
"Address" = "Addresse"; "Address" = "";
"File" = "Fil"; "File" = "";
"Share" = "Del"; "Share" = "";
"Could not delete document" = "Klarte ikke å slette dokumentet"; "Could not delete document" = "";
"Are you sure you want to remove %@ location?" = "Er du sikker på at du ønsker å fjerne %@ lokalisjon?"; "Are you sure you want to remove %@ location?" = "";
"Size" = "Størrelse"; "Size" = "";
"Always" = "Alltid"; "Always" = "";
"Video actions buttons" = "Knapper for video valg"; "Video actions buttons" = "";
"Edit Favorites…" = "Rediger Favoritter..."; "Edit Favorites…" = "";
"Sample Rate" = "Stikkprøve rate"; "Sample Rate" = "";
"Show Inspector" = "Vis inspektør"; "Show Inspector" = "";
"Remove Location" = "Fjern lokasjon"; "Remove Location" = "";
"Format" = "Format"; "Format" = "";
"Verified" = "Verifisert"; "Verified" = "";
"Show icons and text when space permits" = "Vis ikoner og tekst når det er plass"; "Show icons and text when space permits" = "";
"Could not extract video ID" = "Kunne ikke pakke ut video-ID"; "Could not extract video ID" = "Kunne ikke pakke ut video-ID";
"Open Files" = "Åpne Filer"; "Open Files" = "";
"Driver" = "Driver"; "Driver" = "";
"Show Open Videos quick actions" = "Vis hurtigvalg for åpning av Videoer"; "Show Open Videos quick actions" = "";
"Enter links to open, one per line" = "Legg til lenker som skal åpnes, en per linje"; "Enter links to open, one per line" = "";
"No locations available at the moment" = "Ingen lokasjoner tilgjengelig for øyeblikket"; "No locations available at the moment" = "";
"Video Details" = "Video detaljer"; "Video Details" = "";
"Add" = "Legg til"; "Add" = "";
"Show Home" = "Vis Hjem"; "Show Home" = "";
"Pages buttons" = "Side knapper"; "Pages buttons" = "";
"Center" = "Senter"; "Center" = "";
"Shorts" = "Shorts"; "Shorts" = "";
"Open" = "Åpne"; "Open" = "";
"Locations Manifest" = "Lokasjonsmanifest"; "Locations Manifest" = "";
"FPS" = "FPS"; "FPS" = "";
"Inspector visibility" = "Inspektør synlighet"; "Inspector visibility" = "";
"Show Documents" = "Vis Dokumenter"; "Show Documents" = "";
"Open logs in Finder" = "Åpne loggføring i Finder"; "Open logs in Finder" = "Åpne loggføring i Finder";
"Documents" = "Dokumenter"; "Documents" = "";
"Could not update your token." = "Kunne ikke oppdatere symbolet ditt."; "Could not update your token." = "Kunne ikke oppdatere symbolet ditt.";
"Remove…" = "Fjern"; "Remove…" = "";
"Hide" = "Gjem"; "Hide" = "";
"Actions buttons" = "Handlings knapper"; "Actions buttons" = "";
"Audio" = "Lyd"; "Audio" = "";
"Could not extract SID from received cookies: %@" = "Kunne ikke hente ut SID fra mottatte informasjonskapsler: %@"; "Could not extract SID from received cookies: %@" = "Kunne ikke hente ut SID fra mottatte informasjonskapsler: %@";
"Playback Mode" = "Avspilling Modus"; "Playback Mode" = "";
"Clear Queue before opening" = "Tøm kø før åpning"; "Clear Queue before opening" = "";
"Could not create share link" = "Kunne ikke opprette delingslenke"; "Could not create share link" = "Kunne ikke opprette delingslenke";
"Could not refresh Playlists" = "Kunne ikke gjenoppfriske spillelister"; "Could not refresh Playlists" = "";
"Could not refresh Subscriptions" = "Kunne ikke gjenoppfriske abonnementer"; "Could not refresh Subscriptions" = "Kunne ikke gjenoppfriske abonnementer";
"Translations" = "Oversettelser"; "Translations" = "";
"This URL could not be opened" = "Denne URL kunne ikke åpnes"; "This URL could not be opened" = "";
"For custom locations you can configure Frontend URL in Locations settings" = "For tilpassede lokasjoner kan du konfigurere Frontend URL i lokasjons instillinger"; "For custom locations you can configure Frontend URL in Locations settings" = "";
"Could not refresh Trending" = "Kunne ikke gjenoppfriske trendende"; "Could not refresh Trending" = "";
"If you want this app to be available in your language, join translation project." = "Hvis du ønsker denne appen tilgjengelig på ditt språk, bli med i oversettelses prosjektet"; "If you want this app to be available in your language, join translation project." = "";
"This video could not be opened" = "Kunne ikke åpne videoen"; "This video could not be opened" = "Kunne ikke åpne videoen";
"Could not open channel" = "Kunne ikke åpne kanal"; "Could not open channel" = "Kunne ikke åpne kanal";
"Could not open playlist" = "Kunne ikke åpne spilleliste"; "Could not open playlist" = "Kunne ikke åpne spilleliste";
@ -482,21 +482,21 @@
"Could not extract channel information" = "Kunne ikke hente kanalinfo"; "Could not extract channel information" = "Kunne ikke hente kanalinfo";
"Could not load video" = "Kunne ikke laste inn video"; "Could not load video" = "Kunne ikke laste inn video";
"Could not extract playlist ID" = "Kunne ikke hente ut spilleliste-ID"; "Could not extract playlist ID" = "Kunne ikke hente ut spilleliste-ID";
"Could not refresh Popular" = "Kunne ikke gjenoppfriske populært"; "Could not refresh Popular" = "";
"Channel could not be found" = "Fant ikke kanalen"; "Channel could not be found" = "Fant ikke kanalen";
"Live Streams" = "Direkte strømmer"; "Live Streams" = "";
"Channel" = "Kanal"; "Channel" = "";
"No documents" = "Ingen dokumenter"; "No documents" = "";
"\"%@\" will be irreversibly removed from this device." = "\"%@\" vil irreversibelt bli fjernet fra denne enheten"; "\"%@\" will be irreversibly removed from this device." = "";
"Recent History" = "Nylig Historie"; "Recent History" = "";
"Home" = "Hjem"; "Home" = "";
"Pages toolbar position" = "Verktøylinjeposisjon for sider"; "Pages toolbar position" = "";
"URL to Open" = "Åpne URL"; "URL to Open" = "";
"Video" = "Video"; "Video" = "";
"Could not find any links to open in your clipboard" = "Kunne ikke finnen noen linker å åpne i utklippstavlen"; "Could not find any links to open in your clipboard" = "";
"Show sidebar" = "Vis sidebar"; "Show sidebar" = "";
"Default Profile" = "Standard profil"; "Default Profile" = "";
"Playback history is empty" = "Avspillingshisotrikk er tom"; "Playback history is empty" = "";
"Copy%@link" = "Kopier%@lenke"; "Copy%@link" = "";
"Share%@link" = "Del%@lenke"; "Share%@link" = "";
"Share Logs..." = "Del logger …"; "Share Logs..." = "Del logger …";

View File

@ -9,7 +9,7 @@
"Add Account..." = "Добавить аккаунт..."; "Add Account..." = "Добавить аккаунт...";
"Add Location" = "Добавить локацию"; "Add Location" = "Добавить локацию";
"Add profile..." = "Добавить профиль..."; "Add profile..." = "Добавить профиль...";
"Add to %@" = "Добавить к «%@»"; "Add to %@" = "Добавить к %@";
"Add to Favorites" = "Добавить в избранное"; "Add to Favorites" = "Добавить в избранное";
"Add to Playlist" = "Добавить в плейлист"; "Add to Playlist" = "Добавить в плейлист";
"Anonymous" = "Анонимный"; "Anonymous" = "Анонимный";

View File

@ -1,564 +0,0 @@
"%@ Channel" = "%@ சேனல்";
"%@ Playlist" = "%@ பிளேலிச்ட்";
"%@ subscribers" = "%@ சந்தாதாரர்கள்";
"Accounts" = "கணக்குகள்";
"Accounts are not supported for the application of this instance" = "இந்த நிகழ்வின் பயன்பாட்டிற்கு கணக்குகள் ஆதரிக்கப்படவில்லை";
"Add Account" = "கணக்கைச் சேர்க்கவும்";
"Add Location" = "இருப்பிடத்தைச் சேர்க்கவும்";
"Add Location..." = "இருப்பிடத்தைச் சேர்க்கவும் ..";
"Add profile..." = "சுயவிவரத்தைச் சேர்க்கவும் ...";
"Add Quality Profile" = "தரமான சுயவிவரத்தைச் சேர்க்கவும்";
"Add to %@" = "%@ இல் சேர்க்கவும்";
"Add to Favorites" = "பிடித்தவைகளில் சேர்க்கவும்";
"Add to Playlist" = "பிளேலிச்ட்டில் சேர்க்கவும்";
"Add to Playlist..." = "பிளேலிச்ட்டில் சேர்க்கவும் ...";
"Advanced" = "மேம்பட்ட";
"All" = "அனைத்தும்";
"Always use AVPlayer for live videos" = "நேரடி வீடியோக்களுக்கு எப்போதும் AVPlayer ஐப் பயன்படுத்துங்கள்";
"Any" = "ஏதேனும்";
"Apply to all" = "அனைவருக்கும் பொருந்தும்";
"Create Playlist" = "பிளேலிச்ட்டை உருவாக்கவும்";
"Current: %@\n%@" = "நடப்பு: %@\n %@";
"Custom" = "தனிப்பயன்";
"Custom Locations" = "தனிப்பயன் இடங்கள்";
"Date" = "திகதி";
"Decrease rate" = "வீதத்தைக் குறைக்கவும்";
"Delete" = "அழி";
"Disabled" = "முடக்கப்பட்டது";
"Discord Server" = "முரண்பாடு சேவையகம்";
"Discussions take place in Discord and Matrix. It's a good spot for general questions." = "டிச்கார்ட் மற்றும் மேட்ரிக்சில் விவாதங்கள் நடைபெறுகின்றன. பொதுவான கேள்விகளுக்கு இது ஒரு நல்ல இடம்.";
"Don't use public locations" = "பொது இடங்களைப் பயன்படுத்த வேண்டாம்";
"Edit" = "தொகு";
"Edit Playlist" = "பிளேலிச்ட்டைத் திருத்து";
"Edit Quality Profile" = "தரமான சுயவிவரத்தைத் திருத்தவும்";
"Enable logging" = "பதிவை இயக்கவும்";
"Enable Return YouTube Dislike" = "YouTube வெறுப்பைத் திரும்பவும்";
"Error" = "பிழை";
"Error when accessing playlist" = "பிளேலிச்ட்டை அணுகும்போது பிழை";
"Save history of searches, channels and playlists" = "தேடல்கள், சேனல்கள் மற்றும் பிளேலிச்ட்களின் வரலாற்றைச் சேமிக்கவும்";
"Search" = "தேடல்";
"Search history is empty" = "தேடல் வரலாறு காலியாக உள்ளது";
"Sections" = "பிரிவுகள்";
"Seek with horizontal swipe on video" = "வீடியோவில் கிடைமட்ட ச்வைப் கொண்டு தேடுங்கள்";
"Matrix Channel" = "அணி சேனல்";
"Matrix Chat" = "அணி அரட்டை";
"Lock portrait mode" = "பூட்டு உருவப்படம் பயன்முறை";
"Long" = "நீண்ட";
"Low" = "குறைந்த";
"Low quality" = "குறைந்த தகுதி";
"Lowest" = "மிகக் குறைந்த";
"Mark as watched" = "பார்த்தபடி குறி";
"Mark video as watched after playing" = "விளையாடிய பிறகு பார்த்தபடி வீடியோவை குறிக்கவும்";
"Medium" = "சராசரி";
"Medium quality" = "நடுத்தர தகுதி";
"More info can be found in:" = "மேலும் தகவலைக் காணலாம்:";
"MPV Documentation" = "எம்.பி.வி ஆவணம்";
"Open \"Playlists\" tab to create new one" = "புதிய ஒன்றை உருவாக்க \"பிளேலிச்ட்கள்\" தாவலைத் திறக்கவும்";
"Open Settings" = "திறந்த அமைப்புகள்";
"Music" = "இசை";
"Name" = "பெயர்";
"Next" = "அடுத்தது";
"No Playlists" = "பிளேலிச்ட்கள் இல்லை";
"No results" = "முடிவுகள் இல்லை";
"Normal" = "சாதாரண";
"Not available" = "கிடைக்கவில்லை";
"Not Playing" = "விளையாடுவதில்லை";
"Nothing" = "எதுவும்";
"Offtopic in Music Videos" = "மியூசிக் வீடியோக்களில் ஓப்டோபிக்";
"Opening %@ stream…" = "திறத்தல் %@ ச்ட்ரீம்…";
"Outro" = "மற்றொன்று";
"Reset" = "மீட்டமை";
"Reset watched status when playing again" = "மீண்டும் விளையாடும்போது மீட்டெடுக்கப்பட்ட நிலையை மீட்டமை";
"Restart" = "மறுதொடக்கம்";
"Restart the app to apply the settings above." = "மேலே உள்ள அமைப்புகளைப் பயன்படுத்த பயன்பாட்டை மறுதொடக்கம் செய்யுங்கள்.";
"Restart/Play next" = "அடுத்து மறுதொடக்கம்/விளையாடுங்கள்";
"Restore default profiles..." = "இயல்புநிலை சுயவிவரங்களை மீட்டெடுங்கள் ...";
"Add Channels, Playlists and Searches to Favorites using" = "பயன்படுத்தப்பட்ட பிடித்தவைகளில் சேனல்கள், பிளேலிச்ட்கள் மற்றும் தேடல்களைச் சேர்க்கவும்";
"Playing Next" = "அடுத்து விளையாடுவது";
"You can switch between profiles in playback settings controls." = "பிளேபேக் அமைப்புகள் கட்டுப்பாடுகளில் சுயவிவரங்களுக்கு இடையில் நீங்கள் மாறலாம்.";
"Current Playlist" = "தற்போதைய பிளேலிச்ட்";
"Statistics" = "புள்ளிவிவரங்கள்";
"Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "பிளேலிச்ட் காலியாக உள்ளது\n\n ஒரு வீடியோவைத் தட்டவும் பிடிக்கவும்\n \"பிளேலிச்ட்டில் சேர்\"";
"It can be changed later in settings. You can use your own locations too." = "அதை பின்னர் அமைப்புகளில் மாற்றலாம். உங்கள் சொந்த இடங்களையும் பயன்படுத்தலாம்.";
"Hardware decoder" = "வன்பொருள் டிகோடர்";
"Stream FPS" = "ச்ட்ரீம் எஃப்.பி.எச்";
"Rate & Captions" = "விகிதம் மற்றும் தலைப்புகள்";
"Dropped frames" = "கைவிடப்பட்ட பிரேம்கள்";
"Any format" = "எந்த வடிவமும்";
"%@ formats" = "%@ வடிவங்கள்";
"Keep last played video in the queue after restart" = "மறுதொடக்கம் செய்த பிறகு வரிசையில் கடைசியாக விளையாடிய வீடியோவை வைத்திருங்கள்";
"Press and hold remote button to open captions and quality menus" = "தலைப்புகள் மற்றும் தர மெனுக்களைத் திறக்க தொலை பொத்தானை அழுத்திப் பிடிக்கவும்";
"Comments are disabled" = "கருத்துகள் முடக்கப்பட்டுள்ளன";
"No comments" = "கருத்துகள் இல்லை";
"Share Logs..." = "பதிவுகளைப் பகிரவும்…";
"Open logs in Finder" = "கண்டுபிடிப்பாளரில் திறந்த பதிவுகள்";
"Rotate to portrait when exiting fullscreen" = "முழுத்திரை வெளியேறும்போது உருவப்படத்திற்கு சுழல்க";
"Round corners" = "சுற்று மூலைகள்";
"Save history of played videos" = "விளையாடிய வீடியோக்களின் வரலாற்றைச் சேமிக்கவும்";
"Could not refresh Subscriptions" = "சந்தாக்களை புதுப்பிக்க முடியவில்லை";
"Could not load streams" = "ச்ட்ரீம்களை ஏற்ற முடியவில்லை";
"Could not open video" = "வீடியோவை திறக்க முடியவில்லை";
"Channel could not be found" = "சேனலைக் கண்டுபிடிக்க முடியவில்லை";
"Could not extract channel information" = "சேனல் தகவல்களைப் பிரித்தெடுக்க முடியவில்லை";
"Could not extract SID from received cookies: %@" = "பெறப்பட்ட குக்கீகளிலிருந்து SID ஐ பிரித்தெடுக்க முடியவில்லை: %@";
"Could not update your token." = "உங்கள் கிள்ளாக்கைப் புதுப்பிக்க முடியவில்லை.";
"Enter links to open, one per line" = "திறக்க இணைப்புகளை உள்ளிடவும், ஒரு வரிக்கு ஒன்று";
"Playback Mode" = "பிளேபேக் பயன்முறை";
"Hide" = "மறை";
"Always" = "எப்போதும்";
"Format" = "வடிவம்";
"Driver" = "இயக்கி";
"Only for local files and URLs" = "உள்ளக கோப்புகள் மற்றும் முகவரி களுக்கு மட்டுமே";
"Right" = "வலது";
"Channels" = "சேனல்கள்";
"Show icons and text when space permits" = "விண்வெளி அனுமதிக்கும்போது சின்னங்களையும் உரையையும் காட்டுங்கள்";
"Show only icons" = "ஐகான்களை மட்டும் காட்டு";
"Audio" = "ஆடியோ";
"File" = "கோப்பு";
"Video" = "ஒளிதோற்றம்";
"Codec" = "புரிப்பு";
"Size" = "அளவு";
"FPS" = "Fps";
"Sample Rate" = "மாதிரி வீதம்";
"Could not find any links to open in your clipboard" = "உங்கள் கிளிப்போர்டில் திறக்க எந்த இணைப்புகளையும் கண்டுபிடிக்க முடியவில்லை";
"Address" = "முகவரி";
"Remove…" = "அகற்று…";
"Actions buttons" = "செயல்கள் பொத்தான்கள்";
"Show sidebar" = "பக்கப்பட்டியைக் காட்டு";
"Locations Manifest" = "இருப்பிடங்கள் வெளிப்படுகின்றன";
"Remove Location" = "இருப்பிடத்தை அகற்று";
"Default Profile" = "இயல்புநிலை சுயவிவரம்";
"Playback history is empty" = "பின்னணி வரலாறு காலியாக உள்ளது";
"Copy%@link" = "நகலெடு%@இணைப்பு";
"Are you sure you want to remove this document?" = "இந்த ஆவணத்தை அகற்ற விரும்புகிறீர்களா?";
"\"%@\" will be irreversibly removed from this device." = "\"%@\" இந்த சாதனத்திலிருந்து மாற்றமுடியாமல் அகற்றப்படும்.";
"Are you sure you want to remove %@ location?" = "%@ இருப்பிடத்தை அகற்ற விரும்புகிறீர்களா?";
"Shorts" = "குறுக்குகள்";
"Verified" = "சரிபார்க்கப்பட்டது";
"Channel" = "வாய்க்கால்";
"Open expanded" = "திறந்த விரிவாக்கப்பட்டது";
"Mark channel feed as unwatched" = "சேனல் தீவனத்தை கவனக்குறைவாகக் குறிக்கவும்";
"Mark channel feed as watched" = "பார்த்தபடி சேனல் ஊட்டத்தைக் குறிக்கவும்";
"Short videos: visible" = "குறுகிய வீடியோக்கள்: தெரியும்";
"Short videos: hidden" = "குறுகிய வீடியோக்கள்: மறைக்கப்பட்டுள்ளன";
"Play all unwatched" = "எல்லாவற்றையும் கவனிக்காமல் விளையாடுங்கள்";
"Double tap gesture" = "இரட்டை குழாய் சைகை";
"Tap and hold channel thumbnail to open context menu with more actions" = "மேலும் செயல்களுடன் சூழல் மெனுவைத் திறக்க சேனல் சிறுபடத்தைத் தட்டி வைத்திருங்கள்";
"Single tap gesture" = "ஒற்றை குழாய் சைகை";
"Mark all as unwatched" = "அனைத்தையும் கவனக்குறைவாகக் குறிக்கவும்";
"Queue - shuffled" = "வரிசை - மாற்றப்பட்டது";
"Playback Settings" = "பின்னணி அமைப்புகள்";
"Replay" = "மீண்டும்";
"Fullscreen" = "முழு திரை";
"Description" = "விவரம்";
"Loop one" = "லூப் ஒன்";
"Autoplay next" = "ஆட்டோ பிளே அடுத்து";
"Stream" = "ச்ட்ரீம்";
"Enter account credentials to connect..." = "இணைக்க கணக்கு நற்சான்றிதழ்களை உள்ளிடவும் ...";
"Enter location address to connect..." = "இணைக்க இருப்பிட முகவரியை உள்ளிடவும் ...";
"Seek" = "தேடுங்கள்";
"Opened File" = "திறந்த கோப்பு";
"File Extension" = "கோப்பு நீட்டிப்பு";
"Opening file…" = "கோப்பைத் திறக்கும்…";
"Public account" = "பொது கணக்கு";
"Your Accounts" = "உங்கள் கணக்குகள்";
"Browse without account" = "கணக்கு இல்லாமல் உலாவுக";
"Close video and player on end" = "முடிவில் வீடியோ மற்றும் பிளேயரை மூடு";
"Use system controls with AVPlayer" = "AVPlayer உடன் கணினி கட்டுப்பாடுகளைப் பயன்படுத்தவும்";
"Rotate when entering fullscreen on landscape video" = "நிலப்பரப்பு வீடியோவில் முழுத்திரை நுழையும்போது சுழற்றுங்கள்";
"Landscape right" = "இயற்கை சரியானது";
"No rotation" = "சுழற்சி இல்லை";
"Available" = "கிடைக்கிறது";
"Startup section" = "தொடக்க பிரிவு";
"Home Settings" = "வீட்டு அமைப்புகள்";
"Watched: hidden" = "பார்த்தது: மறைக்கப்பட்டுள்ளது";
"(watched and shorts hidden)" = "(பார்த்த மற்றும் குறும்படங்கள் மறைக்கப்பட்டுள்ளன)";
"No videos to show" = "காட்ட வீடியோக்கள் இல்லை";
"(watched hidden)" = "(மறைக்கப்பட்டிருப்பதைப் பார்த்தேன்)";
"(shorts hidden)" = "(சார்ட்ச் மறைக்கப்பட்டுள்ளது)";
"Disable filters" = "வடிப்பான்களை முடக்கு";
"Limit" = "வரம்பு";
"Are you sure you want to remove %@ from Favorites?" = "பிடித்தவைகளிலிருந்து %@ ஐ அகற்ற விரும்புகிறீர்களா?";
"Smaller" = "மிகசிறிய";
"Clear all" = "அனைத்தையும் அழி";
"URL" = "இணையமுகவரி";
"Badge" = "பதக்கம்";
"Badge & Decreased opacity" = "பதக்கம் மற்றும் ஒளிபுகாநிலை குறைவு";
"Show unwatched feed badges" = "எடுக்கப்படாத தீவன பதக்ங்களைக் காட்டு";
" subscribers" = " சந்தாதாரர்கள்";
"%lld videos" = "%எல்.எல்.டி வீடியோக்கள்";
"10 seconds forwards/backwards" = "10 வினாடிகள் முன்னோக்கி/பின்தங்கிய";
"Add Account..." = "கணக்கைச் சேர்க்கவும் ...";
"Anonymous" = "அநாமதேய";
"Autoplaying Next" = "அடுத்ததாக ஆட்டோபிளேயிங்";
"Based on system color scheme" = "கணினி வண்ணத் திட்டத்தின் அடிப்படையில்";
"Battery" = "மின்கலம்";
"Button" = "பொத்தான்";
"Clear" = "தெளிவான";
"Clear Search History..." = "தேடல் வரலாற்றை அழிக்கவும் ...";
"Close" = "மூடு";
"Close player when starting PiP" = "PIP ஐத் தொடங்கும்போது வீரர்";
"Close Video" = "வீடியோவை மூடு";
"Connection failed" = "இணைப்பு தோல்வியடைந்தது";
"Contributing" = "பங்களிப்பு";
"Country" = "நாடு";
"Country Name or Code" = "நாட்டின் பெயர் அல்லது குறியீடு";
"Decreased opacity" = "ஒளிபுகாநிலை குறைந்தது";
"Donations" = "நன்கொடைகள்";
"Done" = "முடிந்தது";
"Duration" = "காலம்";
"Edit..." = "திருத்து ...";
"Enter fullscreen in landscape" = "நிலப்பரப்பில் முழுத்திரை உள்ளிடவும்";
"Favorites" = "பிடித்தவை";
"Filter" = "வடிப்பி";
"For videos which feature music as the primary content." = "இசையை முதன்மை உள்ளடக்கமாக இடம்பெறும் வீடியோக்களுக்கு.";
"Frontend URL" = "ஃபிரான்டென்ட் முகவரி";
"Fullscreen size" = "முழுத்திரை அளவு";
"Help" = "உதவி";
"I like this app!" = "இந்த பயன்பாட்டை நான் விரும்புகிறேன்!";
"Increase rate" = "வீதத்தை அதிகரிக்கவும்";
"Info" = "தகவல்";
"Interaction" = "உள்வினை";
"Issues Tracker" = "டிராக்கரை வெளியிடுகிறது";
"Just watched" = "இப்போது பார்த்தேன்";
"LIVE" = "வாழ";
"Loading streams…" = "ச்ட்ரீம்களை ஏற்றுகிறது…";
"Mark watched videos with" = "மார்க் பார்த்த வீடியோக்கள்";
"Milestones" = "மைல்கற்கள்";
"Month" = "மாதம்";
"Movies" = "திரைப்படங்கள்";
"New Playlist" = "புதிய பிளேலிச்ட்";
"No description" = "விளக்கம் இல்லை";
"Only when signed in" = "கையொப்பமிடும்போது மட்டுமே";
"Opening audio stream…" = "ஆடியோ ச்ட்ரீமைத் திறக்கிறது…";
"Orientation" = "நோக்குநிலை";
"Password" = "கடவுச்சொல்";
"Playback" = "பின்னணி";
"Preferred Formats" = "விருப்பமான வடிவங்கள்";
"Profiles" = "சுயவிவரங்கள்";
"Rate" = "விகிதம்";
"Recents" = "அண்மைக் கால";
"Regular Size" = "வழக்கமான அளவு";
"Related" = "தொடர்புடைய";
"Relevance" = "பொருத்தமானது";
"Remove from history" = "வரலாற்றிலிருந்து அகற்று";
"Reset search filters" = "தேடல் வடிப்பான்களை மீட்டமைக்கவும்";
"Resolution" = "பகுத்தல்";
"Save" = "சேமி";
"Search..." = "தேடுங்கள் ...";
"Seek gesture sensitivity" = "சைகை உணர்திறனைத் தேடுங்கள்";
"Seek gesture speed" = "சைகை வேகத்தைத் தேடுங்கள்";
"Self-promotion" = "தன்வய ஊக்குவிப்பு";
"Share %@ link" = "பகிர்வு %@ இணைப்பு";
"Share %@ link with time" = "நேரத்துடன் %@ இணைப்பைப் பகிரவும்";
"Show history" = "வரலாற்றைக் காட்டு";
"Show playback statistics" = "பிளேபேக் புள்ளிவிவரங்களைக் காட்டு";
"Shuffle" = "கலக்கு";
"Sign In Required" = "தேவையான உள்நுழைவு";
"Sort" = "வரிசைப்படுத்து";
"Subscribe" = "குழுசேர்";
"Subscriptions" = "சந்தாக்கள்";
"Switch to other public location" = "பிற பொது இருப்பிடத்திற்கு மாறவும்";
"This cannot be reverted" = "இதை மாற்ற முடியாது";
"Thumbnails" = "சிறு உருவங்கள்";
"unknown" = "தெரியவில்லை";
"Unsubscribe" = "குழுவிலகவும்";
"Used to create links from videos, channels and playlists" = "வீடியோக்கள், சேனல்கள் மற்றும் பிளேலிச்ட்களிலிருந்து இணைப்புகளை உருவாக்க பயன்படுகிறது";
"Watched %@" = "பார்த்தது %@";
"Week" = "வாரம்";
"Yattee" = "யாட்டீ";
"Yattee %@ (build %@)" = "Yattee %@ (உருவாக்க %@)";
"You have no Playlists" = "உங்களிடம் பிளேலிச்ட்கள் இல்லை";
"Playback queue is empty" = "பிளேபேக் வரிசை காலியாக உள்ளது";
"Make default" = "இயல்புநிலை செய்யுங்கள்";
"Visibility" = "விழிமை";
"Stream & Player" = "ச்ட்ரீம் & பிளேயர்";
"Cached time" = "தற்காலிக சேமிப்பு நேரம்";
"No chapters information available" = "அத்தியாயங்கள் செய்தி எதுவும் கிடைக்கவில்லை";
"Could not refresh Trending" = "போக்கைப் புதுப்பிக்க முடியவில்லை";
"This URL could not be opened" = "இந்த முகவரி ஐ திறக்க முடியவில்லை";
"Could not open channel" = "சேனலைத் திறக்க முடியவில்லை";
"Could not extract playlist ID" = "பிளேலிச்ட் ஐடியை பிரித்தெடுக்க முடியவில்லை";
"Could not load video" = "வீடியோவை ஏற்ற முடியவில்லை";
"Could not refresh Playlists" = "பிளேலிச்ட்களைப் புதுப்பிக்க முடியவில்லை";
"Home" = "வீடு";
"Show Home" = "வீட்டைக் காட்டு";
"Recent History" = "அண்மைக் கால வரலாறு";
"Reload manifest" = "மீண்டும் ஏற்றவும்";
"Enter link to open" = "திறக்க இணைப்பை உள்ளிடவும்";
"Add" = "கூட்டு";
"Open Files" = "கோப்புகளைத் திறக்கவும்";
"Share" = "பங்கு";
"Left" = "இடது";
"Center" = "நடுவண்";
"Documents" = "ஆவணங்கள்";
"Open Video" = "வீடியோ திறந்த வீடியோ";
"Share%@link" = "பகிர்வு%@இணைப்பு";
"Could not delete document" = "ஆவணத்தை நீக்க முடியவில்லை";
"Live Streams" = "நேரடி நீரோடைகள்";
"Player Bar" = "பிளேயர் பார்";
"Always show controls buttons" = "எப்போதும் கட்டுப்பாட்டு பொத்தான்களைக் காட்டு";
"Maximum width expanded" = "அதிகபட்ச அகலம் விரிவடைந்தது";
"Seeking" = "தேடுவது";
"Controls Buttons" = "பொத்தான்களைக் கட்டுப்படுத்துகிறது";
"Controls button: backwards" = "கட்டுப்பாடுகள் பொத்தான்: பின்னோக்கி";
"Controls button: forwards" = "கட்டுப்பாடுகள் பொத்தான்: முன்னோக்கி";
"Hide player" = "பிளேயரை மறைக்க";
"Actions Buttons" = "செயல்கள் பொத்தான்கள்";
"Music Mode" = "இசை முறை";
"Subscribe/Unsubscribe" = "குழுசேரவும்/குழுவிலகவும்";
"Toggle player" = "பிளேயரை மாற்றவும்";
"Feed" = "தீவனம்";
"Inspector" = "இன்ச்பெக்டர்";
"Mark all as watched" = "பார்த்தபடி அனைவரையும் குறிக்கவும்";
"Lock" = "பூட்டு";
"Show scroll to top button in comments" = "கருத்துகளில் மேல் பொத்தானைக் காட்டுங்கள்";
"Landscape left" = "இயற்கை இடது";
"Watched: visible" = "பார்த்தது: தெரியும்";
"Play Now in AVPlayer" = "AVPlayer இல் இப்போது விளையாடுங்கள்";
"Show channel avatars in videos lists" = "வீடியோ பட்டியலில் சேனல் அவதாரங்களைக் காட்டு";
"Description preview" = "விளக்கம் முன்னோட்டம்";
"No preview" = "முன்னோட்டம் இல்லை";
"Other" = "மற்ற";
"Are you sure you want to export unencrypted passwords?" = "மறைகுறியாக்கப்பட்ட கடவுச்சொற்களை ஏற்றுமதி செய்ய விரும்புகிறீர்களா?";
"Icon only" = "படவுரு மட்டும்";
"Platform" = "இயங்குதளம்";
"Action button labels" = "செயல் பொத்தான் லேபிள்கள்";
"Export in progress..." = "முன்னேற்றத்தில் ஏற்றுமதி ...";
"In progress..." = "செயலில் உள்ளது…";
"Contact" = "தொடர்பு";
"Continue" = "தொடரவும்";
"Continue from %@" = "%@ இலிருந்து தொடரவும்";
"Controls" = "கட்டுப்பாடுகள்";
"Copy %@ link" = "நகலெடு %@ இணைப்பு";
"Copy %@ link with time" = "நேரத்துடன் %@ இணைப்பை நகலெடுக்கவும்";
"Could not load locations manifest" = "இருப்பிடங்களை வெளிப்படையாக ஏற்ற முடியவில்லை";
"Are you sure you want to clear history of watched videos?" = "பார்த்த வீடியோக்களின் வரலாற்றை அழிக்க விரும்புகிறீர்களா?";
"Are you sure you want to clear search history?" = "தேடல் வரலாற்றை அழிக்க விரும்புகிறீர்களா?";
"Are you sure you want to delete playlist?" = "பிளேலிச்ட்டை நீக்க விரும்புகிறீர்களா?";
"Are you sure you want to restore default quality profiles?" = "இயல்புநிலை தர சுயவிவரங்களை மீட்டெடுக்க விரும்புகிறீர்களா?";
"Are you sure you want to unsubscribe from %@?" = "%@இலிருந்து குழுவிலக விரும்புகிறீர்களா?";
"Automatic" = "தானியங்கி";
"Backend" = "பின்தளத்தில்";
"Blue" = "நீலம்";
"Browsing" = "உலாவுதல்";
"Buffering stream..." = "இடையக ச்ட்ரீம் ...";
"Bugs and great feature ideas can be sent to the GitHub issues tracker. " = "பிழைகள் மற்றும் சிறந்த அம்ச யோசனைகளை அறிவிலிமையம் சிக்கல்கள் டிராக்கருக்கு அனுப்பலாம். ";
"Cancel" = "ரத்துசெய்";
"Captions" = "தலைப்புகள்";
"Categories to Skip" = "தவிர்க்க வகைகள்";
"Category" = "வகை";
"Cellular" = "செல்லுலார்";
"Chapters" = "பாடங்கள்";
"Charging" = "சார்சிங்";
"Clear All" = "அனைத்தையும் அழிக்கவும்";
"Clear All Recents" = "எல்லா நெறிமுறைகளையும் அழிக்கவும்";
"Clear History" = "வரலாற்றை அழிக்கவும்";
"Clear Search History" = "தேடல் வரலாற்றை அழிக்கவும்";
"Clear the queue" = "வரிசையை அழிக்கவும்";
"Close PiP and open player when application enters foreground" = "பயன்பாடு முன்புறத்தில் நுழையும்போது மூடிய குழாய் மற்றும் திறந்த பிளேயர்";
"Close PiP when player is opened" = "பிளேயர் திறக்கப்படும்போது பிப் மூடு";
"Close PiP when starting playing other video" = "மற்ற வீடியோவை இயக்கத் தொடங்கும் போது PIP ஐ மூடு";
"Close player when closing video" = "வீடியோவை மூடும்போது பிளேயரை மூடு";
"Close video after playing last in the queue" = "வரிசையில் கடைசியாக விளையாடிய பிறகு வீடியோவை மூடு";
"Comments" = "கருத்துகள்";
"Connected successfully (%@)" = "வெற்றிகரமாக இணைக்கப்பட்டுள்ளது (%@)";
"Badge color" = "பதக்க நிறம்";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "எந்தவொரு கட்டண அல்லது இலவச தளத்திலும் (கள்) (எ.கா. ஒரு வீடியோவில் சொடுக்கு செய்க) அவர்களுடன் விரும்புவது, குழுசேர அல்லது தொடர்பு கொள்ள வெளிப்படையான நினைவூட்டல்கள்.";
"Filter: active" = "வடிகட்டி: செயலில்";
"Find Other" = "மற்றவர்களைக் கண்டுபிடி";
"Finding something to play..." = "விளையாட ஏதாவது கண்டுபிடிப்பது ...";
"Formats will be selected in order as listed.\nHLS is an adaptive format (resolution setting does not apply)." = "பட்டியலிடப்பட்டபடி வடிவங்கள் வரிசையில் தேர்ந்தெடுக்கப்படும்.\n எச்.எல்.எச் என்பது ஒரு தகவமைப்பு வடிவமாகும் (தீர்மான அமைப்பு பொருந்தாது).";
"Gaming" = "கேமிங்";
"Hide sidebar" = "பக்கப்பட்டியை மறைக்கவும்";
"High" = "உயர்ந்த";
"Highest" = "அதிகபட்சம்";
"Highest quality" = "மிக உயர்ந்த தகுதி";
"History" = "வரலாறு";
"Honor orientation lock" = "மரியாதை நோக்குநிலை பூட்டு";
"Hour" = "மணி";
"I am lost" = "நான் தொலைந்துவிட்டேன்";
"I found a bug /" = "நான் ஒரு பிழையைக் கண்டேன் /";
"I have a feature request" = "எனக்கு ஒரு அம்ச கோரிக்கை உள்ளது";
"I want to ask a question" = "நான் ஒரு கேள்வி கேட்க விரும்புகிறேன்";
"If you are interested what's coming in future updates, you can track project Milestones." = "எதிர்கால புதுப்பிப்புகளில் என்ன வரப்போகிறது என்பதை நீங்கள் ஆர்வமாக இருந்தால், திட்ட மைல்கற்களைக் கண்காணிக்கலாம்.";
"If you are reporting a bug, include all relevant details (especially: app version, used device and system version, steps to reproduce)." = "நீங்கள் ஒரு பிழையைப் புகாரளிக்கிறீர்கள் என்றால், தொடர்புடைய அனைத்து விவரங்களையும் சேர்க்கவும் (குறிப்பாக: பயன்பாட்டு பதிப்பு, பயன்படுத்தப்பட்ட சாதனம் மற்றும் கணினி பதிப்பு, இனப்பெருக்கம் செய்வதற்கான படிகள்).";
"Instance of current account" = "நடப்பு கணக்கின் நிகழ்வு";
"Interface" = "இடைமுகம்";
"Intro" = "அறிமுகம்";
"Large" = "பெரிய";
"Large layout is not suitable for all devices and using it may cause controls not to fit on the screen." = "பெரிய தளவமைப்பு எல்லா சாதனங்களுக்கும் பொருத்தமானதல்ல, அதைப் பயன்படுத்துவது கட்டுப்பாடுகள் திரையில் பொருந்தாது.";
"Loading..." = "ஏற்றுகிறது ...";
"Locations" = "இருப்பிடங்கள்";
"Replies" = "பதில்கள்";
"Part of a video promoting a product or service not directly related to the creator. The creator will receive payment or compensation in the form of money or free products." = "படைப்பாளருடன் நேரடியாக தொடர்புடைய ஒரு தயாரிப்பு அல்லது சேவையை ஊக்குவிக்கும் வீடியோவின் ஒரு பகுதி. படைப்பாளி பணம் அல்லது இலவச தயாரிப்புகளின் வடிவத்தில் கட்டணம் அல்லது இழப்பீட்டைப் பெறுவார்.";
"Pause" = "இடைநிறுத்தம்";
"Pause when entering background" = "பின்னணியில் நுழையும்போது இடைநிறுத்தம்";
"Pause when player is closed" = "வீரர் மூடப்படும் போது இடைநிறுத்தம்";
"Picture in Picture" = "படத்தில் படம்";
"Play" = "விளையாடுங்கள்";
"Play All" = "அனைத்தையும் விளையாடுங்கள்";
"Play in PiP" = "பைப்பில் விளையாடுங்கள்";
"Play Last" = "கடைசியாக விளையாடுங்கள்";
"Play Music" = "இசை வாசிக்கவும்";
"Play Next" = "அடுத்து விளையாடுங்கள்";
"Play Now" = "இப்போது விளையாடுங்கள்";
"Player" = "வீரர்";
"Playlist" = "பிளேலிச்ட்";
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "பிளேலிச்ட் \"%@\" நீக்கப்படும்.\n அதை மாற்ற முடியாது.";
"Playlists" = "பிளேலிச்ட்கள்";
"Popular" = "மக்கள்";
"Promoting a product or service that is directly related to the creator themselves. This usually includes merchandise or promotion of monetized platforms." = "படைப்பாளருடன் நேரடியாக தொடர்புடைய ஒரு தயாரிப்பு அல்லது சேவையை ஊக்குவித்தல். இது வழக்கமாக பணமாக்கப்பட்ட தளங்களின் வணிக அல்லது விளம்பரத்தை உள்ளடக்கியது.";
"Proxy videos" = "பதிலாள் வீடியோக்கள்";
"Public Locations" = "பொது இடங்கள்";
"Public Manifest" = "பொது மேனிஃபெச்ட்";
"Quality" = "தகுதி";
"Quality Profile" = "தரமான சுயவிவரம்";
"Queue" = "வரிசை";
"Queue is empty" = "வரிசை காலியாக உள்ளது";
"Rating" = "செயல்வரம்பு";
"Red" = "சிவப்பு";
"Refresh" = "புதுப்பிப்பு";
"Regular size" = "வழக்கமான அளவு";
"Remove" = "அகற்று";
"Remove from Favorites" = "பிடித்தவைகளிலிருந்து அகற்று";
"Remove from Playlist" = "பிளேலிச்ட்டிலிருந்து அகற்று";
"Remove from the queue" = "வரிசையிலிருந்து அகற்று";
"Segments typically found at the start of a video that include an animation, still frame or clip which are also seen in other videos by the same creator." = "அனிமேசன், இன்னும் சட்டகம் அல்லது கிளிப் ஆகியவற்றை உள்ளடக்கிய வீடியோவின் தொடக்கத்தில் பொதுவாகக் காணப்படும் பிரிவுகள், அதே படைப்பாளரால் மற்ற வீடியோக்களிலும் காணப்படுகின்றன.";
"Select location closest to you:" = "உங்களுக்கு மிக நெருக்கமான இருப்பிடத்தைத் தேர்ந்தெடுக்கவும்:";
"Settings" = "அமைப்புகள்";
"Share..." = "பங்கு ...";
"Short" = "குறுக்கு";
"Show account username" = "கணக்கு பயனர்பெயரைக் காட்டு";
"Show anonymous accounts" = "அநாமதேய கணக்குகளைக் காட்டு";
"Show channel name" = "சேனல் பெயரைக் காட்டு";
"Show keywords" = "முக்கிய வார்த்தைகளைக் காட்டு";
"Show progress of watching on thumbnails" = "சிறுபடங்களில் பார்க்கும் முன்னேற்றத்தைக் காட்டுங்கள்";
"Show sidebar when space permits" = "விண்வெளி அனுமதிக்கும் போது பக்கப்பட்டியைக் காட்டு";
"Show video length" = "வீடியோ நீளத்தைக் காட்டு";
"Shuffle All" = "அனைத்தையும் மாற்றவும்";
"Sidebar" = "பக்கப்பட்டி";
"Small" = "சிறிய";
"Sort: %@" = "வரிசைப்படுத்துதல்: %@";
"Source" = "மூலம்";
"Sponsor" = "ஒப்புரவாளர்";
"SponsorBlock" = "ஒப்புரவாளர் தொகுதி";
"SponsorBlock API Instance" = "ஒப்புரவாளர் பிளாக் பநிஇ நிகழ்வு";
"Switch to public locations" = "பொது இடங்களுக்கு மாறவும்";
"System controls buttons" = "கணினி பொத்தான்களைக் கட்டுப்படுத்துகிறது";
"System controls show buttons for %@" = "கணினி கட்டுப்பாடுகள் %@ க்கான பொத்தான்களைக் காட்டுகின்றன";
"That's nice to hear. It is fun to deliver apps other people want to use. You can consider donating to the project or help by contributing to new features development." = "அதைக் கேட்பது நல்லது. மற்றவர்கள் பயன்படுத்த விரும்பும் பயன்பாடுகளை வழங்குவது வேடிக்கையாக உள்ளது. புதிய நற்பொருத்தங்கள் மேம்பாட்டுக்கு பங்களிப்பதன் மூலம் திட்டத்திற்கு நன்கொடை அளிப்பதை நீங்கள் பரிசீலிக்கலாம்.";
"This cannot be reverted. You might need to switch between views or restart the app to see changes." = "இதை மாற்ற முடியாது. மாற்றங்களைக் காண நீங்கள் காட்சிகளுக்கு இடையில் மாற வேண்டும் அல்லது பயன்பாட்டை மறுதொடக்கம் செய்ய வேண்டும்.";
"Private" = "தனிப்பட்ட";
"This information will be processed only on your device and used to connect you to the server in the specified country." = "இந்த செய்தி உங்கள் சாதனத்தில் மட்டுமே செயலாக்கப்படும் மற்றும் குறிப்பிட்ட நாட்டில் உள்ள சேவையகத்துடன் உங்களை இணைக்கப் பயன்படுகிறது.";
"This will remove all your custom profiles and return their default values. This cannot be reverted." = "இது உங்கள் தனிப்பயன் சுயவிவரங்கள் அனைத்தையும் அகற்றி அவற்றின் இயல்புநிலை மதிப்புகளைத் தரும். இதை மாற்ற முடியாது.";
"Today" = "இன்று";
"Trending" = "டிரெண்டிங்";
"TV" = "டிவி";
"Typically near or at the end of the video when the credits pop up and/or endcards are shown." = "பொதுவாக வீடியோவின் அருகில் அல்லது வரவுகளை பாப் அப் மற்றும்/அல்லது எண்ட்கார்டுகள் காண்பிக்கும் போது.";
"Upload date" = "பதிவேற்ற தேதி";
"Username" = "பயனர்பெயர்";
"Very Large" = "மிகப் பெரியது";
"Videos" = "வீடியோக்கள்";
"Views" = "காட்சிகள்";
"Watched" = "பார்த்தேன்";
"Watching now" = "இப்போது பார்க்கிறது";
"Welcome" = "வரவேற்கிறோம்";
"When partially watched video is played" = "ஓரளவு பார்த்த வீடியோ இசைக்கப்படும் போது";
"Wi-Fi" = "இல்";
"Wiki" = "விக்கி";
"Year" = "ஆண்டு";
"You can find information about using Yattee in the Wiki pages." = "விக்கி பக்கங்களில் யாட்டியைப் பயன்படுத்துவது பற்றிய தகவல்களை நீங்கள் காணலாம்.";
"You can use automatic profile selection based on current device status or switch it in video playback settings controls." = "தற்போதைய சாதன நிலையின் அடிப்படையில் தானியங்கி சுயவிவரத் தேர்வைப் பயன்படுத்தலாம் அல்லது வீடியோ பிளேபேக் அமைப்புகள் கட்டுப்பாடுகளில் அதை மாற்றலாம்.";
"You have no playlists\n\nTap on \"New Playlist\" to create one" = "உங்களிடம் பிளேலிச்ட்கள் இல்லை\n\n ஒன்றை உருவாக்க \"புதிய பிளேலிச்ட்டை\" தட்டவும்";
"You need to create an instance and accounts\nto access %@ section" = "நீங்கள் ஒரு நிகழ்வு மற்றும் கணக்குகளை உருவாக்க வேண்டும்\n %@ பிரிவை அணுக";
"You need to select an account\nto access %@ section" = "நீங்கள் ஒரு கணக்கைத் தேர்ந்தெடுக்க வேண்டும்\n %@ பிரிவை அணுக";
"Public" = "பொது";
"Unlisted" = "பட்டியலிடப்படாதது";
"Now Playing" = "இப்போது விளையாடுகிறது";
"Current Location" = "தற்போதைய இடம்";
"For custom locations you can configure Frontend URL in Locations settings" = "தனிப்பயன் இடங்களுக்கு நீங்கள் இருப்பிட அமைப்புகளில் முன்பக்க முகவரி ஐ உள்ளமைக்கலாம்";
"Could not refresh Popular" = "பிரபலமாக புதுப்பிக்க முடியவில்லை";
"Could not create share link" = "பங்கு இணைப்பை உருவாக்க முடியவில்லை";
"Could not open playlist" = "பிளேலிச்ட்டைத் திறக்க முடியவில்லை";
"Could not extract video ID" = "வீடியோ ஐடியை பிரித்தெடுக்க முடியவில்லை";
"This video could not be opened" = "இந்த வீடியோவை திறக்க முடியவில்லை";
"No locations available at the moment" = "இந்த நேரத்தில் இடங்கள் எதுவும் கிடைக்கவில்லை";
"If you want this app to be available in your language, join translation project." = "இந்த பயன்பாடு உங்கள் மொழியில் கிடைக்க விரும்பினால், மொழிபெயர்ப்பு திட்டத்தில் சேரவும்.";
"Translations" = "மொழிபெயர்ப்புகள்";
"No documents" = "ஆவணங்கள் இல்லை";
"Recent Documents" = "சமீபத்திய ஆவணங்கள்";
"Share files from Finder on a Mac\nor iTunes on Windows" = "மேக்கில் கண்டுபிடிப்பாளரிடமிருந்து கோப்புகளைப் பகிரவும்\n அல்லது சன்னல்களில் ஐடியூன்ச்";
"Show Open Videos quick actions" = "திறந்த வீடியோக்களை விரைவான செயல்களைக் காட்டு";
"Show Favorites" = "பிடித்தவைகளைக் காட்டு";
"Inspector visibility" = "இன்ச்பெக்டர் தெரிவுநிலை";
"Edit Favorites…" = "பிடித்தவைகளைத் திருத்து…";
"Show Open Videos toolbar button" = "திறந்த வீடியோக்கள் கருவிப்பட்டி பொத்தானைக் காட்டு";
"Buttons labels" = "பொத்தான்கள் லேபிள்கள்";
"Files" = "கோப்புகள்";
"Show Documents" = "ஆவணங்களைக் காட்டு";
"Pages toolbar position" = "பக்கங்கள் கருவிப்பட்டி நிலை";
"Video Details" = "வீடியோ விவரங்கள்";
"Show Inspector" = "காட்டு இன்ச்பெக்டர்";
"Clear Queue before opening" = "திறப்பதற்கு முன் வரிசையை அழிக்கவும்";
"Open" = "திற";
"Video actions buttons" = "வீடியோ செயல்கள் பொத்தான்கள்";
"Pages buttons" = "பக்கங்கள் பொத்தான்கள்";
"URL to Open" = "திறக்க முகவரி";
"Could not open Files" = "கோப்புகளைத் திறக்க முடியவில்லை";
"Paste" = "ஒட்டு";
"Open Videos" = "வீடியோக்களைத் திறக்கவும்";
"Right click channel thumbnail to open context menu with more actions" = "மேலும் செயல்களுடன் சூழல் மெனுவைத் திறக்க சேனல் சிறு உருவத்தை வலது சொடுக்கு செய்யவும்";
"Gesture: fowards" = "சைகை: நோக்கி";
"System controls" = "கணினி கட்டுப்பாடுகள்";
"Gesture: backwards" = "சைகை: பின்னோக்கி";
"Gesture settings control skipping interval for double tap gesture on left/right side of the player. Changing system controls settings requires restart." = "சைகை அமைப்புகள் பிளேயரின் இடது/வலது பக்கத்தில் இரட்டை குழாய் சைகைக்கான இடைவெளியைத் தவிர்க்கும் இடைவெளியைக் கட்டுப்படுத்துகின்றன. கணினி கட்டுப்பாடுகள் அமைப்புகளை மாற்ற மறுதொடக்கம் தேவை.";
"Gesture settings control skipping interval for double click on left/right side of the player. Changing system controls settings requires restart." = "சைகை அமைப்புகள் கட்டுப்பாட்டு வீரரின் இடது/வலது பக்கத்தில் இரட்டை சொடுக்கு செய்வதற்கான இடைவெளியைத் தவிர்க்கவும். கணினி கட்டுப்பாடுகள் அமைப்புகளை மாற்ற மறுதொடக்கம் தேவை.";
"Gesture settings control skipping interval for remote arrow buttons (for 2nd generation Siri Remote or newer). Changing system controls settings requires restart." = "சைகை அமைப்புகள் தொலை அம்பு பொத்தான்களுக்கான இடைவெளியைத் தவிர்க்கின்றன (2 வது தலைமுறை சிரி ரிமோட் அல்லது புதியது). கணினி கட்டுப்பாடுகள் அமைப்புகளை மாற்ற மறுதொடக்கம் தேவை.";
"Play next item" = "அடுத்த உருப்படியை விளையாடுங்கள்";
"Lock orientation" = "பூட்டு நோக்குநிலை";
"Close video" = "வீடியோவை மூடு";
"Total size: %@" = "மொத்த அளவு: %@";
"Open channels with description expanded" = "விளக்கத்துடன் திறந்த சேனல்கள் விரிவாக்கப்பட்டன";
"Cache" = "கேச்";
"Show cache status" = "கேச் நிலையைக் காட்டு";
"Maximum feed items" = "அதிகபட்ச தீவன உருப்படிகள்";
"Are you sure you want to clear cache?" = "நீங்கள் நிச்சயமாக கேச் அழிக்க விரும்புகிறீர்களா?";
"Show Next in Queue" = "அடுத்த வரிசையில் காண்பி";
"Show toggle watch status button" = "வாட்ச் நிலை பொத்தானை மாற்றிக் கொள்ளுங்கள்";
"Next in Queue" = "அடுத்த வரிசையில்";
"List" = "பட்டியல்";
"Cells" = "செல்கள்";
"Toggle size" = "அளவை மாற்றவும்";
"Do nothing" = "எதுவும் செய்ய வேண்டாம்";
"Open channel" = "திறந்த சேனல்";
"Open video description expanded" = "திறந்த வீடியோ விளக்கம் விரிவாக்கப்பட்டது";
"Keep channels with unwatched videos on top of subscriptions list" = "சந்தாக்கள் பட்டியலில் மேலே உள்ள வீடியோக்களுடன் சேனல்களை வைத்திருங்கள்";
"Show video context menu options to force selected backend" = "தேர்ந்தெடுக்கப்பட்ட பின்தளத்தில் கட்டாயப்படுத்த வீடியோ சூழல் பட்டியல் விருப்பங்களைக் காட்டுங்கள்";
"Play Now in MPV" = "MPV இல் இப்போது விளையாடுங்கள்";
"Show channel avatars in channels lists" = "சேனல்கள் பட்டியல்களில் சேனல் அவதாரங்களைக் காட்டு";
"Podcasts" = "பாட்காச்ட்கள்";
"Releases" = "வெளியீடுகள்";
"Add %@" = "%@ சேர்க்கவும்";
"Open vertical chapters expanded" = "திறந்த செங்குத்து அத்தியாயங்கள் விரிவடைந்தன";
"Chapters (if available)" = "அத்தியாயங்கள் (கிடைத்தால்)";
"Import Settings..." = "இறக்குமதி அமைப்புகள் ...";
"Export Settings" = "ஏற்றுமதி அமைப்புகள்";
"Accounts passwords (unencrypted)" = "கணக்குகள் கடவுச்சொற்கள் (மறைகுறியாக்கப்படாதவை)";
"Other data" = "பிற தரவு";
"Export..." = "ஏற்றுமதி…";
"Other data include last used playback preferences and listing options" = "மற்ற தரவுகளில் கடைசியாக பயன்படுத்தப்பட்ட பின்னணி விருப்பத்தேர்வுகள் மற்றும் பட்டியல் விருப்பங்கள் அடங்கும்";
"Do not share this file with anyone or you can lose access to your accounts. If you don't select to export passwords you will be asked to provide them during import" = "இந்த கோப்பை யாருடனும் பகிர வேண்டாம் அல்லது உங்கள் கணக்குகளுக்கான அணுகலை இழக்கலாம். கடவுச்சொற்களை ஏற்றுமதி செய்ய நீங்கள் தேர்ந்தெடுக்கவில்லை என்றால், இறக்குமதியின் போது அவற்றை வழங்குமாறு கேட்கப்படுவீர்கள்";
"Export" = "ஏற்றுமதி";
"File information" = "கோப்பு செய்தி";
"Build" = "உருவாக்கு";
"Import" = "இறக்குமதி";
"Icon and text" = "படவுரு மற்றும் உரை";
"Password required to import" = "இறக்குமதி செய்ய கடவுச்சொல் தேவை";
"Custom Location already exists" = "தனிப்பயன் இடம் ஏற்கனவே உள்ளது";
"Custom Location selected for import" = "இறக்குமதிக்கு தேர்ந்தெடுக்கப்பட்ட தனிப்பயன் இடம்";
"Custom Location not selected for import" = "இறக்குமதிக்கு தனிப்பயன் இடம் தேர்ந்தெடுக்கப்படவில்லை";
"Account already exists" = "கணக்கு ஏற்கனவே உள்ளது";
"Password saved in import file" = "இறக்குமதி கோப்பில் கடவுச்சொல் சேமிக்கப்பட்டது";

View File

@ -266,11 +266,11 @@
"Sections" = "Bölümler"; "Sections" = "Bölümler";
"Save history of searches, channels and playlists" = "Arama, kanal ve çalma listelerinin geçmişini kaydet"; "Save history of searches, channels and playlists" = "Arama, kanal ve çalma listelerinin geçmişini kaydet";
"Restore default profiles..." = "Varsayılan profilleri geri yükle..."; "Restore default profiles..." = "Varsayılan profilleri geri yükle...";
"Visibility" = "Görünürlük"; "Visibility" = "";
"Translations" = "Çeviriler"; "Translations" = "Çeviriler";
"Enter links to open, one per line" = "Açmak için bağlantıları girin, satır başı bir tane"; "Enter links to open, one per line" = "";
"Open Videos" = "Videolar aç"; "Open Videos" = "";
"Playback Mode" = "Oynatma Modu"; "Playback Mode" = "";
/* Selected video was played on given date */ /* Selected video was played on given date */
"Watched %@" = "İzlendi %@"; "Watched %@" = "İzlendi %@";
@ -278,58 +278,58 @@
"That's nice to hear. It is fun to deliver apps other people want to use. You can consider donating to the project or help by contributing to new features development." = "Bunu duymak çok güzel. Başkalarının kullanmak isteyeceği uygulamalar sunmak eğlenceli. Projeye bağış yapmayı düşünebilir veya yeni özelliklerin geliştirilmesine katkıda bulunarak yardımcı olabilirsiniz."; "That's nice to hear. It is fun to deliver apps other people want to use. You can consider donating to the project or help by contributing to new features development." = "Bunu duymak çok güzel. Başkalarının kullanmak isteyeceği uygulamalar sunmak eğlenceli. Projeye bağış yapmayı düşünebilir veya yeni özelliklerin geliştirilmesine katkıda bulunarak yardımcı olabilirsiniz.";
"Thumbnails" = "Küçük Resimler"; "Thumbnails" = "Küçük Resimler";
"Dropped frames" = ""; "Dropped frames" = "";
"SponsorBlock API Instance" = "SponsorBlock API Oluşumu"; "SponsorBlock API Instance" = "";
/* Selected video is being played */ /* Selected video is being played */
"Watching now" = "Şu an izlenen"; "Watching now" = "Şu an izlenen";
"Video Details" = "Video Bilgileri"; "Video Details" = "Video Bilgileri";
"Live Streams" = "Canlı yayınlar"; "Live Streams" = "";
"Typically near or at the end of the video when the credits pop up and/or endcards are shown." = "Genellikle videonun sonunda veya sonuna yakın özet açılır ve/veya bitiş arayüzü gösterilir."; "Typically near or at the end of the video when the credits pop up and/or endcards are shown." = "Genellikle videonun sonunda veya sonuna yakın özet açılır ve/veya bitiş arayüzü gösterilir.";
"Default Profile" = "Varsayılan profil"; "Default Profile" = "";
/* Player controls layout size for TV */ /* Player controls layout size for TV */
"TV" = "TV"; "TV" = "TV";
"Add" = "Ekle"; "Add" = "";
"This URL could not be opened" = "Bu URL açılamadı"; "This URL could not be opened" = "";
"Hide" = "Sakla"; "Hide" = "";
"Playing Next" = "Sonra Oynatılacak"; "Playing Next" = "";
"Are you sure you want to remove this document?" = "Bu belgeyi kaldırmak istediğinizden emin misiniz?"; "Are you sure you want to remove this document?" = "";
"Show channel name" = "Kanal adını göster"; "Show channel name" = "Kanal adını göster";
"Unlisted" = "Liste dışı"; "Unlisted" = "";
"Paste" = "Yapıştır"; "Paste" = "";
"Rate & Captions" = "Puan ve Altyazılar"; "Rate & Captions" = "";
"Format" = "Format"; "Format" = "";
"Right" = "Sağ"; "Right" = "Sağ";
"Stream FPS" = "Yayın FPSi"; "Stream FPS" = "";
"Cached time" = "Önbelleğe alınan zaman"; "Cached time" = "";
"Sign In Required" = "Giriş yapmanız gerekiyor"; "Sign In Required" = "Giriş yapmanız gerekiyor";
"Could not create share link" = "Paylaş linki oluşturulamadı"; "Could not create share link" = "";
"Locations Manifest" = ""; "Locations Manifest" = "";
"When partially watched video is played" = "Video kısmi olarak izlendiyse"; "When partially watched video is played" = "Video kısmi olarak izlendiyse";
"Open Video" = "Video aç"; "Open Video" = "";
"Add Channels, Playlists and Searches to Favorites using" = "Kanallar, oynatma listeleri ve favorileri aramayı ekleyin"; "Add Channels, Playlists and Searches to Favorites using" = "";
"Always" = "Her zaman"; "Always" = "";
/* Video date filter in search */ /* Video date filter in search */
"Year" = "Yıl"; "Year" = "Yıl";
"Playback queue is empty" = "Oynatma kuyruğu boş"; "Playback queue is empty" = "";
"Show Favorites" = "Favorileri Göster"; "Show Favorites" = "Favorileri Göster";
"Driver" = "Sürücü"; "Driver" = "";
"Show progress of watching on thumbnails" = "İzlenme durumu görsellerde görünsün"; "Show progress of watching on thumbnails" = "İzlenme durumu görsellerde görünsün";
"Left" = "Sol"; "Left" = "";
"URL to Open" = "ılıcak URL"; "URL to Open" = "";
"Subscribe" = "Üye ol"; "Subscribe" = "Üye ol";
"Yattee" = "Yattee"; "Yattee" = "Yattee";
"Show Documents" = "Belgeleri Göster"; "Show Documents" = "Belgeleri Göster";
"Press and hold remote button to open captions and quality menus" = "Altyazılar ve kalite menülerini açmak için uzaktan kumanda tuşuna basılı tutun"; "Press and hold remote button to open captions and quality menus" = "";
"No locations available at the moment" = ""; "No locations available at the moment" = "";
"Show account username" = "Hesabın kullanıcı adını göster"; "Show account username" = "Hesabın kullanıcı adını göster";
"Used to create links from videos, channels and playlists" = "Videolardan, kanallardan ve oynatma listelerinden bağlantılar oluşturmak için kullanılır"; "Used to create links from videos, channels and playlists" = "Videolardan, kanallardan ve oynatma listelerinden bağlantılar oluşturmak için kullanılır";
"Size" = "Boyut"; "Size" = "";
"You have no playlists\n\nTap on \"New Playlist\" to create one" = "Çalma listeniz yok\n\nBir çalma listesi oluşturmak için \"Yeni Çalma Listesi\" üzerine dokunun"; "You have no playlists\n\nTap on \"New Playlist\" to create one" = "Çalma listeniz yok\n\nBir çalma listesi oluşturmak için \"Yeni Çalma Listesi\" üzerine dokunun";
"Sort: %@" = "Sırala: %@"; "Sort: %@" = "Sırala: %@";
"Select location closest to you:" = "Size en yakın konumu seçin:"; "Select location closest to you:" = "Size en yakın konumu seçin:";
"Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "Çalma Listesi Boş\n\nBir videoya dokunun ve basılı tutun, ardından\n\"Çalma Listesine Ekle\""; "Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "";
/* Video duration filter in search */ /* Video duration filter in search */
"Short" = "Kısa"; "Short" = "Kısa";
@ -337,29 +337,29 @@
"Remove Location" = ""; "Remove Location" = "";
"Edit Favorites…" = "Favorileri düzenle…"; "Edit Favorites…" = "Favorileri düzenle…";
"Show Open Videos toolbar button" = "Video Aç arayüzünü göster"; "Show Open Videos toolbar button" = "Video Aç arayüzünü göster";
"Sample Rate" = "Örnek hızı"; "Sample Rate" = "";
"Private" = "Gizli"; "Private" = "";
"Browsing" = "Gezinti"; "Browsing" = "Gezinti";
"Documents" = "Belgeler"; "Documents" = "";
"FPS" = "FPS"; "FPS" = "";
"Only for local files and URLs" = "Yalnızca yerel dosyalar ve URL'ler için"; "Only for local files and URLs" = "";
"Show sidebar" = "Kenar çubuğunu göster"; "Show sidebar" = "";
"Current Playlist" = "Şuanki oynatma listesi"; "Current Playlist" = "";
"Center" = "Orta"; "Center" = "";
"Address" = "Adres"; "Address" = "";
"Video actions buttons" = "Video eylem butonları"; "Video actions buttons" = "Video eylem butonları";
"Keep last played video in the queue after restart" = "Yeniden başlatıldığında son izlenen videoyu sırada bırak"; "Keep last played video in the queue after restart" = "";
"Remove…" = "Kaldır…"; "Remove…" = "";
"Trending" = "Trendler"; "Trending" = "Trendler";
"Statistics" = "İstatistikler"; "Statistics" = "";
"Copy%@link" = "%@bağlantıyı kopyala"; "Copy%@link" = "";
"Now Playing" = "Şuanda çalıyor"; "Now Playing" = "";
"Could not delete document" = "Belge silinemedi"; "Could not delete document" = "";
"No comments" = "Yorum yok"; "No comments" = "";
"Could not open Files" = "Dosyalar açılamadı"; "Could not open Files" = "";
"You need to select an account\nto access %@ section" = "%@ kesitine erişebilmek için \nbir hesap seçmeniz gerekiyor"; "You need to select an account\nto access %@ section" = "";
"Reload manifest" = "Yeniden Yükle"; "Reload manifest" = "Yeniden Yükle";
"Could not refresh Subscriptions" = "Abonelikler yenilenemedi"; "Could not refresh Subscriptions" = "";
/* Subscriptions title */ /* Subscriptions title */
"Subscriptions" = "Üyelik"; "Subscriptions" = "Üyelik";
@ -368,29 +368,29 @@
"Shuffle" = "Karıştır"; "Shuffle" = "Karıştır";
"Buttons labels" = "Eylem düğmeleri etiketi"; "Buttons labels" = "Eylem düğmeleri etiketi";
"Share %@ link" = "%@ bağlantısını paylaş"; "Share %@ link" = "%@ bağlantısını paylaş";
"Could not load streams" = "Akışlar yüklenemedi"; "Could not load streams" = "";
"Playback history is empty" = "Oynatma geçmişi boş"; "Playback history is empty" = "";
"Show icons and text when space permits" = "Alan yeterliyse simgeleri ve metni göster"; "Show icons and text when space permits" = "";
"unknown" = "Bilinmeyen"; "unknown" = "Bilinmeyen";
"Share..." = "Paylaş..."; "Share..." = "Paylaş...";
/* Video sort order in search */ /* Video sort order in search */
"Views" = "İzlenme"; "Views" = "İzlenme";
"You need to create an instance and accounts\nto access %@ section" = "%@ bölümüne erişmek için bir örnek ve hesap oluşturmanız gerekmektedir."; "You need to create an instance and accounts\nto access %@ section" = "";
"Verified" = "Doğrulanmış"; "Verified" = "";
"Open Files" = "Dosyaları"; "Open Files" = "";
"Could not refresh Playlists" = "Çalma listesi güncellenemedi"; "Could not refresh Playlists" = "Çalma listesi güncellenemedi";
"Actions buttons" = "Eylem düğmeleri"; "Actions buttons" = "";
"Any format" = "Herhangi bir format"; "Any format" = "";
"Show playback statistics" = "Oynatma istatistiklerini göster"; "Show playback statistics" = "Oynatma istatistiklerini göster";
"Pages buttons" = "Sayfa butonları"; "Pages buttons" = "Sayfa butonları";
"Videos" = "Videolar"; "Videos" = "Videolar";
"Codec" = "Kodek"; "Codec" = "";
"Comments are disabled" = "Yorumlar devre dışı"; "Comments are disabled" = "";
"Audio" = "Ses"; "Audio" = "";
"Public" = "Halka açık"; "Public" = "";
"Files" = "Dosyalar"; "Files" = "Dosyalar";
"Show Home" = "Ana Sayfayı Göster"; "Show Home" = "Ana Sayfayı Göster";
"Open" = "Aç"; "Open" = "Aç";
@ -400,30 +400,30 @@
"Clear Queue before opening" = "Açmadan önce Bekleme Listesini temizle"; "Clear Queue before opening" = "Açmadan önce Bekleme Listesini temizle";
"Copy %@ link with time" = "Bağlantıyı %@ zaman ile kopyala"; "Copy %@ link with time" = "Bağlantıyı %@ zaman ile kopyala";
"Show Inspector" = "Denetleyiciyi Göster"; "Show Inspector" = "Denetleyiciyi Göster";
"Make default" = "Varsayılan yap"; "Make default" = "";
"Are you sure you want to remove %@ location?" = "%@ konumunu kaldırmak istediğinizden emin misiniz?"; "Are you sure you want to remove %@ location?" = "";
"No chapters information available" = "Bölüm bilgisi mevcut değil"; "No chapters information available" = "";
"Share Logs..." = "Hata kayıtlarını paylaş…"; "Share Logs..." = "";
"Enter link to open" = "ılıcak bağlantıyı girin"; "Enter link to open" = "";
"No documents" = "Belge yok"; "No documents" = "Belge yok";
"Inspector visibility" = "Denetleyici görünümü"; "Inspector visibility" = "Denetleyici görünümü";
"Could not update your token." = ""; "Could not update your token." = "";
"Could not find any links to open in your clipboard" = "Panonuzda açılacak hiçbir bağlantı bulunamadı"; "Could not find any links to open in your clipboard" = "";
/* Video date filter in search */ /* Video date filter in search */
"Week" = "Hafta"; "Week" = "Hafta";
"Sidebar" = "Kenar çubuğu"; "Sidebar" = "Kenar çubuğu";
"Show only icons" = "Sadece ikonları göster"; "Show only icons" = "";
"Current: %@\n%@" = "Şuan: %@\n%@"; "Current: %@\n%@" = "Şuan: %@\n%@";
"Show anonymous accounts" = "Anonim hesapları göster"; "Show anonymous accounts" = "Anonim hesapları göster";
"Could not open playlist" = "Çalma listesi açılamadı"; "Could not open playlist" = "Çalma listesi açılamadı";
"Round corners" = "Yuvarlak köşeler"; "Round corners" = "";
"URL" = "URL"; "URL" = "URL";
"Recents" = "Yakın Zamandakiler"; "Recents" = "";
"Show sidebar when space permits" = "Alan olduğu sürece kenar çubuğunu göster"; "Show sidebar when space permits" = "Alan olduğu sürece kenar çubuğunu göster";
"System controls buttons" = "Sistem kontrol butonları"; "System controls buttons" = "Sistem kontrol butonları";
"Could not extract channel information" = "Kanal bilgisi çıkarılamadı"; "Could not extract channel information" = "";
"Public Locations" = "Herkese Açık Alanlar"; "Public Locations" = "";
"You can find information about using Yattee in the Wiki pages." = "Wiki sayfalarında Yattee kullanımı hakkında bilgilere erişebilirsiniz."; "You can find information about using Yattee in the Wiki pages." = "Wiki sayfalarında Yattee kullanımı hakkında bilgilere erişebilirsiniz.";
/* Player controls layout size */ /* Player controls layout size */
@ -440,16 +440,16 @@
"Smaller" = "Küçült"; "Smaller" = "Küçült";
"Sort" = "Sırala"; "Sort" = "Sırala";
"This cannot be reverted" = "Geriye alınamaz"; "This cannot be reverted" = "Geriye alınamaz";
"Public Manifest" = "Herkese Açık Bildiri"; "Public Manifest" = "";
"You have no Playlists" = "Çalma listeniz bulunmamaktadır"; "You have no Playlists" = "Çalma listeniz bulunmamaktadır";
"Watched" = "İzlendi"; "Watched" = "İzlendi";
"Could not open video" = "Video açılamadı"; "Could not open video" = "";
"Channel could not be found" = "Kanal bulunamadı"; "Channel could not be found" = "";
"Show video length" = "Video uzunluğunu göster"; "Show video length" = "Video uzunluğunu göster";
"Source" = "Kaynak"; "Source" = "Kaynak";
"Welcome" = "Hoşgeldiniz"; "Welcome" = "Hoşgeldiniz";
"Wi-Fi" = "Wi-Fi"; "Wi-Fi" = "Wi-Fi";
"Could not open channel" = "Kanal açılamadı"; "Could not open channel" = "";
"This video could not be opened" = "Bu video oynatılamadı"; "This video could not be opened" = "Bu video oynatılamadı";
"Could not extract playlist ID" = "Çalma listesi ID bilgisi alınamadı"; "Could not extract playlist ID" = "Çalma listesi ID bilgisi alınamadı";
"Could not load video" = "Video yüklenemedi"; "Could not load video" = "Video yüklenemedi";
@ -458,168 +458,50 @@
"Share %@ link with time" = "%@ bağlantısını zaman bilgisiyle birlikte paylaş"; "Share %@ link with time" = "%@ bağlantısını zaman bilgisiyle birlikte paylaş";
"This cannot be reverted. You might need to switch between views or restart the app to see changes." = "Bu geri döndürülemez. Değişiklikleri görmek için görünümler arasında geçiş yapmanız veya uygulamayı yeniden başlatmanız gerekebilir."; "This cannot be reverted. You might need to switch between views or restart the app to see changes." = "Bu geri döndürülemez. Değişiklikleri görmek için görünümler arasında geçiş yapmanız veya uygulamayı yeniden başlatmanız gerekebilir.";
"Unsubscribe" = "Abonelikten çık"; "Unsubscribe" = "Abonelikten çık";
"Current Location" = "Şuanki konum"; "Current Location" = "";
"Stream & Player" = "Yayın ve Oynatıcı"; "Stream & Player" = "";
"Hardware decoder" = "Donanımsal çözücü"; "Hardware decoder" = "Donanımsal çözücü";
"Honor orientation lock" = "Yön kilidine sadık kal"; "Honor orientation lock" = "";
"Seek with horizontal swipe on video" = "Video üzerinde yatay kaydırma"; "Seek with horizontal swipe on video" = "Video üzerinde yatay kaydırma";
"Segments typically found at the start of a video that include an animation, still frame or clip which are also seen in other videos by the same creator." = "Genellikle bir videonun başlangıcında bulunan ve aynı içerik oluşturucunun diğer videolarında da görülen bir animasyon, sabit kare veya klip içeren kısımlar."; "Segments typically found at the start of a video that include an animation, still frame or clip which are also seen in other videos by the same creator." = "";
"Switch to public locations" = "Herkese açık konumlara geç"; "Switch to public locations" = "";
"%@ formats" = ""; "%@ formats" = "";
"Open logs in Finder" = "Hata kayıtlarını Finder'da aç"; "Open logs in Finder" = "";
"Could not refresh Popular" = "Popüler yenilenemedi"; "Could not refresh Popular" = "";
/* SponsorBlock category name */ /* SponsorBlock category name */
"Self-promotion" = "Kendi reklamını yapma"; "Self-promotion" = "";
/* Player controls layout size */ /* Player controls layout size */
"Small" = "Küçük"; "Small" = "Küçük";
/* SponsorBlock category name */ /* SponsorBlock category name */
"Sponsor" = "Sponsor"; "Sponsor" = "Sponsor";
"System controls show buttons for %@" = "%@ için sistem denetleme tuşlarını göster"; "System controls show buttons for %@" = "";
"Show history" = "Kullanım geçmişini göster"; "Show history" = "Kullanım geçmişini göster";
"Could not extract SID from received cookies: %@" = ""; "Could not extract SID from received cookies: %@" = "";
"This will remove all your custom profiles and return their default values. This cannot be reverted." = "Bu eylem tüm kişiselleştirilmiş ayarlarınızı kaldıracak ve varsayılan ayarları geri getirecektir. Bu işlem geri döndürülemez."; "This will remove all your custom profiles and return their default values. This cannot be reverted." = "Bu eylem tüm kişiselleştirilmiş ayarlarınızı kaldıracak ve varsayılan ayarları geri getirecektir. Bu işlem geri döndürülemez.";
"Switch to other public location" = "Başka herkese açık konuma geç"; "Switch to other public location" = "";
"SponsorBlock" = "SponsorBlock"; "SponsorBlock" = "";
"Seek gesture speed" = "Kaydırma hızı"; "Seek gesture speed" = "Kaydırma hızı";
"If you want this app to be available in your language, join translation project." = "Uygulamanın kendi dilinize çevrilmesini istiyorsanız, çeviri projesine katılın."; "If you want this app to be available in your language, join translation project." = "Uygulamanın kendi dilinize çevrilmesini istiyorsanız, çeviri projesine katılın.";
"Could not refresh Trending" = "Trendde olanlar yenilenemedi"; "Could not refresh Trending" = "";
/* Video date filter in search */ /* Video date filter in search */
"Today" = "Bugün"; "Today" = "Bugün";
"Shorts" = ""; "Shorts" = "";
"Channel" = "Kanal"; "Channel" = "";
"Share files from Finder on a Mac\nor iTunes on Windows" = "Finder üzerinden Mac ile belge paylaşın\nveya iTunes üzerinden Windows ile"; "Share files from Finder on a Mac\nor iTunes on Windows" = "Finder üzerinden Mac ile belge paylaşın\nveya iTunes üzerinden Windows ile";
"\"%@\" will be irreversibly removed from this device." = "\"%@\" bu cihazdan geri alınamaz şekilde silinecektir."; "\"%@\" will be irreversibly removed from this device." = "";
"Recent Documents" = "Son kullanılan belgeler"; "Recent Documents" = "Son kullanılan belgeler";
"Recent History" = "Yakın zamanda izlenilenler"; "Recent History" = "Yakın zamanda izlenilenler";
"Show Open Videos quick actions" = "Video Aç hızlı eylemlerini göster"; "Show Open Videos quick actions" = "Video Aç hızlı eylemlerini göster";
"Pages toolbar position" = ""; "Pages toolbar position" = "";
"Video" = "Video"; "Video" = "";
"Channels" = "Kanallar"; "Channels" = "";
"Share" = "Paylaş"; "Share" = "";
"File" = "Dosya"; "File" = "";
"Share%@link" = "%@bağlantıyı paylaş"; "Share%@link" = "";
"Cache" = "Önbellek"; "Cache" = "Önbellek";
"Enter account credentials to connect..." = "Bağlanmak için hesap bilgilerini girin..."; "Enter account credentials to connect..." = "Bağlanmak için hesap bilgilerini girin...";
"Show scroll to top button in comments" = "Yorumlarda yukarıya götür düğmesini göster"; "Show scroll to top button in comments" = "Yorumlarda yukarıya götür düğmesini göster";
"Import Settings..." = "Ayarları içe aktar...";
"Export Settings" = "Ayarları dışa aktar";
"Accounts passwords (unencrypted)" = "Hesap parolaları (şifrelenmemiş)";
"Other" = "Diğer";
"Other data" = "Diğer veri";
"Export..." = "Dışa aktar…";
"Other data include last used playback preferences and listing options" = "Diğer veriler, son kullanılan oynatma tercihleri ve listeleme seçeneklerini içerir";
"Are you sure you want to export unencrypted passwords?" = "Şifrelenmemiş parolaları dışa aktarmak istediğinizden emin misiniz?";
"Icon only" = "Sadece ikon";
"Export" = "Dışa aktar";
"Build" = "Derleme";
"Platform" = "Platform";
"Import" = "İçe aktar";
"Action button labels" = "İşlem düğmesi etiketleri";
"Icon and text" = "İkon ve metin";
"File information" = "Dosya detayları";
"Password required to import" = "İçe aktarma için parola gerekli";
"Custom Location already exists" = "Özel konum zaten mevcut\"";
"Custom Location selected for import" = "İçe aktarma için özel konum seçildi";
"Custom Location not selected for import" = "İçe aktarma için özel konum seçilmedi";
"Account already exists" = "Hesap zaten var";
"Password saved in import file" = "Parola içe aktarma dosyasına kaydedildi";
"Export in progress..." = "Dışa aktarım devam etmekte...";
"In progress..." = "Devam ediyor…";
"Play Now in AVPlayer" = "Şimdi AVPlayerda oynat";
"Opening file…" = "Dosya açılıyor…";
"Show channel avatars in channels lists" = "Kanal avatarlarını kanallar listesinde göster";
"Keep channels with unwatched videos on top of subscriptions list" = "İzlenmemiş videoları olan kanalları abonelik listesinin üstte tut";
"Play Now in MPV" = "Şimdi MPVde oynat";
"Seek" = "İlerle";
"Description preview" = "Açıklama önizlemesi";
"No preview" = "Önizleme yok";
"Open vertical chapters expanded" = "Dikey bölümleri genişletilmiş olarak aç";
"Chapters (if available)" = "Bölümler (mevcutsa)";
"Opened File" = "Açılan dosya";
"File Extension" = "Dosya uzantısı";
"Short videos: hidden" = "Kısa videolar: gizli";
"Double tap gesture" = "Çift dokunma hareketi";
"Maximum width expanded" = "Maksimum genişlik genişletildi";
"Clear all" = "Herşeyi temizle";
"Right click channel thumbnail to open context menu with more actions" = "Daha fazla işlem için kanal kapak resmine sağ tıklayın";
"Show unwatched feed badges" = "İzlenmemiş akış rozetlerini göster";
"Gesture: fowards" = "Hareket: İleri";
"System controls" = "Sistem kontrolleri";
"Gesture: backwards" = "Hareket: Geri";
"Hide player" = "Oynatıcıyı gizle";
"Play next item" = "Sıradaki öğeyi çal";
"Music Mode" = "Müzik modu";
"Close video" = "Videoyu kapat";
"Total size: %@" = "Toplam boyut: %@";
"Are you sure you want to clear cache?" = "Önbelleği temizlemek istediğinizden emin misiniz?";
"Player Bar" = "Oynatıcı bar";
"Open expanded" = "Genişletilmiş aç";
"Mark channel feed as unwatched" = "Kanal akışını izlenmemiş olarak işaretle";
"Mark channel feed as watched" = "Kanal akışını izlenmiş olarak işaretle";
"Always show controls buttons" = "Her zaman kontrol tuşlarını göster";
"Tap and hold channel thumbnail to open context menu with more actions" = "Daha fazla işlem için kanal kapak resmine dokunup basılı tutun";
"Short videos: visible" = "Kısa videolar: görünür";
"Play all unwatched" = "İzlenmemiş hepsini oynat";
"Single tap gesture" = "Tek dokunma hareketi";
"Seeking" = "İlerleme";
"Controls Buttons" = "Kontrol düğmeleri";
"Gesture settings control skipping interval for double tap gesture on left/right side of the player. Changing system controls settings requires restart." = "Hareket ayarları, oynatıcıdaki sol/sağ taraf için çift dokunma hareketiyle atlanacak aralığı kontrol eder. Sistem kontrol ayarlarını değiştirmek yeniden başlatma gerektirir.";
"Gesture settings control skipping interval for double click on left/right side of the player. Changing system controls settings requires restart." = "Hareket ayarları, oynatıcıdaki sol/sağ taraf için çift tıklama ile atlanacak aralığı kontrol eder. Sistem kontrol ayarlarını değiştirmek yeniden başlatma gerektirir.";
"Gesture settings control skipping interval for remote arrow buttons (for 2nd generation Siri Remote or newer). Changing system controls settings requires restart." = "Hareket ayarları, uzaktan kumanda ok tuşları için atlama aralığını kontrol eder (2. nesil Siri uzaktan kumanda veya daha yenisi için). Sistem kontrol ayarlarını değiştirmek yeniden başlatma gerektirir.";
"Close video and player on end" = "Video ve oynatıcıyı bitişte kapat";
"Public account" = "Halka açık hesap";
"Your Accounts" = "Hesaplarınız";
"Browse without account" = "Hesap olmadan gezin";
"Rotate when entering fullscreen on landscape video" = "Yatay videoda tam ekran moduna geçerken döndür";
"Landscape left" = "Yatay sol";
"Landscape right" = "Yatay sağ";
"No rotation" = "Yön değiştirme yok";
"Open channels with description expanded" = "Açıklaması genişletilmiş kanalları aç";
"Show cache status" = "Önbellek durumunu göster";
"Maximum feed items" = "Maksimum akış öğeleri";
"Toggle size" = "Boyutu değiştir";
"Open channel" = "Kanalı aç";
"Mark all as watched" = "Hepsini izlenmiş olarak işaretle";
"Mark all as unwatched" = "Hepsini izlenmemiş olarak işaretle";
"Toggle player" = "Oynatıcıyı aç/kapa";
"Open video description expanded" = "Video açıklamasını genişletilmiş bir şekilde aç";
"Playback Settings" = "Oynatma ayarları";
"Enter location address to connect..." = "Bağlanmak için konum adresini girin...";
"Actions Buttons" = "Eylem düğmeleri";
"Lock orientation" = "Ekran yönünü kilitle";
"Subscribe/Unsubscribe" = "Abone ol/Abonelikten çık";
"Use system controls with AVPlayer" = "AVPlayer ile sistem kontrollerini kullan";
"Do not share this file with anyone or you can lose access to your accounts. If you don't select to export passwords you will be asked to provide them during import" = "Bu dosyayı kimseyle paylaşmayın, yoksa hesaplarınıza erişiminizi kaybedebilirsiniz. Parolaları dışa aktarmayı seçmezseniz, içe aktarma sırasında bunları sağlamanız istenecektir";
"(shorts hidden)" = "(kısa videolar gizli)";
"Disable filters" = "Filtreleri devre dışı bırak";
"Show channel avatars in videos lists" = "Kanal avatarlarını videolar listesinde göster";
"Available" = "Kullanılabilir";
"Startup section" = "Başlangıç bölümü";
"Home Settings" = "Ana sayfa ayarları";
"Watched: hidden" = "İzlenen: gizli";
"Watched: visible" = "İzlenen: görünür";
"(watched and shorts hidden)" = "(izlenen ve kısa videolar gizli)";
"No videos to show" = "Gösterilecek video yok";
"(watched hidden)" = "(izlenmiş gizli)";
"Limit" = "Limit";
"Are you sure you want to remove %@ from Favorites?" = "Favorilerden %@'yi kaldırmak istediğinizden emin misiniz?";
"List" = "Liste";
"Do nothing" = "Hiçbir şey yapma";
"Show Next in Queue" = "Kuyruktaki sıradakini göster";
"Show toggle watch status button" = "İzleme durumunu değiştiren düğmeyi göster";
"Next in Queue" = "Kuyruktaki sonraki";
"Feed" = "Akış";
"Queue - shuffled" = "Kuyruk - karışık";
"Replay" = "Yeniden oynatma";
"Loop one" = "Bir videoyu döngüde oynat";
"Description" = "Açıklama";
"Autoplay next" = "Otomatik sonrakini oynat";
"Stream" = "Yayın";
"Fullscreen" = "Tam ekran";
"Lock" = "Kilitle";
"Podcasts" = "Podcastler";
"Add %@" = "Ekle %@";

View File

@ -1306,7 +1306,6 @@
376BE50A27349108009AD608 /* BrowsingSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsingSettings.swift; sourceTree = "<group>"; }; 376BE50A27349108009AD608 /* BrowsingSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsingSettings.swift; sourceTree = "<group>"; };
376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Instance+Fixtures.swift"; sourceTree = "<group>"; }; 376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Instance+Fixtures.swift"; sourceTree = "<group>"; };
376E331128AD3B320070E30C /* ScrollDismissesKeyboard+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ScrollDismissesKeyboard+Backport.swift"; sourceTree = "<group>"; }; 376E331128AD3B320070E30C /* ScrollDismissesKeyboard+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ScrollDismissesKeyboard+Backport.swift"; sourceTree = "<group>"; };
376EC9D82D1DD39800EC4500 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = "<group>"; };
3771429529087BE100306CEA /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; }; 3771429529087BE100306CEA /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; };
3771429629087BF000306CEA /* az */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = az; path = az.lproj/Localizable.strings; sourceTree = "<group>"; }; 3771429629087BF000306CEA /* az */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = az; path = az.lproj/Localizable.strings; sourceTree = "<group>"; };
3772002527E8ED2600CB2475 /* BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = "<group>"; }; 3772002527E8ED2600CB2475 /* BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = "<group>"; };
@ -1501,7 +1500,6 @@
37DD9DA22785BBC900539416 /* NoCommentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoCommentsView.swift; sourceTree = "<group>"; }; 37DD9DA22785BBC900539416 /* NoCommentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoCommentsView.swift; sourceTree = "<group>"; };
37E04C0E275940FB00172673 /* VerticalScrollingFix.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalScrollingFix.swift; sourceTree = "<group>"; }; 37E04C0E275940FB00172673 /* VerticalScrollingFix.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalScrollingFix.swift; sourceTree = "<group>"; };
37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsNavigationLink.swift; sourceTree = "<group>"; }; 37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsNavigationLink.swift; sourceTree = "<group>"; };
37E21DC52CDE528A008DF47C /* ta */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ta; path = ta.lproj/Localizable.strings; sourceTree = "<group>"; };
37E64DD026D597EB00C71877 /* SubscribedChannelsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribedChannelsModel.swift; sourceTree = "<group>"; }; 37E64DD026D597EB00C71877 /* SubscribedChannelsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribedChannelsModel.swift; sourceTree = "<group>"; };
37E6D79B2944AE1A00550C3D /* FeedModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedModel.swift; sourceTree = "<group>"; }; 37E6D79B2944AE1A00550C3D /* FeedModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedModel.swift; sourceTree = "<group>"; };
37E6D79F2944CD3800550C3D /* CacheStatusHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheStatusHeader.swift; sourceTree = "<group>"; }; 37E6D79F2944CD3800550C3D /* CacheStatusHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheStatusHeader.swift; sourceTree = "<group>"; };
@ -2804,8 +2802,6 @@
tr, tr,
ru, ru,
"zh-Hant", "zh-Hant",
ta,
hu,
); );
mainGroup = 37D4B0BC2671614700C925CA; mainGroup = 37D4B0BC2671614700C925CA;
packageReferences = ( packageReferences = (
@ -4093,8 +4089,6 @@
3767F3332B25058300F257BC /* tr */, 3767F3332B25058300F257BC /* tr */,
3767F3342B2505EF00F257BC /* ru */, 3767F3342B2505EF00F257BC /* ru */,
37367E582B8F63C200436163 /* zh-Hant */, 37367E582B8F63C200436163 /* zh-Hant */,
37E21DC52CDE528A008DF47C /* ta */,
376EC9D82D1DD39800EC4500 /* hu */,
); );
name = Localizable.strings; name = Localizable.strings;
sourceTree = "<group>"; sourceTree = "<group>";
@ -4109,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 = 200; CURRENT_PROJECT_VERSION = 192;
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";
@ -4140,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 = 200; CURRENT_PROJECT_VERSION = 192;
"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";
@ -4171,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 = 200; CURRENT_PROJECT_VERSION = 192;
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;
@ -4191,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 = 200; CURRENT_PROJECT_VERSION = 192;
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;
@ -4332,7 +4326,6 @@
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;
@ -4355,7 +4348,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 = 200; CURRENT_PROJECT_VERSION = 192;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GCC_PREPROCESSOR_DEFINITIONS = ( GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1", "DEBUG=1",
@ -4372,8 +4365,7 @@
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UIRequiresFullScreen = YES; INFOPLIST_KEY_UIRequiresFullScreen = YES;
INFOPLIST_KEY_UIStatusBarHidden = NO; INFOPLIST_KEY_UIStatusBarHidden = NO;
INFOPLIST_KEY_UIStatusBarStyle = ""; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -4408,7 +4400,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 = 200; CURRENT_PROJECT_VERSION = 192;
"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";
@ -4422,8 +4414,7 @@
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UIRequiresFullScreen = YES; INFOPLIST_KEY_UIRequiresFullScreen = YES;
INFOPLIST_KEY_UIStatusBarHidden = NO; INFOPLIST_KEY_UIStatusBarHidden = NO;
INFOPLIST_KEY_UIStatusBarStyle = ""; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -4461,7 +4452,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 = 200; CURRENT_PROJECT_VERSION = 192;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
@ -4500,14 +4491,13 @@
"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 = 200; CURRENT_PROJECT_VERSION = 192;
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;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_USER_SELECTED_FILES = readonly; ENABLE_USER_SELECTED_FILES = readonly;
GCC_OPTIMIZATION_LEVEL = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = macOS/Info.plist; INFOPLIST_FILE = macOS/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.video"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.video";
@ -4535,7 +4525,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 200; CURRENT_PROJECT_VERSION = 192;
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 = (
@ -4558,7 +4548,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 200; CURRENT_PROJECT_VERSION = 192;
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 = (
@ -4583,7 +4573,7 @@
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 200; CURRENT_PROJECT_VERSION = 192;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@ -4607,7 +4597,7 @@
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 200; CURRENT_PROJECT_VERSION = 192;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@ -4633,7 +4623,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 = 200; CURRENT_PROJECT_VERSION = 192;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@ -4673,7 +4663,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 = 200; CURRENT_PROJECT_VERSION = 192;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
"DEVELOPMENT_TEAM[sdk=appletvos*]" = 78Z5H3M6RJ; "DEVELOPMENT_TEAM[sdk=appletvos*]" = 78Z5H3M6RJ;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@ -4713,7 +4703,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 200; CURRENT_PROJECT_VERSION = 192;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -4736,7 +4726,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 200; CURRENT_PROJECT_VERSION = 192;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -4919,7 +4909,7 @@
repositoryURL = "https://github.com/sindresorhus/Defaults"; repositoryURL = "https://github.com/sindresorhus/Defaults";
requirement = { requirement = {
kind = upToNextMajorVersion; kind = upToNextMajorVersion;
minimumVersion = 7.3.1; minimumVersion = 7.0.0;
}; };
}; };
372AA40E286D067B0000B1DC /* XCRemoteSwiftPackageReference "Repeat" */ = { 372AA40E286D067B0000B1DC /* XCRemoteSwiftPackageReference "Repeat" */ = {
@ -4934,8 +4924,8 @@
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/hyperoslo/Cache.git"; repositoryURL = "https://github.com/hyperoslo/Cache.git";
requirement = { requirement = {
kind = upToNextMajorVersion; branch = master;
minimumVersion = 7.4.0; kind = branch;
}; };
}; };
375B8AAF28B57F4200397B31 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { 375B8AAF28B57F4200397B31 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = {
@ -4950,16 +4940,16 @@
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/pinterest/PINCache"; repositoryURL = "https://github.com/pinterest/PINCache";
requirement = { requirement = {
kind = upToNextMajorVersion; branch = master;
minimumVersion = 3.0.4; kind = branch;
}; };
}; };
379325D329A265A300181CF1 /* XCRemoteSwiftPackageReference "swift-log" */ = { 379325D329A265A300181CF1 /* XCRemoteSwiftPackageReference "swift-log" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/apple/swift-log.git"; repositoryURL = "https://github.com/yattee/swift-log.git";
requirement = { requirement = {
kind = upToNextMajorVersion; branch = main;
minimumVersion = 1.6.1; kind = branch;
}; };
}; };
3797104728D3D10600D5F53C /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */ = { 3797104728D3D10600D5F53C /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */ = {
@ -4967,7 +4957,7 @@
repositoryURL = "https://github.com/SDWebImage/SDWebImageSwiftUI.git"; repositoryURL = "https://github.com/SDWebImage/SDWebImageSwiftUI.git";
requirement = { requirement = {
kind = upToNextMajorVersion; kind = upToNextMajorVersion;
minimumVersion = 2.2.7; minimumVersion = 2.1.0;
}; };
}; };
3797757B268922D100DD52A8 /* XCRemoteSwiftPackageReference "siesta" */ = { 3797757B268922D100DD52A8 /* XCRemoteSwiftPackageReference "siesta" */ = {
@ -4975,7 +4965,7 @@
repositoryURL = "https://github.com/bustoutsolutions/siesta"; repositoryURL = "https://github.com/bustoutsolutions/siesta";
requirement = { requirement = {
kind = upToNextMajorVersion; kind = upToNextMajorVersion;
minimumVersion = 1.5.2; minimumVersion = 1.5.0;
}; };
}; };
3799AC0728B03CEC001376F9 /* XCRemoteSwiftPackageReference "ActiveLabel.swift" */ = { 3799AC0728B03CEC001376F9 /* XCRemoteSwiftPackageReference "ActiveLabel.swift" */ = {
@ -4991,7 +4981,7 @@
repositoryURL = "https://github.com/Alamofire/Alamofire.git"; repositoryURL = "https://github.com/Alamofire/Alamofire.git";
requirement = { requirement = {
kind = upToNextMajorVersion; kind = upToNextMajorVersion;
minimumVersion = 5.9.1; minimumVersion = 5.0.0;
}; };
}; };
37BD07C52698B27B003EBB87 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = { 37BD07C52698B27B003EBB87 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = {
@ -4999,7 +4989,7 @@
repositoryURL = "https://github.com/siteline/SwiftUI-Introspect.git"; repositoryURL = "https://github.com/siteline/SwiftUI-Introspect.git";
requirement = { requirement = {
kind = upToNextMajorVersion; kind = upToNextMajorVersion;
minimumVersion = 1.3.0; minimumVersion = 0.1.3;
}; };
}; };
37CF8B8228535E4F00B71E37 /* XCRemoteSwiftPackageReference "SDWebImage" */ = { 37CF8B8228535E4F00B71E37 /* XCRemoteSwiftPackageReference "SDWebImage" */ = {
@ -5007,7 +4997,7 @@
repositoryURL = "https://github.com/SDWebImage/SDWebImage"; repositoryURL = "https://github.com/SDWebImage/SDWebImage";
requirement = { requirement = {
kind = upToNextMajorVersion; kind = upToNextMajorVersion;
minimumVersion = 5.19.7; minimumVersion = 5.19.1;
}; };
}; };
37D4B19B2671817900C925CA /* XCRemoteSwiftPackageReference "SwiftyJSON" */ = { 37D4B19B2671817900C925CA /* XCRemoteSwiftPackageReference "SwiftyJSON" */ = {
@ -5015,7 +5005,7 @@
repositoryURL = "https://github.com/SwiftyJSON/SwiftyJSON.git"; repositoryURL = "https://github.com/SwiftyJSON/SwiftyJSON.git";
requirement = { requirement = {
kind = upToNextMajorVersion; kind = upToNextMajorVersion;
minimumVersion = 5.0.2; minimumVersion = 5.0.0;
}; };
}; };
37EE6DC328A305AD00BFD632 /* XCRemoteSwiftPackageReference "Reachability" */ = { 37EE6DC328A305AD00BFD632 /* XCRemoteSwiftPackageReference "Reachability" */ = {
@ -5023,7 +5013,7 @@
repositoryURL = "https://github.com/ashleymills/Reachability.swift"; repositoryURL = "https://github.com/ashleymills/Reachability.swift";
requirement = { requirement = {
kind = upToNextMajorVersion; kind = upToNextMajorVersion;
minimumVersion = 5.2.3; minimumVersion = 5.1.0;
}; };
}; };
37FB2847272207F000A57617 /* XCRemoteSwiftPackageReference "SDWebImageWebPCoder" */ = { 37FB2847272207F000A57617 /* XCRemoteSwiftPackageReference "SDWebImageWebPCoder" */ = {
@ -5031,7 +5021,7 @@
repositoryURL = "https://github.com/SDWebImage/SDWebImageWebPCoder.git"; repositoryURL = "https://github.com/SDWebImage/SDWebImageWebPCoder.git";
requirement = { requirement = {
kind = upToNextMajorVersion; kind = upToNextMajorVersion;
minimumVersion = 0.14.6; minimumVersion = 0.8.4;
}; };
}; };
37FB285227220D8400A57617 /* XCRemoteSwiftPackageReference "SDWebImagePINPlugin" */ = { 37FB285227220D8400A57617 /* XCRemoteSwiftPackageReference "SDWebImagePINPlugin" */ = {
@ -5046,8 +5036,8 @@
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/mpvkit/MPVKit.git"; repositoryURL = "https://github.com/mpvkit/MPVKit.git";
requirement = { requirement = {
kind = upToNextMajorVersion; kind = exactVersion;
minimumVersion = 0.39.0; version = "0.38.0-fix";
}; };
}; };
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */

View File

@ -1,5 +1,5 @@
{ {
"originHash" : "173de1b718eb898698eaba0221b46be9781899a652725709c8400d3ddfb01980", "originHash" : "193e96313b1796c618e74a8b1c36659d0f16f66278ff8045f9e02c42590ae5aa",
"pins" : [ "pins" : [
{ {
"identity" : "activelabel.swift", "identity" : "activelabel.swift",
@ -15,8 +15,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/Alamofire/Alamofire.git", "location" : "https://github.com/Alamofire/Alamofire.git",
"state" : { "state" : {
"revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5", "revision" : "f455c2975872ccd2d9c81594c658af65716e9b9a",
"version" : "5.10.2" "version" : "5.9.1"
} }
}, },
{ {
@ -24,8 +24,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/hyperoslo/Cache.git", "location" : "https://github.com/hyperoslo/Cache.git",
"state" : { "state" : {
"revision" : "24e47109e31b2031cb26e25cc1b81b607496066c", "branch" : "master",
"version" : "7.4.0" "revision" : "d2e8f5a53c601b43371fdc90277d7f64b0e89a25"
} }
}, },
{ {
@ -51,8 +51,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/libwebp-Xcode.git", "location" : "https://github.com/SDWebImage/libwebp-Xcode.git",
"state" : { "state" : {
"revision" : "0d60654eeefd5d7d2bef3835804892c40225e8b2", "revision" : "b2b1d20a90b14d11f6ef4241da6b81c1d3f171e4",
"version" : "1.5.0" "version" : "1.3.2"
} }
}, },
{ {
@ -60,8 +60,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/mpvkit/MPVKit.git", "location" : "https://github.com/mpvkit/MPVKit.git",
"state" : { "state" : {
"revision" : "839dfa34b96029daef10b32d401c98edf17f04ae", "revision" : "ee72059235566df8b455bff15e3f83a1c9053e78",
"version" : "0.39.0" "version" : "0.38.0-fix"
} }
}, },
{ {
@ -69,8 +69,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/pinterest/PINCache", "location" : "https://github.com/pinterest/PINCache",
"state" : { "state" : {
"revision" : "2fb85948463292c2e824148cf17dc62a4c217a94", "branch" : "master",
"version" : "3.0.4" "revision" : "2fb85948463292c2e824148cf17dc62a4c217a94"
} }
}, },
{ {
@ -87,8 +87,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/ashleymills/Reachability.swift", "location" : "https://github.com/ashleymills/Reachability.swift",
"state" : { "state" : {
"revision" : "21d1dc412cfecbe6e34f1f4c4eb88d3f912654a6", "revision" : "7cbd73f46a7dfaeca079e18df7324c6de6d1834a",
"version" : "5.2.4" "version" : "5.2.3"
} }
}, },
{ {
@ -105,8 +105,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImage", "location" : "https://github.com/SDWebImage/SDWebImage",
"state" : { "state" : {
"revision" : "cac9a55a3ae92478a2c95042dcc8d9695d2129ca", "revision" : "8a1be70a625683bc04d6903e2935bf23f3c6d609",
"version" : "5.21.0" "version" : "5.19.7"
} }
}, },
{ {
@ -148,10 +148,10 @@
{ {
"identity" : "swift-log", "identity" : "swift-log",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log.git", "location" : "https://github.com/yattee/swift-log.git",
"state" : { "state" : {
"revision" : "3d8596ed08bd13520157f0355e35caed215ffbfa", "branch" : "main",
"version" : "1.6.3" "revision" : "3f3dc1390a2f116894887c352792dc8d5fa9e875"
} }
}, },
{ {
@ -168,8 +168,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/siteline/SwiftUI-Introspect.git", "location" : "https://github.com/siteline/SwiftUI-Introspect.git",
"state" : { "state" : {
"revision" : "807f73ce09a9b9723f12385e592b4e0aaebd3336", "revision" : "121c146fe591b1320238d054ae35c81ffa45f45a",
"version" : "1.3.0" "version" : "0.12.0"
} }
}, },
{ {

View File

@ -66,11 +66,6 @@
value = "Yes" value = "Yes"
isEnabled = "YES"> isEnabled = "YES">
</EnvironmentVariable> </EnvironmentVariable>
<EnvironmentVariable
key = "IDELogRedirectionPolicy"
value = "oslogToStdio"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables> </EnvironmentVariables>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction

View File

@ -1,38 +1,22 @@
import AVFoundation
import Defaults
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.delegate")
private(set) static var instance: AppDelegate! private(set) static var instance: AppDelegate!
func application(_: UIApplication, supportedInterfaceOrientationsFor _: UIWindow?) -> UIInterfaceOrientationMask { func application(_: UIApplication, supportedInterfaceOrientationsFor _: UIWindow?) -> UIInterfaceOrientationMask {
return orientationLock orientationLock
} }
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)
#if !os(macOS)
UIViewController.swizzleHomeIndicatorProperty() UIViewController.swizzleHomeIndicatorProperty()
OrientationTracker.shared.startDeviceOrientationTracking() OrientationTracker.shared.startDeviceOrientationTracking()
OrientationModel.shared.startOrientationUpdates()
// 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
} }

View File

@ -1,4 +1,5 @@
import CoreMotion import CoreMotion
import Defaults
import Logging import Logging
import UIKit import UIKit
@ -34,7 +35,7 @@ enum Orientation {
let rotateOrientationMask = rotateOrientation == .portrait ? UIInterfaceOrientationMask.portrait : let rotateOrientationMask = rotateOrientation == .portrait ? UIInterfaceOrientationMask.portrait :
rotateOrientation == .landscapeLeft ? .landscapeLeft : rotateOrientation == .landscapeLeft ? .landscapeLeft :
rotateOrientation == .landscapeRight ? .landscapeRight : rotateOrientation == .landscapeRight ? .landscapeRight :
.all .allButUpsideDown
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: rotateOrientationMask)) { error in windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: rotateOrientationMask)) { error in
print("denied rotation \(error)") print("denied rotation \(error)")

View File

@ -1,86 +1,91 @@
import Defaults import Defaults
import Foundation import Foundation
import Logging
import Repeat import Repeat
import SwiftUI import SwiftUI
final class OrientationModel { final class OrientationModel {
static var shared = OrientationModel() static var shared = OrientationModel()
let logger = Logger(label: "stream.yattee.orientation.model")
var orientation = UIInterfaceOrientation.portrait var orientation = UIInterfaceOrientation.portrait
var lastOrientation: UIInterfaceOrientation? var lastOrientation: UIInterfaceOrientation?
var orientationDebouncer = Debouncer(.milliseconds(300)) var orientationDebouncer = Debouncer(.milliseconds(300))
var orientationObserver: Any? var orientationObserver: Any?
@Default(.lockPortraitWhenBrowsing) private var lockPortraitWhenBrowsing
@Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape
private var player = PlayerModel.shared private var player = PlayerModel.shared
func startOrientationUpdates() { func configureOrientationUpdatesBasedOnAccelerometer() {
// Ensure the orientation observer is active let currentOrientation = OrientationTracker.shared.currentInterfaceOrientation
if currentOrientation.isLandscape,
Defaults[.enterFullscreenInLandscape],
!Defaults[.honorSystemOrientationLock],
!player.playingFullScreen,
!player.currentItem.isNil,
player.lockedOrientation.isNil || player.lockedOrientation!.contains(.landscape),
!player.playingInPictureInPicture,
player.presentingPlayer
{
DispatchQueue.main.async {
self.player.controls.presentingControls = false
self.player.enterFullScreen(showControls: false)
}
player.onPresentPlayer.append {
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: currentOrientation)
}
}
orientationObserver = NotificationCenter.default.addObserver( orientationObserver = NotificationCenter.default.addObserver(
forName: OrientationTracker.deviceOrientationChangedNotification, forName: OrientationTracker.deviceOrientationChangedNotification,
object: nil, object: nil,
queue: .main queue: .main
) { _ in ) { _ in
self.logger.info("Notification received: Device orientation changed.") guard !Defaults[.honorSystemOrientationLock],
self.player.presentingPlayer,
// We only allow .portrait and are not showing the player !self.player.playingInPictureInPicture,
guard (!self.player.presentingPlayer && !self.lockPortraitWhenBrowsing) || self.player.presentingPlayer self.player.lockedOrientation.isNil
else { else {
return return
} }
let orientation = OrientationTracker.shared.currentInterfaceOrientation let orientation = OrientationTracker.shared.currentInterfaceOrientation
self.logger.info("Current interface orientation: \(orientation)")
// Always update lastOrientation to keep track of the latest state guard self.lastOrientation != orientation else {
if self.lastOrientation != orientation {
self.lastOrientation = orientation
self.logger.info("Orientation changed to: \(orientation)")
} else {
self.logger.info("Orientation has not changed.")
}
// Only take action if the player is active and presenting
guard (!self.player.isOrientationLocked && !self.player.playingInPictureInPicture) || (!self.lockPortraitWhenBrowsing && !self.player.presentingPlayer) || (!self.lockPortraitWhenBrowsing && self.player.presentingPlayer && !self.player.isOrientationLocked)
else {
self.logger.info("Only updating orientation without actions.")
return return
} }
self.lastOrientation = orientation
DispatchQueue.main.async { DispatchQueue.main.async {
guard Defaults[.enterFullscreenInLandscape],
self.player.presentingPlayer
else {
return
}
self.orientationDebouncer.callback = { self.orientationDebouncer.callback = {
DispatchQueue.main.async { DispatchQueue.main.async {
if orientation.isLandscape { if orientation.isLandscape {
if self.enterFullscreenInLandscape, self.player.presentingPlayer {
self.logger.info("Entering fullscreen because orientation is landscape.")
self.player.controls.presentingControls = false self.player.controls.presentingControls = false
self.player.enterFullScreen(showControls: false) self.player.enterFullScreen(showControls: false)
}
Orientation.lockOrientation(OrientationTracker.shared.currentInterfaceOrientationMask, andRotateTo: orientation) Orientation.lockOrientation(OrientationTracker.shared.currentInterfaceOrientationMask, andRotateTo: orientation)
} else { } else {
self.logger.info("Exiting fullscreen because orientation is portrait.")
if self.player.playingFullScreen {
self.player.exitFullScreen(showControls: false) self.player.exitFullScreen(showControls: false)
} Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
if self.lockPortraitWhenBrowsing {
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
} else {
Orientation.lockOrientation(OrientationTracker.shared.currentInterfaceOrientationMask, andRotateTo: orientation)
}
} }
} }
} }
self.orientationDebouncer.call() self.orientationDebouncer.call()
} }
} }
} }
func stopOrientationUpdates() {
guard let observer = orientationObserver else { return }
NotificationCenter.default.removeObserver(observer)
}
func lockOrientation(_ orientation: UIInterfaceOrientationMask, andRotateTo rotateOrientation: UIInterfaceOrientation? = nil) { func lockOrientation(_ orientation: UIInterfaceOrientationMask, andRotateTo rotateOrientation: UIInterfaceOrientation? = nil) {
logger.info("Locking orientation to: \(orientation), rotating to: \(String(describing: rotateOrientation)).")
if let rotateOrientation { if let rotateOrientation {
self.orientation = rotateOrientation self.orientation = rotateOrientation
lastOrientation = rotateOrientation lastOrientation = rotateOrientation

View File

@ -11,7 +11,6 @@ struct InstancesSettings: View {
@State private var frontendURL = "" @State private var frontendURL = ""
@State private var proxiesVideos = false @State private var proxiesVideos = false
@State private var invidiousCompanion = false
@Environment(\.colorScheme) private var colorScheme @Environment(\.colorScheme) private var colorScheme
@ObservedObject private var accounts = AccountsModel.shared @ObservedObject private var accounts = AccountsModel.shared
@ -106,16 +105,6 @@ struct InstancesSettings: View {
} }
} }
if selectedInstance != nil, selectedInstance.app == .invidious {
invidiousCompanionToggle
.onAppear {
invidiousCompanion = selectedInstance.invidiousCompanion
}
.onChange(of: invidiousCompanion) { newValue in
InstancesModel.shared.setInvidiousCompanion(selectedInstance, newValue)
}
}
if selectedInstance != nil, !selectedInstance.app.supportsAccounts { if selectedInstance != nil, !selectedInstance.app.supportsAccounts {
Spacer() Spacer()
Text("Accounts are not supported for the application of this instance") Text("Accounts are not supported for the application of this instance")
@ -202,10 +191,6 @@ struct InstancesSettings: View {
private var proxiesVideosToggle: some View { private var proxiesVideosToggle: some View {
Toggle("Proxy videos", isOn: $proxiesVideos) Toggle("Proxy videos", isOn: $proxiesVideos)
} }
private var invidiousCompanionToggle: some View {
Toggle("Invidious companion", isOn: $invidiousCompanion)
}
} }
struct InstancesSettingsView_Previews: PreviewProvider { struct InstancesSettingsView_Previews: PreviewProvider {