mirror of
https://github.com/yattee/yattee.git
synced 2025-04-25 08:06:31 +00:00
Compare commits
145 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2a597ab3cb | ||
![]() |
4d662115e4 | ||
![]() |
e068257f14 | ||
![]() |
8b809fb0f1 | ||
![]() |
d3e80f500e | ||
![]() |
9343e9d023 | ||
![]() |
e4b25b0f80 | ||
![]() |
09c2fb19a9 | ||
![]() |
043b07274e | ||
![]() |
7f7e12d719 | ||
![]() |
d990c6630e | ||
![]() |
5239b36cfe | ||
![]() |
addc13ebfb | ||
![]() |
2a6f26ec68 | ||
![]() |
2e2f502d97 | ||
![]() |
59afc2f4c7 | ||
![]() |
2f902e74bb | ||
![]() |
500b75da4f | ||
![]() |
3a17cc4dee | ||
![]() |
16897338e6 | ||
![]() |
7c870b8e61 | ||
![]() |
75d9c5c747 | ||
![]() |
9e0f1a72ab | ||
![]() |
7f3b3ac0ab | ||
![]() |
84b70b794b | ||
![]() |
e6bae84162 | ||
![]() |
9efbac3d15 | ||
![]() |
1289f57f60 | ||
![]() |
cc03ab059b | ||
![]() |
17484f65fd | ||
![]() |
65247227e7 | ||
![]() |
625c01aaac | ||
![]() |
7465ff9c5c | ||
![]() |
41de28a698 | ||
![]() |
7baab7a88a | ||
![]() |
43599632b2 | ||
![]() |
e4f413ed2d | ||
![]() |
661b7547c5 | ||
![]() |
d69f410d92 | ||
![]() |
db7abe31ea | ||
![]() |
fff36ece26 | ||
![]() |
8a0e9ae75a | ||
![]() |
d6e5b5ed76 | ||
![]() |
cef1a1caea | ||
![]() |
6c6abe8c84 | ||
![]() |
9732537602 | ||
![]() |
c0deeabaed | ||
![]() |
f29dbcbe36 | ||
![]() |
c7e1a50e56 | ||
![]() |
dd205db15f | ||
![]() |
9ca5d292ec | ||
![]() |
748bc16342 | ||
![]() |
798d2fc67f | ||
![]() |
a5a88f8890 | ||
![]() |
f69ccb6bd6 | ||
![]() |
892b3dea17 | ||
![]() |
9a11e9f9f5 | ||
![]() |
055d5575ba | ||
![]() |
28b6a517b6 | ||
![]() |
b4bcd0c0a0 | ||
![]() |
e62010d5d5 | ||
![]() |
3339e8cb1f | ||
![]() |
4855f9bead | ||
![]() |
a65ed67751 | ||
![]() |
72dcbe4515 | ||
![]() |
7e02b08933 | ||
![]() |
8596ee8811 | ||
![]() |
894439ad5e | ||
![]() |
5dad7a1b47 | ||
![]() |
6d48a825cd | ||
![]() |
ed11e593ff | ||
![]() |
102dfba751 | ||
![]() |
4202b27c03 | ||
![]() |
2f937f74fa | ||
![]() |
34a957b28e | ||
![]() |
0bef798341 | ||
![]() |
28a7b6e981 | ||
![]() |
4663aab3da | ||
![]() |
0de0445805 | ||
![]() |
9cb0325503 | ||
![]() |
5e85fd294c | ||
![]() |
b2421da95d | ||
![]() |
4e4add3c42 | ||
![]() |
2185718d50 | ||
![]() |
b0264aaabe | ||
![]() |
035f3503c4 | ||
![]() |
e3ac11c172 | ||
![]() |
7aed6ac0d9 | ||
![]() |
457c0ce7b3 | ||
![]() |
747baf3edd | ||
![]() |
cd24a0322f | ||
![]() |
d525a22215 | ||
![]() |
322a550666 | ||
![]() |
98fa0b98e5 | ||
![]() |
5313e4ead0 | ||
![]() |
fa7b897e76 | ||
![]() |
9bf3df1a29 | ||
![]() |
34a805b986 | ||
![]() |
36f680be62 | ||
![]() |
a27ab02433 | ||
![]() |
59dd0785b3 | ||
![]() |
d7be915e7e | ||
![]() |
3752f67630 | ||
![]() |
dfe7565138 | ||
![]() |
4d02538cb9 | ||
![]() |
3229528a09 | ||
![]() |
fffc4f4a5f | ||
![]() |
e85bfe5007 | ||
![]() |
b00b733fd5 | ||
![]() |
119c663436 | ||
![]() |
e8fcee23ef | ||
![]() |
d56ef74a99 | ||
![]() |
98f5b1a22b | ||
![]() |
f0b7bd3ab8 | ||
![]() |
2d7a101ce0 | ||
![]() |
b2114174b4 | ||
![]() |
e9f502a486 | ||
![]() |
6978e9437c | ||
![]() |
2026201a5f | ||
![]() |
633af02577 | ||
![]() |
1fd62f04aa | ||
![]() |
e749307a0e | ||
![]() |
d76ec881be | ||
![]() |
72a39a2c75 | ||
![]() |
8a84db5a2d | ||
![]() |
663c37e3d2 | ||
![]() |
ea2b329df2 | ||
![]() |
bd79f56800 | ||
![]() |
9a650b4ac0 | ||
![]() |
13382270d5 | ||
![]() |
24626c2299 | ||
![]() |
18ac577c7f | ||
![]() |
617af2cd20 | ||
![]() |
1b778318dc | ||
![]() |
c9ce574c7a | ||
![]() |
9a1f0d7aaa | ||
![]() |
1cb695848c | ||
![]() |
740a2f85ac | ||
![]() |
1a22ac71be | ||
![]() |
f0d581d512 | ||
![]() |
049a42f2e8 | ||
![]() |
cea2684a29 | ||
![]() |
772e5016c4 | ||
![]() |
ed3d9a7d7c | ||
![]() |
bde9aade11 |
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
@ -27,9 +27,9 @@ jobs:
|
||||
# lane: ['mac beta', 'ios beta', 'tvos beta']
|
||||
lane: ['ios beta', 'tvos beta']
|
||||
name: Releasing ${{ matrix.lane }} version to TestFlight
|
||||
runs-on: macos-13
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.0'
|
||||
@ -44,16 +44,16 @@ jobs:
|
||||
- uses: maierj/fastlane-action@v3.0.0
|
||||
with:
|
||||
lane: ${{ matrix.lane }}
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.lane }} build
|
||||
path: fastlane/builds/**/*.ipa
|
||||
if-no-files-found: ignore
|
||||
mac_notarized:
|
||||
name: Build and notarize macOS app
|
||||
runs-on: macos-13
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
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
|
||||
- name: ZIP build
|
||||
run: /usr/bin/ditto -c -k --keepParent ${{ env.APP_PATH }} ${{ env.ZIP_PATH }}
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mac notarized build
|
||||
path: ${{ env.ZIP_PATH }}
|
||||
@ -86,10 +86,10 @@ jobs:
|
||||
name: Create GitHub release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- 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
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
- uses: ncipollo/release-action@v1
|
||||
|
56
CHANGELOG.md
56
CHANGELOG.md
@ -1,10 +1,8 @@
|
||||
## Build 192
|
||||
* 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
|
||||
## Build 199
|
||||
|
||||
## What's Changed
|
||||
* Add support for invidious companion by @lifo9 in https://github.com/yattee/yattee/pull/863
|
||||
* Translations update from Hosted Weblate by @weblate in https://github.com/yattee/yattee/pull/851
|
||||
|
||||
## Previous builds
|
||||
* Add skip, play/pause, and fullscreen shortcuts to macOS player (by @rickykresslein)
|
||||
@ -23,6 +21,50 @@
|
||||
* Add import export of missing settings
|
||||
* macOS: Fix settings windows layout
|
||||
* 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
|
||||
* don’t open video when dismissing context menu by @stonerl in https://github.com/yattee/yattee/pull/780
|
||||
* mpv: remove video layer when entering background by @stonerl in https://github.com/yattee/yattee/pull/793
|
||||
* hi-res invidious logos by @stonerl in https://github.com/yattee/yattee/pull/791
|
||||
* enable -O3 by @stonerl in https://github.com/yattee/yattee/pull/794
|
||||
* Better audio ducking by @stonerl in https://github.com/yattee/yattee/pull/779
|
||||
* fix picture in picture by @stonerl in https://github.com/yattee/yattee/pull/789
|
||||
* 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
|
||||
* 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
|
||||
|
84
Gemfile.lock
84
Gemfile.lock
@ -9,21 +9,22 @@ GEM
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.968.0)
|
||||
aws-sdk-core (3.201.5)
|
||||
aws-eventstream (1.3.2)
|
||||
aws-partitions (1.1072.0)
|
||||
aws-sdk-core (3.220.2)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.88.0)
|
||||
aws-sdk-core (~> 3, >= 3.201.0)
|
||||
aws-sdk-kms (1.99.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.159.0)
|
||||
aws-sdk-core (~> 3, >= 3.201.0)
|
||||
aws-sdk-s3 (1.182.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.9.1)
|
||||
aws-sigv4 (1.11.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.2.0)
|
||||
@ -33,13 +34,13 @@ GEM
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.6.5)
|
||||
digest-crc (0.7.0)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
domain_name (0.6.20240107)
|
||||
dotenv (2.8.1)
|
||||
emoji_regex (3.2.3)
|
||||
excon (0.111.0)
|
||||
faraday (1.10.3)
|
||||
excon (0.112.0)
|
||||
faraday (1.10.4)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
@ -58,17 +59,17 @@ GEM
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-multipart (1.1.0)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.0)
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.3.1)
|
||||
fastlane (2.222.0)
|
||||
fastimage (2.4.0)
|
||||
fastlane (2.227.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
@ -84,6 +85,7 @@ GEM
|
||||
faraday-cookie_jar (~> 0.0.6)
|
||||
faraday_middleware (~> 1.0)
|
||||
fastimage (>= 2.1.0, < 3.0.0)
|
||||
fastlane-sirp (>= 1.0.0)
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-apis-androidpublisher_v3 (~> 0.3)
|
||||
google-apis-playcustomapp_v1 (~> 0.1)
|
||||
@ -107,8 +109,10 @@ GEM
|
||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.3.0)
|
||||
xcpretty (~> 0.4.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||
fastlane-sirp (1.0.0)
|
||||
sysrandom (~> 1.0)
|
||||
gh_inspector (1.1.3)
|
||||
google-apis-androidpublisher_v3 (0.54.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
@ -126,12 +130,12 @@ GEM
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-storage_v1 (0.31.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-cloud-core (1.7.1)
|
||||
google-cloud-core (1.8.0)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
google-cloud-errors (1.4.0)
|
||||
google-cloud-errors (1.5.0)
|
||||
google-cloud-storage (1.47.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
@ -147,23 +151,25 @@ GEM
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.7)
|
||||
http-cookie (1.0.8)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
jmespath (1.6.2)
|
||||
json (2.7.2)
|
||||
jwt (2.8.2)
|
||||
json (2.10.2)
|
||||
jwt (2.10.1)
|
||||
base64
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.4.1)
|
||||
nanaimo (0.3.0)
|
||||
mutex_m (0.3.0)
|
||||
nanaimo (0.4.0)
|
||||
naturally (2.2.1)
|
||||
nkf (0.2.0)
|
||||
optparse (0.5.0)
|
||||
optparse (0.6.0)
|
||||
os (1.1.4)
|
||||
plist (3.7.1)
|
||||
plist (3.7.2)
|
||||
public_suffix (6.0.1)
|
||||
rake (13.2.1)
|
||||
representable (3.2.0)
|
||||
@ -171,11 +177,10 @@ GEM
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.3.6)
|
||||
strscan
|
||||
rouge (2.0.7)
|
||||
rexml (3.4.1)
|
||||
rouge (3.28.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
rubyzip (2.4.1)
|
||||
security (0.1.5)
|
||||
signet (0.19.0)
|
||||
addressable (~> 2.8)
|
||||
@ -185,7 +190,7 @@ GEM
|
||||
simctl (1.6.10)
|
||||
CFPropertyList
|
||||
naturally
|
||||
strscan (3.1.0)
|
||||
sysrandom (1.0.5)
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
@ -195,24 +200,27 @@ GEM
|
||||
tty-spinner (0.9.3)
|
||||
tty-cursor (~> 0.7)
|
||||
uber (0.1.0)
|
||||
unicode-display_width (2.5.0)
|
||||
unicode-display_width (2.6.0)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.25.0)
|
||||
xcodeproj (1.27.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.3.0)
|
||||
rexml (>= 3.3.2, < 4.0)
|
||||
xcpretty (0.3.0)
|
||||
rouge (~> 2.0.7)
|
||||
nanaimo (~> 0.4.0)
|
||||
rexml (>= 3.3.6, < 4.0)
|
||||
xcpretty (0.4.0)
|
||||
rouge (~> 3.28.0)
|
||||
xcpretty-travis-formatter (1.0.1)
|
||||
xcpretty (~> 0.2, >= 0.0.7)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-21
|
||||
arm64-darwin-23
|
||||
arm64-darwin-24
|
||||
x86_64-darwin-19
|
||||
x86_64-darwin-20
|
||||
x86_64-darwin-21
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
|
@ -10,11 +10,28 @@ struct AccountsBridge: Defaults.Bridge {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse the urlString to check for embedded username and password
|
||||
var sanitizedUrlString = value.urlString
|
||||
if var urlComponents = URLComponents(string: value.urlString) {
|
||||
if let user = urlComponents.user, let password = urlComponents.password {
|
||||
// Sanitize the embedded username and password
|
||||
let sanitizedUser = user.addingPercentEncoding(withAllowedCharacters: .urlUserAllowed) ?? user
|
||||
let sanitizedPassword = password.addingPercentEncoding(withAllowedCharacters: .urlPasswordAllowed) ?? password
|
||||
|
||||
// Update the URL components with sanitized credentials
|
||||
urlComponents.user = sanitizedUser
|
||||
urlComponents.password = sanitizedPassword
|
||||
|
||||
// Reconstruct the sanitized URL
|
||||
sanitizedUrlString = urlComponents.string ?? value.urlString
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
"id": value.id,
|
||||
"instanceID": value.instanceID ?? "",
|
||||
"name": value.name,
|
||||
"apiURL": value.urlString,
|
||||
"apiURL": sanitizedUrlString,
|
||||
"username": value.username,
|
||||
"password": value.password ?? ""
|
||||
]
|
||||
|
@ -10,14 +10,16 @@ struct Instance: Defaults.Serializable, Hashable, Identifiable {
|
||||
let apiURLString: String
|
||||
var frontendURL: String?
|
||||
var proxiesVideos: Bool
|
||||
var invidiousCompanion: Bool
|
||||
|
||||
init(app: VideosApp, id: String? = nil, name: String? = nil, apiURLString: String, frontendURL: String? = nil, proxiesVideos: Bool = false) {
|
||||
init(app: VideosApp, id: String? = nil, name: String? = nil, apiURLString: String, frontendURL: String? = nil, proxiesVideos: Bool = false, invidiousCompanion: Bool = false) {
|
||||
self.app = app
|
||||
self.id = id ?? UUID().uuidString
|
||||
self.name = name ?? app.rawValue
|
||||
self.apiURLString = apiURLString
|
||||
self.frontendURL = frontendURL
|
||||
self.proxiesVideos = proxiesVideos
|
||||
self.invidiousCompanion = invidiousCompanion
|
||||
}
|
||||
|
||||
var apiURL: URL! {
|
||||
|
@ -16,7 +16,8 @@ struct InstancesBridge: Defaults.Bridge {
|
||||
"name": value.name,
|
||||
"apiURL": value.apiURLString,
|
||||
"frontendURL": value.frontendURL ?? "",
|
||||
"proxiesVideos": value.proxiesVideos ? "true" : "false"
|
||||
"proxiesVideos": value.proxiesVideos ? "true" : "false",
|
||||
"invidiousCompanion": value.invidiousCompanion ? "true" : "false"
|
||||
]
|
||||
}
|
||||
|
||||
@ -33,7 +34,8 @@ struct InstancesBridge: Defaults.Bridge {
|
||||
let name = object["name"] ?? ""
|
||||
let frontendURL: String? = object["frontendURL"]!.isEmpty ? nil : object["frontendURL"]
|
||||
let proxiesVideos = object["proxiesVideos"] == "true"
|
||||
let invidiousCompanion = object["invidiousCompanion"] == "true"
|
||||
|
||||
return Instance(app: app, id: id, name: name, apiURLString: apiURL, frontendURL: frontendURL, proxiesVideos: proxiesVideos)
|
||||
return Instance(app: app, id: id, name: name, apiURLString: apiURL, frontendURL: frontendURL, proxiesVideos: proxiesVideos, invidiousCompanion: invidiousCompanion)
|
||||
}
|
||||
}
|
||||
|
@ -79,6 +79,17 @@ final class InstancesModel: ObservableObject {
|
||||
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) {
|
||||
let accounts = accounts(instance.id)
|
||||
if let index = Defaults[.instances].firstIndex(where: { $0.id == instance.id }) {
|
||||
|
@ -247,27 +247,27 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
}
|
||||
|
||||
func feed(_ page: Int?) -> Resource? {
|
||||
resource(baseURL: account.url, path: "\(Self.basePath)/auth/feed")
|
||||
resourceWithAuthCheck(baseURL: account.url, path: "\(Self.basePath)/auth/feed")
|
||||
.withParam("page", String(page ?? 1))
|
||||
}
|
||||
|
||||
var feed: Resource? {
|
||||
resource(baseURL: account.url, path: basePathAppending("auth/feed"))
|
||||
resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/feed"))
|
||||
}
|
||||
|
||||
var subscriptions: Resource? {
|
||||
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
|
||||
resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
|
||||
}
|
||||
|
||||
func subscribe(_ channelID: String, onCompletion: @escaping () -> Void = {}) {
|
||||
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
|
||||
resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
|
||||
.child(channelID)
|
||||
.request(.post)
|
||||
.onCompletion { _ in onCompletion() }
|
||||
}
|
||||
|
||||
func unsubscribe(_ channelID: String, onCompletion: @escaping () -> Void) {
|
||||
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
|
||||
resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
|
||||
.child(channelID)
|
||||
.request(.delete)
|
||||
.onCompletion { _ in onCompletion() }
|
||||
@ -308,11 +308,11 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
return nil
|
||||
}
|
||||
|
||||
return resource(baseURL: account.url, path: basePathAppending("auth/playlists"))
|
||||
return resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/playlists"))
|
||||
}
|
||||
|
||||
func playlist(_ id: String) -> Resource? {
|
||||
resource(baseURL: account.url, path: basePathAppending("auth/playlists/\(id)"))
|
||||
resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/playlists/\(id)"))
|
||||
}
|
||||
|
||||
func playlistVideos(_ id: String) -> Resource? {
|
||||
@ -445,6 +445,9 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
|
||||
urlComponents.scheme = instanceURLComponents.scheme
|
||||
urlComponents.host = instanceURLComponents.host
|
||||
urlComponents.user = instanceURLComponents.user
|
||||
urlComponents.password = instanceURLComponents.password
|
||||
urlComponents.port = instanceURLComponents.port
|
||||
|
||||
guard let url = urlComponents.url else {
|
||||
return nil
|
||||
@ -495,7 +498,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
indexID: indexID,
|
||||
live: json["liveNow"].boolValue,
|
||||
upcoming: json["isUpcoming"].boolValue,
|
||||
short: length <= Video.shortLength,
|
||||
short: length <= Video.shortLength && length != 0.0,
|
||||
publishedAt: publishedAt,
|
||||
likes: json["likeCount"].int,
|
||||
dislikes: json["dislikeCount"].int,
|
||||
@ -553,6 +556,30 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
)
|
||||
}
|
||||
|
||||
// Determines if the request requires Basic Auth credentials to be removed
|
||||
private func needsBasicAuthRemoval(for path: String) -> Bool {
|
||||
return path.hasPrefix("\(Self.basePath)/auth/")
|
||||
}
|
||||
|
||||
// Creates a resource URL with consideration for removing Basic Auth credentials
|
||||
private func createResourceURL(baseURL: URL, path: String) -> URL {
|
||||
var resourceURL = baseURL
|
||||
|
||||
// Remove Basic Auth credentials if required
|
||||
if needsBasicAuthRemoval(for: path), var urlComponents = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) {
|
||||
urlComponents.user = nil
|
||||
urlComponents.password = nil
|
||||
resourceURL = urlComponents.url ?? baseURL
|
||||
}
|
||||
|
||||
return resourceURL.appendingPathComponent(path)
|
||||
}
|
||||
|
||||
func resourceWithAuthCheck(baseURL: URL, path: String) -> Resource {
|
||||
let sanitizedURL = createResourceURL(baseURL: baseURL, path: path)
|
||||
return super.resource(absoluteURL: sanitizedURL)
|
||||
}
|
||||
|
||||
private func extractThumbnails(from details: JSON) -> [Thumbnail] {
|
||||
details["videoThumbnails"].arrayValue.compactMap { json in
|
||||
guard let url = json["url"].url,
|
||||
@ -563,13 +590,20 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
return nil
|
||||
}
|
||||
|
||||
// some of instances are not configured properly and return thumbnails links
|
||||
// with incorrect scheme
|
||||
// Some instances are not configured properly and return thumbnail links
|
||||
// with an incorrect scheme or a missing port.
|
||||
components.scheme = accountUrlComponents.scheme
|
||||
components.port = accountUrlComponents.port
|
||||
|
||||
// If basic HTTP authentication is used,
|
||||
// the username and password need to be prepended to the URL.
|
||||
components.user = accountUrlComponents.user
|
||||
components.password = accountUrlComponents.password
|
||||
|
||||
guard let thumbnailUrl = components.url else {
|
||||
return nil
|
||||
}
|
||||
print("Final thumbnail URL: \(thumbnailUrl)")
|
||||
|
||||
return Thumbnail(url: thumbnailUrl, quality: .init(rawValue: quality)!)
|
||||
}
|
||||
@ -621,21 +655,29 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
if json["liveNow"].boolValue {
|
||||
return hls
|
||||
}
|
||||
let videoId = json["videoId"].stringValue
|
||||
|
||||
return extractFormatStreams(from: json["formatStreams"].arrayValue) +
|
||||
extractAdaptiveFormats(from: json["adaptiveFormats"].arrayValue) +
|
||||
return extractFormatStreams(from: json["formatStreams"].arrayValue, videoId: videoId) +
|
||||
extractAdaptiveFormats(from: json["adaptiveFormats"].arrayValue, videoId: videoId) +
|
||||
hls
|
||||
}
|
||||
|
||||
private func extractFormatStreams(from streams: [JSON]) -> [Stream] {
|
||||
private func extractFormatStreams(from streams: [JSON], videoId: String?) -> [Stream] {
|
||||
streams.compactMap { stream in
|
||||
guard let streamURL = stream["url"].url else {
|
||||
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(
|
||||
instance: account.instance,
|
||||
avAsset: AVURLAsset(url: streamURL),
|
||||
avAsset: AVURLAsset(url: finalURL),
|
||||
resolution: Stream.Resolution.from(resolution: stream["resolution"].string ?? ""),
|
||||
kind: .stream,
|
||||
encoding: stream["encoding"].string ?? ""
|
||||
@ -643,7 +685,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
}
|
||||
}
|
||||
|
||||
private func extractAdaptiveFormats(from streams: [JSON]) -> [Stream] {
|
||||
private func extractAdaptiveFormats(from streams: [JSON], videoId: String?) -> [Stream] {
|
||||
let audioStreams = streams
|
||||
.filter { $0["type"].stringValue.starts(with: "audio/mp4") }
|
||||
.sorted {
|
||||
@ -658,15 +700,29 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
|
||||
return videoStreams.compactMap { videoStream in
|
||||
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 {
|
||||
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(
|
||||
instance: account.instance,
|
||||
audioAsset: AVURLAsset(url: audioAssetURL),
|
||||
videoAsset: AVURLAsset(url: videoAssetURL),
|
||||
audioAsset: AVURLAsset(url: finalAudioURL),
|
||||
videoAsset: AVURLAsset(url: finalVideoURL),
|
||||
resolution: Stream.Resolution.from(resolution: videoStream["resolution"].stringValue),
|
||||
kind: .adaptive,
|
||||
encoding: videoStream["encoding"].string,
|
||||
|
@ -515,7 +515,8 @@ final class PeerTubeAPI: Service, ObservableObject, VideosAPI {
|
||||
.dictionaryValue["files"]?.arrayValue.first?
|
||||
.dictionaryValue["fileUrl"]?.url
|
||||
{
|
||||
streams.append(SingleAssetStream(instance: account.instance, avAsset: AVURLAsset(url: fileURL), resolution: .hd720p30, kind: .stream))
|
||||
let resolution = Stream.Resolution.predefined(.hd720p30)
|
||||
streams.append(SingleAssetStream(instance: account.instance, avAsset: AVURLAsset(url: fileURL), resolution: resolution, kind: .stream))
|
||||
}
|
||||
|
||||
return streams
|
||||
|
@ -5,6 +5,7 @@ final class AdvancedSettingsGroupExporter: SettingsGroupExporter {
|
||||
override var globalJSON: JSON {
|
||||
[
|
||||
"showPlayNowInBackendContextMenu": Defaults[.showPlayNowInBackendContextMenu],
|
||||
"videoLoadingRetryCount": Defaults[.videoLoadingRetryCount],
|
||||
"showMPVPlaybackStats": Defaults[.showMPVPlaybackStats],
|
||||
"mpvEnableLogging": Defaults[.mpvEnableLogging],
|
||||
"mpvCacheSecs": Defaults[.mpvCacheSecs],
|
||||
@ -13,6 +14,7 @@ final class AdvancedSettingsGroupExporter: SettingsGroupExporter {
|
||||
"mpvDeinterlace": Defaults[.mpvDeinterlace],
|
||||
"mpvHWdec": Defaults[.mpvHWdec],
|
||||
"mpvDemuxerLavfProbeInfo": Defaults[.mpvDemuxerLavfProbeInfo],
|
||||
"mpvSetRefreshToContentFPS": Defaults[.mpvSetRefreshToContentFPS],
|
||||
"mpvInitialAudioSync": Defaults[.mpvInitialAudioSync],
|
||||
"showCacheStatus": Defaults[.showCacheStatus],
|
||||
"feedCacheSize": Defaults[.feedCacheSize]
|
||||
|
@ -11,6 +11,7 @@ final class BrowsingSettingsGroupExporter: SettingsGroupExporter {
|
||||
"favorites": Defaults[.favorites].compactMap { jsonFromString(FavoriteItem.bridge.serialize($0)) },
|
||||
"widgetsSettings": Defaults[.widgetsSettings].compactMap { widgetSettingsJSON($0) },
|
||||
"startupSection": Defaults[.startupSection].rawValue,
|
||||
"showSearchSuggestions": Defaults[.showSearchSuggestions],
|
||||
"visibleSections": Defaults[.visibleSections].compactMap { $0.rawValue },
|
||||
"showOpenActionsToolbarItem": Defaults[.showOpenActionsToolbarItem],
|
||||
"accountPickerDisplaysAnonymousAccounts": Defaults[.accountPickerDisplaysAnonymousAccounts],
|
||||
|
@ -5,11 +5,13 @@ final class ConstrolsSettingsGroupExporter: SettingsGroupExporter {
|
||||
override var globalJSON: JSON {
|
||||
[
|
||||
"avPlayerUsesSystemControls": Defaults[.avPlayerUsesSystemControls],
|
||||
"fullscreenPlayerGestureEnabled": Defaults[.fullscreenPlayerGestureEnabled],
|
||||
"horizontalPlayerGestureEnabled": Defaults[.horizontalPlayerGestureEnabled],
|
||||
"seekGestureSensitivity": Defaults[.seekGestureSensitivity],
|
||||
"seekGestureSpeed": Defaults[.seekGestureSpeed],
|
||||
"playerControlsLayout": Defaults[.playerControlsLayout].rawValue,
|
||||
"fullScreenPlayerControlsLayout": Defaults[.fullScreenPlayerControlsLayout].rawValue,
|
||||
"playerControlsBackgroundOpacity": Defaults[.playerControlsBackgroundOpacity],
|
||||
"systemControlsCommands": Defaults[.systemControlsCommands].rawValue,
|
||||
"buttonBackwardSeekDuration": Defaults[.buttonBackwardSeekDuration],
|
||||
"buttonForwardSeekDuration": Defaults[.buttonForwardSeekDuration],
|
||||
|
@ -44,7 +44,7 @@ final class PlayerSettingsGroupExporter: SettingsGroupExporter {
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
export["honorSystemOrientationLock"].bool = Defaults[.honorSystemOrientationLock]
|
||||
export["isOrientationLocked"].bool = Defaults[.isOrientationLocked]
|
||||
export["enterFullscreenInLandscape"].bool = Defaults[.enterFullscreenInLandscape]
|
||||
export["rotateToLandscapeOnEnterFullScreen"].string = Defaults[.rotateToLandscapeOnEnterFullScreen].rawValue
|
||||
#endif
|
||||
|
@ -9,6 +9,10 @@ struct AdvancedSettingsGroupImporter {
|
||||
Defaults[.showPlayNowInBackendContextMenu] = showPlayNowInBackendContextMenu
|
||||
}
|
||||
|
||||
if let videoLoadingRetryCount = json["videoLoadingRetryCount"].int {
|
||||
Defaults[.videoLoadingRetryCount] = videoLoadingRetryCount
|
||||
}
|
||||
|
||||
if let showMPVPlaybackStats = json["showMPVPlaybackStats"].bool {
|
||||
Defaults[.showMPVPlaybackStats] = showMPVPlaybackStats
|
||||
}
|
||||
@ -41,6 +45,10 @@ struct AdvancedSettingsGroupImporter {
|
||||
Defaults[.mpvDemuxerLavfProbeInfo] = mpvDemuxerLavfProbeInfo
|
||||
}
|
||||
|
||||
if let mpvSetRefreshToContentFPS = json["mpvSetRefreshToContentFPS"].bool {
|
||||
Defaults[.mpvSetRefreshToContentFPS] = mpvSetRefreshToContentFPS
|
||||
}
|
||||
|
||||
if let mpvInitialAudioSync = json["mpvInitialAudioSync"].bool {
|
||||
Defaults[.mpvInitialAudioSync] = mpvInitialAudioSync
|
||||
}
|
||||
|
@ -46,6 +46,10 @@ struct BrowsingSettingsGroupImporter {
|
||||
Defaults[.startupSection] = startupSection
|
||||
}
|
||||
|
||||
if let showSearchSuggestions = json["showSearchSuggestions"].bool {
|
||||
Defaults[.showSearchSuggestions] = showSearchSuggestions
|
||||
}
|
||||
|
||||
if let visibleSections = json["visibleSections"].array {
|
||||
let sections = visibleSections.compactMap { visibleSectionJSON in
|
||||
if let visibleSectionString = visibleSectionJSON.rawString(options: []),
|
||||
|
@ -9,6 +9,10 @@ struct ConstrolsSettingsGroupImporter {
|
||||
Defaults[.avPlayerUsesSystemControls] = avPlayerUsesSystemControls
|
||||
}
|
||||
|
||||
if let fullscreenPlayerGestureEnabled = json["fullscreenPlayerGestureEnabled"].bool {
|
||||
Defaults[.fullscreenPlayerGestureEnabled] = fullscreenPlayerGestureEnabled
|
||||
}
|
||||
|
||||
if let horizontalPlayerGestureEnabled = json["horizontalPlayerGestureEnabled"].bool {
|
||||
Defaults[.horizontalPlayerGestureEnabled] = horizontalPlayerGestureEnabled
|
||||
}
|
||||
@ -33,6 +37,10 @@ struct ConstrolsSettingsGroupImporter {
|
||||
Defaults[.fullScreenPlayerControlsLayout] = fullScreenPlayerControlsLayout
|
||||
}
|
||||
|
||||
if let playerControlsBackgroundOpacity = json["playerControlsBackgroundOpacity"].double {
|
||||
Defaults[.playerControlsBackgroundOpacity] = playerControlsBackgroundOpacity
|
||||
}
|
||||
|
||||
if let systemControlsCommandsString = json["systemControlsCommands"].string,
|
||||
let systemControlsCommands = SystemControlsCommands(rawValue: systemControlsCommandsString)
|
||||
{
|
||||
|
@ -97,8 +97,8 @@ struct PlayerSettingsGroupImporter {
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
if let honorSystemOrientationLock = json["honorSystemOrientationLock"].bool {
|
||||
Defaults[.honorSystemOrientationLock] = honorSystemOrientationLock
|
||||
if let isOrientationLocked = json["isOrientationLocked"].bool {
|
||||
Defaults[.isOrientationLocked] = isOrientationLocked
|
||||
}
|
||||
|
||||
if let enterFullscreenInLandscape = json["enterFullscreenInLandscape"].bool {
|
||||
|
@ -102,7 +102,7 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
|
||||
private var frequentTimeObserver: Any?
|
||||
private var infrequentTimeObserver: Any?
|
||||
private var playerTimeControlStatusObserver: Any?
|
||||
private var playerTimeControlStatusObserver: NSKeyValueObservation?
|
||||
|
||||
private var statusObservation: NSKeyValueObservation?
|
||||
|
||||
@ -119,10 +119,30 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
#if os(iOS)
|
||||
controller.player = avPlayer
|
||||
#endif
|
||||
logger.info("AVPlayerBackend initialized.")
|
||||
}
|
||||
|
||||
deinit {
|
||||
// Invalidate any observers to avoid memory leaks
|
||||
statusObservation?.invalidate()
|
||||
playerTimeControlStatusObserver?.invalidate()
|
||||
|
||||
// Remove any time observers added to AVPlayer
|
||||
if let frequentObserver = frequentTimeObserver {
|
||||
avPlayer.removeTimeObserver(frequentObserver)
|
||||
}
|
||||
if let infrequentObserver = infrequentTimeObserver {
|
||||
avPlayer.removeTimeObserver(infrequentObserver)
|
||||
}
|
||||
|
||||
// Remove notification observers
|
||||
removeItemDidPlayToEndTimeObserver()
|
||||
|
||||
logger.info("AVPlayerBackend deinitialized.")
|
||||
}
|
||||
|
||||
func canPlay(_ stream: Stream) -> Bool {
|
||||
stream.kind == .hls || stream.kind == .stream || (stream.kind == .adaptive && stream.format == .mp4)
|
||||
stream.kind == .hls || stream.kind == .stream
|
||||
}
|
||||
|
||||
func playStream(
|
||||
@ -161,7 +181,9 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
{
|
||||
seek(to: 0, seekType: .loopRestart)
|
||||
}
|
||||
|
||||
#if !os(macOS)
|
||||
model.setAudioSessionActive(true)
|
||||
#endif
|
||||
avPlayer.play()
|
||||
|
||||
// Setting hasStarted to true the first time player started
|
||||
@ -176,7 +198,9 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
guard avPlayer.timeControlStatus != .paused else {
|
||||
return
|
||||
}
|
||||
|
||||
#if !os(macOS)
|
||||
model.setAudioSessionActive(false)
|
||||
#endif
|
||||
avPlayer.pause()
|
||||
model.objectWillChange.send()
|
||||
}
|
||||
@ -190,6 +214,9 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
}
|
||||
|
||||
func stop() {
|
||||
#if !os(macOS)
|
||||
model.setAudioSessionActive(false)
|
||||
#endif
|
||||
avPlayer.replaceCurrentItem(with: nil)
|
||||
hasStarted = false
|
||||
}
|
||||
@ -344,7 +371,7 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
|
||||
let startPlaying = {
|
||||
#if !os(macOS)
|
||||
try? AVAudioSession.sharedInstance().setActive(true)
|
||||
self.model.setAudioSessionActive(true)
|
||||
#endif
|
||||
|
||||
self.setRate(self.model.currentRate)
|
||||
@ -779,7 +806,7 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
opened = true
|
||||
controller.startPictureInPicture()
|
||||
} else {
|
||||
print("PiP not possible, waited \(delay) seconds")
|
||||
self.logger.info("PiP not possible, waited \(delay) seconds")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import SwiftUI
|
||||
final class MPVBackend: PlayerBackend {
|
||||
static var timeUpdateInterval = 0.5
|
||||
static var networkStateUpdateInterval = 0.1
|
||||
static var refreshRateUpdateInterval = 0.5
|
||||
|
||||
private var logger = Logger(label: "mpv-backend")
|
||||
|
||||
@ -22,13 +23,14 @@ final class MPVBackend: PlayerBackend {
|
||||
|
||||
var stream: Stream?
|
||||
var video: Video?
|
||||
var captions: Captions? { didSet {
|
||||
guard let captions else {
|
||||
client?.removeSubs()
|
||||
return
|
||||
var captions: Captions? {
|
||||
didSet {
|
||||
Task {
|
||||
await handleCaptionsChange()
|
||||
}
|
||||
}
|
||||
addSubTrack(captions.url)
|
||||
}}
|
||||
}
|
||||
|
||||
var currentTime: CMTime?
|
||||
|
||||
var loadedVideo = false
|
||||
@ -89,6 +91,7 @@ final class MPVBackend: PlayerBackend {
|
||||
|
||||
private var clientTimer: Repeater!
|
||||
private var networkStateTimer: Repeater!
|
||||
private var refreshRateTimer: Repeater!
|
||||
|
||||
private var onFileLoaded: (() -> Void)?
|
||||
|
||||
@ -184,27 +187,30 @@ final class MPVBackend: PlayerBackend {
|
||||
}
|
||||
|
||||
init() {
|
||||
// swiftlint:disable shorthand_optional_binding
|
||||
clientTimer = .init(interval: .seconds(Self.timeUpdateInterval), mode: .infinite) { [weak self] _ in
|
||||
guard let self = self, self.model.activeBackend == .mpv else {
|
||||
guard let self, self.model.activeBackend == .mpv else {
|
||||
return
|
||||
}
|
||||
self.getTimeUpdates()
|
||||
}
|
||||
|
||||
networkStateTimer = .init(interval: .seconds(Self.networkStateUpdateInterval), mode: .infinite) { [weak self] _ in
|
||||
guard let self = self, self.model.activeBackend == .mpv else {
|
||||
guard let self, self.model.activeBackend == .mpv else {
|
||||
return
|
||||
}
|
||||
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
|
||||
|
||||
func canPlay(_ stream: Stream) -> Bool {
|
||||
stream.resolution != .unknown && stream.format != .av1
|
||||
stream.format != .av1
|
||||
}
|
||||
|
||||
func playStream(_ stream: Stream, of video: Video, preservingTime: Bool, upgrading: Bool) {
|
||||
@ -246,18 +252,7 @@ final class MPVBackend: PlayerBackend {
|
||||
|
||||
let startPlaying = {
|
||||
#if !os(macOS)
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setActive(true)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(self.handleAudioSessionInterruption(_:)),
|
||||
name: AVAudioSession.interruptionNotification,
|
||||
object: nil
|
||||
)
|
||||
} catch {
|
||||
self.logger.error("Error setting up audio session: \(error)")
|
||||
}
|
||||
self.model.setAudioSessionActive(true)
|
||||
#endif
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
@ -350,8 +345,20 @@ final class MPVBackend: PlayerBackend {
|
||||
startClientUpdates()
|
||||
}
|
||||
|
||||
func startRefreshRateUpdates() {
|
||||
refreshRateTimer.start()
|
||||
}
|
||||
|
||||
func stopRefreshRateUpdates() {
|
||||
refreshRateTimer.pause()
|
||||
}
|
||||
|
||||
func play() {
|
||||
#if !os(macOS)
|
||||
model.setAudioSessionActive(true)
|
||||
#endif
|
||||
startClientUpdates()
|
||||
startRefreshRateUpdates()
|
||||
|
||||
if controls.presentingControls {
|
||||
startControlsUpdates()
|
||||
@ -378,7 +385,11 @@ final class MPVBackend: PlayerBackend {
|
||||
}
|
||||
|
||||
func pause() {
|
||||
#if !os(macOS)
|
||||
model.setAudioSessionActive(false)
|
||||
#endif
|
||||
stopClientUpdates()
|
||||
stopRefreshRateUpdates()
|
||||
|
||||
client?.pause()
|
||||
isPaused = true
|
||||
@ -398,6 +409,11 @@ final class MPVBackend: PlayerBackend {
|
||||
}
|
||||
|
||||
func stop() {
|
||||
#if !os(macOS)
|
||||
model.setAudioSessionActive(false)
|
||||
#endif
|
||||
stopClientUpdates()
|
||||
stopRefreshRateUpdates()
|
||||
client?.stop()
|
||||
isPlaying = false
|
||||
isPaused = false
|
||||
@ -479,6 +495,52 @@ 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>!) {
|
||||
logger.info(.init(stringLiteral: "RECEIVED event: \(String(cString: mpv_event_name(event.pointee.event_id)))"))
|
||||
|
||||
@ -559,8 +621,14 @@ final class MPVBackend: PlayerBackend {
|
||||
}
|
||||
|
||||
func addSubTrack(_ url: URL) {
|
||||
client?.removeSubs()
|
||||
client?.addSubTrack(url)
|
||||
Task {
|
||||
if let areSubtitlesAdded = client?.areSubtitlesAdded {
|
||||
if await areSubtitlesAdded() {
|
||||
await client?.removeSubs()
|
||||
}
|
||||
}
|
||||
await client?.addSubTrack(url)
|
||||
}
|
||||
}
|
||||
|
||||
func setVideoToAuto() {
|
||||
@ -624,6 +692,17 @@ 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) {
|
||||
switch name {
|
||||
case "pause":
|
||||
@ -649,33 +728,4 @@ final class MPVBackend: PlayerBackend {
|
||||
logger.info("MPV backend received unhandled property: \(name)")
|
||||
}
|
||||
}
|
||||
|
||||
#if !os(macOS)
|
||||
@objc func handleAudioSessionInterruption(_ notification: Notification) {
|
||||
logger.info("Audio session interruption received.")
|
||||
|
||||
guard let info = notification.userInfo,
|
||||
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt
|
||||
else {
|
||||
logger.info("AVAudioSessionInterruptionTypeKey is missing or not a UInt in userInfo.")
|
||||
return
|
||||
}
|
||||
|
||||
let type = AVAudioSession.InterruptionType(rawValue: typeValue)
|
||||
|
||||
logger.info("Interruption type received: \(String(describing: type))")
|
||||
|
||||
switch type {
|
||||
case .began:
|
||||
pause()
|
||||
logger.info("Audio session interrupted.")
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self, name: AVAudioSession.interruptionNotification, object: nil)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ import Logging
|
||||
#if !os(macOS)
|
||||
import Siesta
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
final class MPVClient: ObservableObject {
|
||||
@ -14,6 +16,8 @@ final class MPVClient: ObservableObject {
|
||||
}
|
||||
|
||||
private var logger = Logger(label: "mpv-client")
|
||||
private var needsDrawingCooldown = false
|
||||
private var needsDrawingWorkItem: DispatchWorkItem?
|
||||
|
||||
var mpv: OpaquePointer!
|
||||
var mpvGL: OpaquePointer!
|
||||
@ -27,6 +31,7 @@ final class MPVClient: ObservableObject {
|
||||
var backend: MPVBackend!
|
||||
|
||||
var seeking = false
|
||||
var currentRefreshRate = 60
|
||||
|
||||
func create(frame: CGRect? = nil) {
|
||||
#if !os(macOS)
|
||||
@ -37,7 +42,7 @@ final class MPVClient: ObservableObject {
|
||||
|
||||
mpv = mpv_create()
|
||||
if mpv == nil {
|
||||
print("failed creating context\n")
|
||||
logger.critical("failed creating context\n")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
@ -74,6 +79,29 @@ final class MPVClient: ObservableObject {
|
||||
checkError(mpv_set_option_string(mpv, "user-agent", UserAgentManager.shared.userAgent))
|
||||
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 //
|
||||
|
||||
checkError(mpv_set_option_string(mpv, "hwdec", Defaults[.mpvHWdec]))
|
||||
@ -81,7 +109,6 @@ final class MPVClient: ObservableObject {
|
||||
|
||||
// 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, "opengl-swapinterval", "0"))
|
||||
|
||||
#if !os(macOS)
|
||||
checkError(mpv_set_option_string(mpv, "opengl-es", "yes"))
|
||||
@ -112,7 +139,7 @@ final class MPVClient: ObservableObject {
|
||||
get_proc_address_ctx: nil
|
||||
)
|
||||
|
||||
queue = DispatchQueue(label: "mpv")
|
||||
queue = DispatchQueue(label: "mpv", qos: .userInteractive, attributes: [.concurrent])
|
||||
|
||||
withUnsafeMutablePointer(to: &initParams) { initParams in
|
||||
var params = [
|
||||
@ -122,7 +149,7 @@ final class MPVClient: ObservableObject {
|
||||
]
|
||||
|
||||
if mpv_render_context_create(&mpvGL, mpv, ¶ms) < 0 {
|
||||
print("failed to initialize mpv GL context")
|
||||
logger.critical("failed to initialize mpv GL context")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
@ -318,6 +345,31 @@ final class MPVClient: ObservableObject {
|
||||
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) {
|
||||
guard !seeking else {
|
||||
logger.warning("ignoring seek, another in progress")
|
||||
@ -361,7 +413,7 @@ final class MPVClient: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
DispatchQueue.main.async(qos: .userInteractive) { [weak self] in
|
||||
guard let self else { return }
|
||||
let model = self.backend.model
|
||||
let aspectRatio = self.aspectRatio > 0 && self.aspectRatio < VideoPlayerView.defaultAspectRatio ? self.aspectRatio : VideoPlayerView.defaultAspectRatio
|
||||
@ -389,10 +441,30 @@ final class MPVClient: ObservableObject {
|
||||
}
|
||||
|
||||
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)")
|
||||
|
||||
// Set the cooldown flag to true and cancel any existing work item
|
||||
needsDrawingCooldown = true
|
||||
needsDrawingWorkItem?.cancel()
|
||||
|
||||
#if !os(macOS)
|
||||
glView?.needsDrawing = needsDrawing
|
||||
#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(
|
||||
@ -420,16 +492,59 @@ 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) {
|
||||
command("video-add", args: [url.absoluteString])
|
||||
}
|
||||
|
||||
func addSubTrack(_ url: URL) {
|
||||
command("sub-add", args: [url.absoluteString])
|
||||
func addSubTrack(_ url: URL) async {
|
||||
await Task {
|
||||
command("sub-add", args: [url.absoluteString])
|
||||
}.value
|
||||
}
|
||||
|
||||
func removeSubs() {
|
||||
command("sub-remove")
|
||||
func removeSubs() async {
|
||||
await Task {
|
||||
command("sub-remove")
|
||||
}.value
|
||||
}
|
||||
|
||||
func setVideoToAuto() {
|
||||
|
@ -153,7 +153,9 @@ extension PlayerBackend {
|
||||
// Filter out non-HLS streams and streams with resolution more than maxResolution
|
||||
let nonHLSStreams = streams.filter {
|
||||
let isHLS = $0.kind == .hls
|
||||
let isWithinResolution = $0.resolution <= maxResolution.value
|
||||
// Check if the stream's resolution is within the maximum allowed resolution
|
||||
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("Is HLS: \(isHLS), Is within resolution: \(isWithinResolution)")
|
||||
return !isHLS && isWithinResolution
|
||||
@ -187,6 +189,7 @@ extension PlayerBackend {
|
||||
}
|
||||
|
||||
let filteredStreams = adjustedStreams.filter { stream in
|
||||
// Check if the stream's resolution is within the maximum allowed resolution
|
||||
let isWithinResolution = stream.resolution <= maxResolution.value
|
||||
logger.info("Filtered stream ID: \(stream.id) - Is within max resolution: \(isWithinResolution)")
|
||||
return isWithinResolution
|
||||
|
@ -47,7 +47,7 @@ final class PlayerModel: ObservableObject {
|
||||
|
||||
static var shared = PlayerModel()
|
||||
|
||||
let logger = Logger(label: "stream.yattee.app")
|
||||
let logger = Logger(label: "stream.yattee.player.model")
|
||||
|
||||
var playerItem: AVPlayerItem?
|
||||
|
||||
@ -130,7 +130,15 @@ final class PlayerModel: ObservableObject {
|
||||
|
||||
#if os(iOS)
|
||||
@Published var lockedOrientation: UIInterfaceOrientationMask?
|
||||
@Default(.rotateToLandscapeOnEnterFullScreen) private var rotateToLandscapeOnEnterFullScreen
|
||||
@Published var isOrientationLocked: Bool {
|
||||
didSet {
|
||||
Defaults[.isOrientationLocked] = isOrientationLocked
|
||||
}
|
||||
}
|
||||
|
||||
@Default(.rotateToLandscapeOnEnterFullScreen) var rotateToLandscapeOnEnterFullScreen
|
||||
@Default(.lockPortraitWhenBrowsing) var lockPortraitWhenBrowsing
|
||||
var fullscreenInitiatedByButton = false
|
||||
#endif
|
||||
|
||||
@Published var currentChapterIndex: Int?
|
||||
@ -195,14 +203,43 @@ final class PlayerModel: ObservableObject {
|
||||
var rateToRestore: Float?
|
||||
private var remoteCommandCenterConfigured = false
|
||||
|
||||
// Used in the PlayerModel extension in PlayerQueue
|
||||
var retryAttempts = [String: Int]()
|
||||
|
||||
#if os(macOS)
|
||||
var keyPressMonitor: Any?
|
||||
#endif
|
||||
|
||||
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)
|
||||
mpvBackend.controller = mpvController
|
||||
mpvBackend.client = mpvController.client
|
||||
|
||||
// Register for audio session interruption notifications
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleAudioSessionInterruption(_:)),
|
||||
name: AVAudioSession.interruptionNotification,
|
||||
object: nil
|
||||
)
|
||||
|
||||
// Register for audio session route change notifications
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleRouteChange(_:)),
|
||||
name: AVAudioSession.routeChangeNotification,
|
||||
object: AVAudioSession.sharedInstance()
|
||||
)
|
||||
#endif
|
||||
|
||||
playbackMode = Defaults[.playbackMode]
|
||||
@ -219,6 +256,20 @@ final class PlayerModel: ObservableObject {
|
||||
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() {
|
||||
#if os(macOS)
|
||||
if presentingPlayer {
|
||||
@ -502,7 +553,10 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
|
||||
private func handlePresentationChange() {
|
||||
backend.setNeedsDrawing(presentingPlayer)
|
||||
#if os(macOS)
|
||||
// TODO: Check whether this is needed on macOS
|
||||
backend.setNeedsDrawing(presentingPlayer)
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
if presentingPlayer, activeBackend == .appleAVPlayer, avPlayerUsesSystemControls, Constants.isIPhone {
|
||||
@ -531,13 +585,11 @@ final class PlayerModel: ObservableObject {
|
||||
|
||||
if !presentingPlayer {
|
||||
#if os(iOS)
|
||||
if Defaults[.lockPortraitWhenBrowsing] {
|
||||
if lockPortraitWhenBrowsing {
|
||||
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
||||
} else {
|
||||
Orientation.lockOrientation(.allButUpsideDown)
|
||||
Orientation.lockOrientation(.all)
|
||||
}
|
||||
|
||||
OrientationModel.shared.stopOrientationUpdates()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@ -644,32 +696,37 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
|
||||
func closeCurrentItem(finished: Bool = false) {
|
||||
pause()
|
||||
videoBeingOpened = nil
|
||||
advancing = false
|
||||
forceBackendOnPlay = nil
|
||||
|
||||
guard !closing else { return }
|
||||
closing = true
|
||||
controls.presentingControls = false
|
||||
|
||||
self.prepareCurrentItemForHistory(finished: finished)
|
||||
if playingFullScreen { exitFullScreen() }
|
||||
|
||||
self.hide()
|
||||
|
||||
Delay.by(0.8) { [weak self] in
|
||||
Delay.by(0.3) { [weak self] in
|
||||
guard let self else { return }
|
||||
self.closePiP()
|
||||
pause()
|
||||
videoBeingOpened = nil
|
||||
advancing = false
|
||||
forceBackendOnPlay = nil
|
||||
|
||||
withAnimation {
|
||||
self.currentItem = nil
|
||||
controls.presentingControls = false
|
||||
|
||||
self.prepareCurrentItemForHistory(finished: finished)
|
||||
self.hide()
|
||||
|
||||
Delay.by(0.7) { [weak self] in
|
||||
guard let self else { return }
|
||||
if playingInPictureInPicture { self.closePiP() }
|
||||
|
||||
withAnimation {
|
||||
self.currentItem = nil
|
||||
}
|
||||
|
||||
self.updateNowPlayingInfo()
|
||||
self.backend.closeItem()
|
||||
self.aspectRatio = VideoPlayerView.defaultAspectRatio
|
||||
self.resetAutoplay()
|
||||
self.closing = false
|
||||
}
|
||||
self.updateNowPlayingInfo()
|
||||
|
||||
self.backend.closeItem()
|
||||
self.aspectRatio = VideoPlayerView.defaultAspectRatio
|
||||
self.resetAutoplay()
|
||||
self.closing = false
|
||||
self.playingFullScreen = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -678,38 +735,24 @@ final class PlayerModel: ObservableObject {
|
||||
avPlayerBackend.startPictureInPictureOnPlay = false
|
||||
avPlayerBackend.startPictureInPictureOnSwitch = false
|
||||
|
||||
if activeBackend == .appleAVPlayer {
|
||||
guard activeBackend != .appleAVPlayer else {
|
||||
avPlayerBackend.tryStartingPictureInPicture()
|
||||
return
|
||||
}
|
||||
|
||||
// First, we need to create an array with supported formats.
|
||||
let formatOrderPiP: [QualityProfile.Format] = [.hls, .stream, .mp4]
|
||||
avPlayerBackend.startPictureInPictureOnSwitch = true
|
||||
|
||||
guard let video = currentVideo else { return }
|
||||
guard let stream = avPlayerBackend.bestPlayable(availableStreams, maxResolution: .hd720p30, formatOrder: formatOrderPiP) else { return }
|
||||
|
||||
if avPlayerBackend.video == video {
|
||||
if activeBackend != .appleAVPlayer {
|
||||
avPlayerBackend.startPictureInPictureOnSwitch = true
|
||||
}
|
||||
changeActiveBackend(from: activeBackend, to: .appleAVPlayer)
|
||||
} else {
|
||||
avPlayerBackend.startPictureInPictureOnPlay = true
|
||||
playStream(stream, of: video, preservingTime: true, upgrading: true, withBackend: avPlayerBackend)
|
||||
}
|
||||
|
||||
var retryCount = 0
|
||||
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
|
||||
if let pipController = self?.pipController, pipController.isPictureInPictureActive, self?.avPlayerBackend.isPlaying == true {
|
||||
self?.exitFullScreen()
|
||||
self?.controls.objectWillChange.send()
|
||||
timer.invalidate()
|
||||
} else if retryCount < 3, self?.activeBackend == .appleAVPlayer, self?.avPlayerBackend.startPictureInPictureOnSwitch == false {
|
||||
// If PiP didn't start, try starting it again up to 3 times,
|
||||
self?.avPlayerBackend.startPictureInPictureOnSwitch = true
|
||||
self?.avPlayerBackend.tryStartingPictureInPicture()
|
||||
retryCount += 1
|
||||
saveTime {
|
||||
self.changeActiveBackend(from: .mpv, to: .appleAVPlayer)
|
||||
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
|
||||
if let pipController = self?.pipController, pipController.isPictureInPictureActive, self?.avPlayerBackend.isPlaying == true {
|
||||
self?.exitFullScreen()
|
||||
self?.controls.objectWillChange.send()
|
||||
timer.invalidate()
|
||||
} else if self?.activeBackend == .appleAVPlayer, self?.avPlayerBackend.startPictureInPictureOnSwitch == false {
|
||||
self?.avPlayerBackend.startPictureInPictureOnSwitch = true
|
||||
self?.avPlayerBackend.tryStartingPictureInPicture()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -739,19 +782,27 @@ final class PlayerModel: ObservableObject {
|
||||
show()
|
||||
#endif
|
||||
|
||||
if previousActiveBackend == .mpv {
|
||||
saveTime {
|
||||
self.changeActiveBackend(from: self.activeBackend, to: .mpv, isInClosePip: true)
|
||||
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
|
||||
if self?.activeBackend == .mpv, self?.mpvBackend.isPlaying == true {
|
||||
self?.backend.closePiP()
|
||||
self?.controls.resetTimer()
|
||||
timer.invalidate()
|
||||
}
|
||||
avPlayerBackend.closePiP()
|
||||
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
|
||||
if self?.activeBackend == .appleAVPlayer, self?.avPlayerBackend.isPlaying == true, self?.playingInPictureInPicture == false {
|
||||
timer.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
guard previousActiveBackend == .mpv else { return }
|
||||
|
||||
saveTime {
|
||||
self.changeActiveBackend(from: .appleAVPlayer, to: .mpv, isInClosePip: true)
|
||||
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
|
||||
if self?.activeBackend == .mpv, self?.mpvBackend.isPlaying == true {
|
||||
timer.invalidate()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
backend.closePiP()
|
||||
}
|
||||
|
||||
// We need to remove the itme from the player, if not it will be displayed when next video goe to PiP.
|
||||
Delay.by(1.0) {
|
||||
self.avPlayerBackend.closeItem()
|
||||
}
|
||||
}
|
||||
|
||||
@ -764,7 +815,7 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
|
||||
func toggleFullScreenAction() {
|
||||
toggleFullscreen(playingFullScreen, showControls: false)
|
||||
toggleFullscreen(playingFullScreen, showControls: false, initiatedByButton: true)
|
||||
}
|
||||
|
||||
func togglePiPAction() {
|
||||
@ -777,20 +828,21 @@ final class PlayerModel: ObservableObject {
|
||||
|
||||
#if os(iOS)
|
||||
var lockOrientationImage: String {
|
||||
lockedOrientation.isNil ? "lock.rotation.open" : "lock.rotation"
|
||||
isOrientationLocked ? "lock.rotation" : "lock.rotation.open"
|
||||
}
|
||||
|
||||
func lockOrientationAction() {
|
||||
if lockedOrientation.isNil {
|
||||
// This makes toggling orientation lock more robust
|
||||
if lockedOrientation.isNil || !isOrientationLocked {
|
||||
isOrientationLocked = true
|
||||
let orientationMask = OrientationTracker.shared.currentInterfaceOrientationMask
|
||||
lockedOrientation = orientationMask
|
||||
let orientation = OrientationTracker.shared.currentInterfaceOrientation
|
||||
Orientation.lockOrientation(orientationMask, andRotateTo: .landscapeLeft)
|
||||
// iOS 16 workaround
|
||||
Orientation.lockOrientation(orientationMask, andRotateTo: orientation)
|
||||
Orientation.lockOrientation(orientationMask, andRotateTo: playingFullScreen ? nil : orientation)
|
||||
} else {
|
||||
isOrientationLocked = false
|
||||
lockedOrientation = nil
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: OrientationTracker.shared.currentInterfaceOrientation)
|
||||
Orientation.lockOrientation(.all)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -880,26 +932,29 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
|
||||
func updateRemoteCommandCenter() {
|
||||
let skipForwardCommand = MPRemoteCommandCenter.shared().skipForwardCommand
|
||||
let skipBackwardCommand = MPRemoteCommandCenter.shared().skipBackwardCommand
|
||||
let previousTrackCommand = MPRemoteCommandCenter.shared().previousTrackCommand
|
||||
let nextTrackCommand = MPRemoteCommandCenter.shared().nextTrackCommand
|
||||
let commandCenter = MPRemoteCommandCenter.shared()
|
||||
let skipForwardCommand = commandCenter.skipForwardCommand
|
||||
let skipBackwardCommand = commandCenter.skipBackwardCommand
|
||||
let previousTrackCommand = commandCenter.previousTrackCommand
|
||||
let nextTrackCommand = commandCenter.nextTrackCommand
|
||||
|
||||
if !remoteCommandCenterConfigured {
|
||||
remoteCommandCenterConfigured = true
|
||||
|
||||
#if !os(macOS)
|
||||
try? AVAudioSession.sharedInstance().setCategory(
|
||||
.playback,
|
||||
mode: .moviePlayback
|
||||
)
|
||||
|
||||
UIApplication.shared.beginReceivingRemoteControlEvents()
|
||||
#endif
|
||||
|
||||
let interval = TimeInterval(systemControlsSeekDuration) ?? 10
|
||||
let preferredIntervals = [NSNumber(value: interval)]
|
||||
|
||||
// Remove existing targets to avoid duplicates
|
||||
skipForwardCommand.removeTarget(nil)
|
||||
skipBackwardCommand.removeTarget(nil)
|
||||
previousTrackCommand.removeTarget(nil)
|
||||
nextTrackCommand.removeTarget(nil)
|
||||
commandCenter.playCommand.removeTarget(nil)
|
||||
commandCenter.pauseCommand.removeTarget(nil)
|
||||
commandCenter.togglePlayPauseCommand.removeTarget(nil)
|
||||
commandCenter.changePlaybackPositionCommand.removeTarget(nil)
|
||||
|
||||
// Re-add targets for handling commands
|
||||
skipForwardCommand.preferredIntervals = preferredIntervals
|
||||
skipBackwardCommand.preferredIntervals = preferredIntervals
|
||||
|
||||
@ -923,22 +978,22 @@ final class PlayerModel: ObservableObject {
|
||||
return .success
|
||||
}
|
||||
|
||||
MPRemoteCommandCenter.shared().playCommand.addTarget { [weak self] _ in
|
||||
commandCenter.playCommand.addTarget { [weak self] _ in
|
||||
self?.play()
|
||||
return .success
|
||||
}
|
||||
|
||||
MPRemoteCommandCenter.shared().pauseCommand.addTarget { [weak self] _ in
|
||||
commandCenter.pauseCommand.addTarget { [weak self] _ in
|
||||
self?.pause()
|
||||
return .success
|
||||
}
|
||||
|
||||
MPRemoteCommandCenter.shared().togglePlayPauseCommand.addTarget { [weak self] _ in
|
||||
commandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in
|
||||
self?.togglePlay()
|
||||
return .success
|
||||
}
|
||||
|
||||
MPRemoteCommandCenter.shared().changePlaybackPositionCommand.addTarget { [weak self] remoteEvent in
|
||||
commandCenter.changePlaybackPositionCommand.addTarget { [weak self] remoteEvent in
|
||||
guard let event = remoteEvent as? MPChangePlaybackPositionCommandEvent else { return .commandFailed }
|
||||
|
||||
self?.backend.seek(to: event.positionTime, seekType: .userInteracted)
|
||||
@ -973,25 +1028,43 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
#else
|
||||
func handleEnterForeground() {
|
||||
setNeedsDrawing(presentingPlayer)
|
||||
DispatchQueue.global(qos: .userInteractive).async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
if !musicMode, activeBackend == .appleAVPlayer {
|
||||
avPlayerBackend.bindPlayerToLayer()
|
||||
if !self.musicMode, self.activeBackend == .mpv {
|
||||
self.mpvBackend.addVideoTrackFromStream()
|
||||
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 {
|
||||
return
|
||||
}
|
||||
|
||||
show()
|
||||
closePiP()
|
||||
// Needs to be delayed a bit, otherwise the PiP windows stays open
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||
self?.closePiP()
|
||||
}
|
||||
}
|
||||
|
||||
func handleEnterBackground() {
|
||||
#if os(iOS)
|
||||
OrientationTracker.shared.stopDeviceOrientationTracking()
|
||||
#endif
|
||||
|
||||
if Defaults[.pauseOnEnteringBackground], !playingInPictureInPicture, !musicMode {
|
||||
pause()
|
||||
} else if !playingInPictureInPicture {
|
||||
} else if !playingInPictureInPicture, activeBackend == .appleAVPlayer {
|
||||
avPlayerBackend.removePlayerFromLayer()
|
||||
} else if activeBackend == .mpv, !musicMode {
|
||||
mpvBackend.setVideoToNo()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -1017,18 +1090,22 @@ final class PlayerModel: ObservableObject {
|
||||
guard activeBackend == .mpv else { return }
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
if activeBackend == .appleAVPlayer, avPlayerUsesSystemControls {
|
||||
return
|
||||
}
|
||||
#endif
|
||||
|
||||
guard let video = currentItem?.video else {
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = .none
|
||||
return
|
||||
}
|
||||
|
||||
let currentTime = (backend.currentTime?.seconds.isFinite ?? false) ? backend.currentTime!.seconds : 0
|
||||
|
||||
// Determine the media type based on musicMode
|
||||
let mediaType: NSNumber
|
||||
if musicMode {
|
||||
mediaType = MPMediaType.anyAudio.rawValue as NSNumber
|
||||
} else {
|
||||
mediaType = MPMediaType.anyVideo.rawValue as NSNumber
|
||||
}
|
||||
|
||||
// Prepare the Now Playing info dictionary
|
||||
var nowPlayingInfo: [String: AnyObject] = [
|
||||
MPMediaItemPropertyTitle: video.displayTitle as AnyObject,
|
||||
MPMediaItemPropertyArtist: video.displayAuthor as AnyObject,
|
||||
@ -1036,7 +1113,7 @@ final class PlayerModel: ObservableObject {
|
||||
MPNowPlayingInfoPropertyElapsedPlaybackTime: currentTime as AnyObject,
|
||||
MPNowPlayingInfoPropertyPlaybackQueueCount: queue.count as AnyObject,
|
||||
MPNowPlayingInfoPropertyPlaybackQueueIndex: 1 as AnyObject,
|
||||
MPMediaItemPropertyMediaType: MPMediaType.anyVideo.rawValue as AnyObject
|
||||
MPMediaItemPropertyMediaType: mediaType
|
||||
]
|
||||
|
||||
if !currentArtwork.isNil {
|
||||
@ -1057,7 +1134,7 @@ final class PlayerModel: ObservableObject {
|
||||
|
||||
func updateCurrentArtwork() {
|
||||
guard let video = currentVideo,
|
||||
let thumbnailURL = video.thumbnailURL(quality: .medium)
|
||||
let thumbnailURL = video.thumbnailURL(quality: Constants.isIPhone ? .medium : .maxres)
|
||||
else {
|
||||
return
|
||||
}
|
||||
@ -1079,7 +1156,7 @@ final class PlayerModel: ObservableObject {
|
||||
task.resume()
|
||||
}
|
||||
|
||||
func toggleFullscreen(_ isFullScreen: Bool, showControls: Bool = true) {
|
||||
func toggleFullscreen(_ isFullScreen: Bool, showControls: Bool = true, initiatedByButton: Bool = false) {
|
||||
controls.presentingControls = showControls && isFullScreen
|
||||
|
||||
#if os(macOS)
|
||||
@ -1091,18 +1168,27 @@ final class PlayerModel: ObservableObject {
|
||||
#if os(iOS)
|
||||
if playingFullScreen {
|
||||
if activeBackend == .appleAVPlayer, avPlayerUsesSystemControls {
|
||||
fullscreenInitiatedByButton = initiatedByButton
|
||||
avPlayerBackend.controller.enterFullScreen(animated: true)
|
||||
return
|
||||
}
|
||||
guard rotateToLandscapeOnEnterFullScreen.isRotating else { return }
|
||||
let lockOrientation = rotateToLandscapeOnEnterFullScreen.interfaceOrientation
|
||||
if currentVideoIsLandscape {
|
||||
let delay = activeBackend == .appleAVPlayer && avPlayerUsesSystemControls ? 0.8 : 0
|
||||
// not sure why but first rotation call is ignore so doing rotate to same orientation first
|
||||
Delay.by(delay) {
|
||||
let orientation = OrientationTracker.shared.currentDeviceOrientation.isLandscape ? OrientationTracker.shared.currentInterfaceOrientation : self.rotateToLandscapeOnEnterFullScreen.interaceOrientation
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: OrientationTracker.shared.currentInterfaceOrientation)
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: orientation)
|
||||
if initiatedByButton {
|
||||
Orientation.lockOrientation(isOrientationLocked
|
||||
? (lockOrientation == .landscapeRight ? .landscapeRight : .landscapeLeft)
|
||||
: .landscape)
|
||||
}
|
||||
let orientation = OrientationTracker.shared.currentDeviceOrientation.isLandscape
|
||||
? OrientationTracker.shared.currentInterfaceOrientation
|
||||
: rotateToLandscapeOnEnterFullScreen.interfaceOrientation
|
||||
|
||||
Orientation.lockOrientation(
|
||||
isOrientationLocked
|
||||
? (lockOrientation == .landscapeRight ? .landscapeRight : .landscapeLeft)
|
||||
: .all,
|
||||
andRotateTo: orientation
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if activeBackend == .appleAVPlayer, avPlayerUsesSystemControls {
|
||||
@ -1110,10 +1196,12 @@ final class PlayerModel: ObservableObject {
|
||||
avPlayerBackend.controller.dismiss(animated: true)
|
||||
return
|
||||
}
|
||||
let rotationOrientation = Constants.isIPhone ? UIInterfaceOrientation.portrait : nil
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: rotationOrientation)
|
||||
if lockPortraitWhenBrowsing {
|
||||
lockedOrientation = UIInterfaceOrientationMask.portrait
|
||||
}
|
||||
let rotationOrientation = lockPortraitWhenBrowsing ? UIInterfaceOrientation.portrait : nil
|
||||
Orientation.lockOrientation(lockPortraitWhenBrowsing ? .portrait : .all, andRotateTo: rotationOrientation)
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -1203,9 +1291,143 @@ final class PlayerModel: ObservableObject {
|
||||
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)
|
||||
private func assignKeyPressMonitor() {
|
||||
keyPressMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { keyEvent -> NSEvent? in
|
||||
keyPressMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] 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 {
|
||||
case 124:
|
||||
if !self.liveStreamInAVPlayer {
|
||||
@ -1240,7 +1462,7 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
|
||||
private func destroyKeyPressMonitor() {
|
||||
if let keyPressMonitor = keyPressMonitor {
|
||||
if let keyPressMonitor {
|
||||
NSEvent.removeMonitor(keyPressMonitor)
|
||||
}
|
||||
}
|
||||
|
@ -359,6 +359,31 @@ extension PlayerModel {
|
||||
}
|
||||
|
||||
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
|
||||
if let errorDictionary = error.json.dictionaryObject,
|
||||
let errorMessage = errorDictionary["message"] ?? errorDictionary["error"],
|
||||
|
@ -6,12 +6,12 @@ struct QualityProfile: Hashable, Identifiable, Defaults.Serializable {
|
||||
static var defaultProfile = Self(id: "default", backend: .mpv, resolution: .hd720p60, formats: [.stream], order: Array(Format.allCases.indices))
|
||||
|
||||
enum Format: String, CaseIterable, Identifiable, Defaults.Serializable {
|
||||
case hls
|
||||
case stream
|
||||
case avc1
|
||||
case stream
|
||||
case webm
|
||||
case mp4
|
||||
case av1
|
||||
case webm
|
||||
case hls
|
||||
|
||||
var id: String {
|
||||
rawValue
|
||||
@ -30,18 +30,18 @@ struct QualityProfile: Hashable, Identifiable, Defaults.Serializable {
|
||||
|
||||
var streamFormat: Stream.Format? {
|
||||
switch self {
|
||||
case .hls:
|
||||
return nil
|
||||
case .stream:
|
||||
return nil
|
||||
case .avc1:
|
||||
return .avc1
|
||||
case .stream:
|
||||
return nil
|
||||
case .webm:
|
||||
return .webm
|
||||
case .mp4:
|
||||
return .mp4
|
||||
case .av1:
|
||||
return .av1
|
||||
case .webm:
|
||||
return .webm
|
||||
case .hls:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -59,14 +59,16 @@ struct QualityProfile: Hashable, Identifiable, Defaults.Serializable {
|
||||
}
|
||||
|
||||
var formatsDescription: String {
|
||||
if formats.count == Format.allCases.count {
|
||||
switch formats.count {
|
||||
case Format.allCases.count:
|
||||
return "Any format".localized()
|
||||
}
|
||||
if formats.count <= 3 {
|
||||
case 0:
|
||||
return "No format selected".localized()
|
||||
case 1 ... 3:
|
||||
return formats.map(\.description).joined(separator: ", ")
|
||||
default:
|
||||
return String(format: "%@ formats".localized(), String(formats.count))
|
||||
}
|
||||
|
||||
return String(format: "%@ formats".localized(), String(formats.count))
|
||||
}
|
||||
|
||||
func isPreferred(_ stream: Stream) -> Bool {
|
||||
@ -74,7 +76,8 @@ struct QualityProfile: Hashable, Identifiable, Defaults.Serializable {
|
||||
return true
|
||||
}
|
||||
|
||||
let resolutionMatch = !stream.resolution.isNil && resolution.value >= stream.resolution
|
||||
let defaultResolution = Stream.Resolution.custom(height: 720, refreshRate: 30)
|
||||
let resolutionMatch = resolution.value ?? defaultResolution >= stream.resolution
|
||||
|
||||
if resolutionMatch, formats.contains(.stream), stream.kind == .stream {
|
||||
return true
|
||||
|
@ -18,6 +18,8 @@ final class SearchModel: ObservableObject {
|
||||
|
||||
@Published var focused = false
|
||||
|
||||
@Default(.showSearchSuggestions) private var showSearchSuggestions
|
||||
|
||||
#if os(iOS)
|
||||
var textField: UITextField!
|
||||
#elseif os(macOS)
|
||||
@ -102,7 +104,7 @@ final class SearchModel: ObservableObject {
|
||||
}}
|
||||
|
||||
func loadSuggestions(_ query: String) {
|
||||
guard accounts.app.supportsSearchSuggestions else {
|
||||
guard accounts.app.supportsSearchSuggestions, showSearchSuggestions else {
|
||||
querySuggestions.removeAll()
|
||||
return
|
||||
}
|
||||
|
@ -4,288 +4,126 @@ import Foundation
|
||||
|
||||
// swiftlint:disable:next final_class
|
||||
class Stream: Equatable, Hashable, Identifiable {
|
||||
enum Resolution: String, CaseIterable, Comparable, Defaults.Serializable {
|
||||
// Some 16:19 and 16:10 resolutions are also used in 2:1 videos
|
||||
enum Resolution: Comparable, Codable, Defaults.Serializable {
|
||||
case predefined(PredefinedResolution)
|
||||
case custom(height: Int, refreshRate: Int)
|
||||
|
||||
// 8K UHD (16:9) Resolutions
|
||||
case hd4320p60
|
||||
case hd4320p50
|
||||
case hd4320p48
|
||||
case hd4320p30
|
||||
case hd4320p25
|
||||
case hd4320p24
|
||||
enum PredefinedResolution: String, CaseIterable, Codable {
|
||||
// 8K UHD (16:9) Resolutions
|
||||
case hd4320p60, hd4320p30
|
||||
|
||||
// 5K (16:9) Resolutions
|
||||
case hd2560p60
|
||||
case hd2560p50
|
||||
case hd2560p48
|
||||
case hd2560p30
|
||||
case hd2560p25
|
||||
case hd2560p24
|
||||
// 4K UHD (16:9) Resolutions
|
||||
case hd2160p60, hd2160p30
|
||||
|
||||
// 2:1 Aspect Ratio (Univisium) Resolutions
|
||||
case hd2880p60
|
||||
case hd2880p50
|
||||
case hd2880p48
|
||||
case hd2880p30
|
||||
case hd2880p25
|
||||
case hd2880p24
|
||||
// 1440p (16:9) Resolutions
|
||||
case hd1440p60, hd1440p30
|
||||
|
||||
// 16:10 Resolutions
|
||||
case hd2400p60
|
||||
case hd2400p50
|
||||
case hd2400p48
|
||||
case hd2400p30
|
||||
case hd2400p25
|
||||
case hd2400p24
|
||||
// 1080p (Full HD, 16:9) Resolutions
|
||||
case hd1080p60, hd1080p30
|
||||
|
||||
// 16:9 Resolutions
|
||||
case hd2160p60
|
||||
case hd2160p50
|
||||
case hd2160p48
|
||||
case hd2160p30
|
||||
case hd2160p25
|
||||
case hd2160p24
|
||||
// 720p (HD, 16:9) Resolutions
|
||||
case hd720p60, hd720p30
|
||||
|
||||
// 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
|
||||
case sd854p30
|
||||
case sd854p25
|
||||
case sd768p30
|
||||
case sd768p25
|
||||
case sd640p30
|
||||
case sd640p25
|
||||
case sd480p30
|
||||
case sd480p25
|
||||
|
||||
case sd428p30
|
||||
case sd428p25
|
||||
case sd360p30
|
||||
case sd360p25
|
||||
case sd320p30
|
||||
case sd320p25
|
||||
case sd240p30
|
||||
case sd240p25
|
||||
case sd214p30
|
||||
case sd214p25
|
||||
case sd144p30
|
||||
case sd144p25
|
||||
case sd128p30
|
||||
case sd128p25
|
||||
|
||||
case unknown
|
||||
// Standard Definition (SD) Resolutions
|
||||
case sd480p30
|
||||
case sd360p30
|
||||
case sd240p30
|
||||
case sd144p30
|
||||
}
|
||||
|
||||
var name: String {
|
||||
"\(height)p\(refreshRate != -1 && refreshRate != 30 ? ", \(refreshRate) fps" : "")"
|
||||
switch self {
|
||||
case let .predefined(predefined):
|
||||
return predefined.rawValue
|
||||
case let .custom(height, refreshRate):
|
||||
return "\(height)p\(refreshRate != 30 ? ", \(refreshRate) fps" : "")"
|
||||
}
|
||||
}
|
||||
|
||||
var height: Int {
|
||||
if self == .unknown {
|
||||
return -1
|
||||
switch self {
|
||||
case let .predefined(predefined):
|
||||
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 {
|
||||
if self == .unknown {
|
||||
return -1
|
||||
switch self {
|
||||
case let .predefined(predefined):
|
||||
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 {
|
||||
switch self {
|
||||
// 8K UHD (16:9) Resolutions
|
||||
case .hd4320p60, .hd4320p50, .hd4320p48, .hd4320p30, .hd4320p25, .hd4320p24:
|
||||
return 85_000_000 // 85 Mbit/s
|
||||
|
||||
// 5K (16:9) Resolutions
|
||||
case .hd2880p60, .hd2880p50, .hd2880p48, .hd2880p30, .hd2880p25, .hd2880p24:
|
||||
return 45_000_000 // 45 Mbit/s
|
||||
|
||||
// 2:1 Aspect Ratio (Univisium) Resolutions
|
||||
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
|
||||
case let .predefined(predefined):
|
||||
return predefined.bitrate
|
||||
case let .custom(height, refreshRate):
|
||||
// Find the closest predefined resolution based on height and refresh rate
|
||||
let closestPredefined = Stream.Resolution.PredefinedResolution.allCases.min {
|
||||
abs($0.height - height) + abs($0.refreshRate - refreshRate) <
|
||||
abs($1.height - height) + abs($1.refreshRate - refreshRate)
|
||||
}
|
||||
// Return the bitrate of the closest predefined resolution or a default bitrate if no close match is found
|
||||
return closestPredefined?.bitrate ?? 5_000_000
|
||||
}
|
||||
}
|
||||
|
||||
static func from(resolution: String, fps: Int? = nil) -> Self {
|
||||
allCases.first { $0.rawValue.contains(resolution) && $0.refreshRate == (fps ?? 30) } ?? .unknown
|
||||
if let predefined = PredefinedResolution(rawValue: resolution) {
|
||||
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 {
|
||||
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 {
|
||||
@ -478,3 +316,97 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,10 +5,27 @@ final class ThumbnailsModel: ObservableObject {
|
||||
static var shared = ThumbnailsModel()
|
||||
|
||||
@Published var unloadable = Set<URL>()
|
||||
private var retryCounts = [URL: Int]()
|
||||
private let maxRetries = 3
|
||||
private let retryDelay: TimeInterval = 1.0
|
||||
|
||||
func insertUnloadable(_ url: URL) {
|
||||
DispatchQueue.main.async {
|
||||
self.unloadable.insert(url)
|
||||
let retries = (retryCounts[url] ?? 0) + 1
|
||||
|
||||
if retries >= maxRetries {
|
||||
DispatchQueue.main.async {
|
||||
self.unloadable.insert(url)
|
||||
self.retryCounts.removeValue(forKey: url)
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.retryCounts[url] = retries
|
||||
}
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + retryDelay) {
|
||||
DispatchQueue.main.async {
|
||||
self.retryCounts[url] = retries
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,17 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Invidious.svg",
|
||||
"filename" : "Invidious_512x512@1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Invidious_512x512@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Invidious_512x512@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
|
@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="512pt" height="512pt" version="1.0" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><g><rect x="-.0072516" y=".00056299" width="512.01" height="512.02" fill="#575757" stroke-width=".063019"/><path d="m247.17 455.95c-19.792-0.78921-38.719-4.2564-57.154-10.47-60.968-20.55-108.68-68.579-127-127.86-7.8955-25.538-10.062-53.943-6.2586-82.067 3.7105-27.439 13.603-53.515 29.342-77.344 12.069-18.273 29.138-36.277 47.228-49.816 36.891-27.61 85.944-42.49 132.38-40.157 25.88 1.3001 49.939 6.765 73.106 16.606 8.1948 3.481 20.024 9.6845 27.696 14.525 14.15 8.9272 22.367 15.498 34.482 27.573 13.254 13.211 22.128 24.276 30.398 37.906 7.2081 11.879 14.099 27.15 18.229 40.397 1.5996 5.1305 4.442 16.456 5.6852 22.653 2.3908 11.917 2.6998 15.722 2.7049 33.312 6e-3 18.515-0.46256 24.413-2.9166 36.758-9.3274 46.92-35.58 88.167-74.872 117.64-22.814 17.112-50.027 29.535-78.547 35.858-16.714 3.7059-35.421 5.2453-54.498 4.4846zm-35.1-78.786c-5.3e-4 -0.52647-0.0741-2.0564-0.16311-3.3999l-0.16178-2.4427-4.7018-0.26271c-4.0477-0.22614-4.7968-0.33363-5.3847-0.77253-2.0235-1.5108-1.4679-6.0695 2.2494-18.457 0.8637-2.8781 3.3371-11.321 5.4966-18.762 2.1594-7.4409 5.2002-17.836 6.7573-23.101 1.5571-5.2648 4.1948-14.282 5.8615-20.038 1.6667-5.7562 3.6145-12.4 4.3284-14.764 0.71391-2.3641 3.2583-11.037 5.6542-19.272 4.9475-17.007 8.1626-27.723 8.9438-29.811 0.51852-1.3858 0.54785-1.4139 0.99761-0.95317 0.25486 0.26106 3.8462 7.3667 7.9807 15.79 4.1345 8.4236 13.089 26.573 19.898 40.331 17.188 34.73 37.849 76.578 43.261 87.622l4.5356 9.257 11.359-0.0895c6.2475-0.0492 11.615-0.19623 11.929-0.32672 0.5614-0.23385 0.54167-0.2959-1.3723-4.3176-1.068-2.2442-8.1436-16.601-15.724-31.904-48.687-98.293-61.22-123.86-67.889-138.48-4.7022-10.309-6.9031-14.807-7.7139-15.762-0.82931-0.97742-1.6319-1.0638-2.3704-0.25525-1.1993 1.313-4.1046 10.063-9.3869 28.27-2.0569 7.0899-6.5372 22.425-9.9562 34.077-6.6396 22.629-8.5182 29.037-14.33 48.883-2.0354 6.9495-4.7977 16.369-6.1385 20.931-1.3408 4.5628-4.033 13.81-5.9826 20.549-4.304 14.877-6.136 20.889-7.3886 24.25-2.1371 5.7334-2.5723 6.3292-4.9216 6.7384-0.88855 0.15472-2.4102 0.28196-3.3815 0.28275-2.1993 3e-3 -3.5494 0.36339-4.0558 1.0863-0.42176 0.60215-0.56421 4.8802-0.18251 5.4812 0.20573 0.32388 2.4672 0.37414 23.34 0.51873l8.6151 0.0597-7e-4 -0.95723zm36.751-205.59c4.3282-0.92335 8.4607-4.943 9.4374-9.1796 0.36569-1.5862 0.32543-4.9758-0.077-6.4799-0.85108-3.1813-3.2688-6.291-6.039-7.7675-3.8111-2.0313-9.456-2.0295-13.272 5e-3 -5.9828 3.1888-8.1556 11.089-4.7878 17.408 2.6995 5.0648 8.3611 7.3754 14.738 6.015z" fill="#f0f0f0" stroke-width=".025526"/></g><g transform="matrix(.069892 0 0 -.069892 44.236 474.48)"><path d="m2787 4669c-124-65-123-255 3-319 86-44 196-16 247 62 58 87 26 211-67 258-51 26-132 26-183-1z" fill="#00b6f0" stroke="#00b6f0" stroke-width="4.25"/><path d="m2882 4108c-12-16-63-166-102-303-30-104-101-350-165-565-20-69-58-199-85-290-26-91-64-221-85-290-20-69-58-199-85-290-26-91-64-221-85-290-20-69-57-195-81-280-59-207-93-299-115-310-10-6-35-10-56-10-73 0-84-8-81-54l3-41 228-3 228-2-3 47-3 48-73 3c-66 3-74 5-84 27-13 28 0 104 37 225 13 41 47 156 75 255s66 230 85 290c18 61 56 191 85 290 28 99 66 230 85 290 18 61 56 191 85 290 85 297 123 419 131 429 5 5 17-11 28-35 10-24 192-393 403-819s447-902 523-1058l139-282h168c92 0 168 4 168 8s-75 158-166 342c-588 1183-969 1958-1033 2100-29 63-69 151-89 195-44 95-58 110-80 83z" fill="#575757"/></g></svg>
|
Before Width: | Height: | Size: 3.4 KiB |
BIN
Shared/Assets.xcassets/Invidious.imageset/Invidious_512x512@1x.png
vendored
Normal file
BIN
Shared/Assets.xcassets/Invidious.imageset/Invidious_512x512@1x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
BIN
Shared/Assets.xcassets/Invidious.imageset/Invidious_512x512@2x.png
vendored
Normal file
BIN
Shared/Assets.xcassets/Invidious.imageset/Invidious_512x512@2x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 85 KiB |
BIN
Shared/Assets.xcassets/Invidious.imageset/Invidious_512x512@3x.png
vendored
Normal file
BIN
Shared/Assets.xcassets/Invidious.imageset/Invidious_512x512@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 146 KiB |
@ -0,0 +1,38 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
@ -39,6 +39,38 @@ enum Constants {
|
||||
#endif
|
||||
}
|
||||
|
||||
static var isTvOS: Bool {
|
||||
#if os(tvOS)
|
||||
true
|
||||
#else
|
||||
false
|
||||
#endif
|
||||
}
|
||||
|
||||
static var isMacOS: Bool {
|
||||
#if os(macOS)
|
||||
true
|
||||
#else
|
||||
false
|
||||
#endif
|
||||
}
|
||||
|
||||
static var isIOS: Bool {
|
||||
#if os(iOS)
|
||||
true
|
||||
#else
|
||||
false
|
||||
#endif
|
||||
}
|
||||
|
||||
static var detailsVisibility: Bool {
|
||||
#if os(iOS)
|
||||
false
|
||||
#else
|
||||
true
|
||||
#endif
|
||||
}
|
||||
|
||||
static var progressViewScale: Double {
|
||||
#if os(macOS)
|
||||
0.4
|
||||
@ -71,11 +103,11 @@ enum Constants {
|
||||
#endif
|
||||
}
|
||||
|
||||
static var detailsVisibility: Bool {
|
||||
#if os(iOS)
|
||||
false
|
||||
static var contentViewMinWidth: Double {
|
||||
#if os(macOS)
|
||||
835
|
||||
#else
|
||||
true
|
||||
0
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -15,19 +15,20 @@ extension Defaults.Keys {
|
||||
static let favorites = Key<[FavoriteItem]>("favorites", default: [])
|
||||
static let widgetsSettings = Key<[WidgetSettings]>("widgetsSettings", default: [])
|
||||
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 showOpenActionsToolbarItem = Key<Bool>("showOpenActionsToolbarItem", default: false)
|
||||
#if os(iOS)
|
||||
static let showDocuments = Key<Bool>("showDocuments", default: false)
|
||||
static let lockPortraitWhenBrowsing = Key<Bool>("lockPortraitWhenBrowsing", default: UIDevice.current.userInterfaceIdiom == .phone)
|
||||
static let lockPortraitWhenBrowsing = Key<Bool>("lockPortraitWhenBrowsing", default: Constants.isIPhone)
|
||||
#endif
|
||||
|
||||
#if !os(tvOS)
|
||||
#if os(macOS)
|
||||
static let accountPickerDisplaysUsernameDefault = true
|
||||
#else
|
||||
static let accountPickerDisplaysUsernameDefault = UIDevice.current.userInterfaceIdiom == .pad
|
||||
static let accountPickerDisplaysUsernameDefault = Constants.isIPad
|
||||
#endif
|
||||
static let accountPickerDisplaysUsername = Key<Bool>("accountPickerDisplaysUsername", default: accountPickerDisplaysUsernameDefault)
|
||||
#endif
|
||||
@ -41,9 +42,9 @@ extension Defaults.Keys {
|
||||
static let showChannelAvatarInVideosListing = Key<Bool>("showChannelAvatarInVideosListing", default: true)
|
||||
|
||||
static let playerButtonSingleTapGesture = Key<PlayerTapGestureAction>("playerButtonSingleTapGesture", default: .togglePlayer)
|
||||
static let playerButtonDoubleTapGesture = Key<PlayerTapGestureAction>("playerButtonDoubleTapGesture", default: .nothing)
|
||||
static let playerButtonShowsControlButtonsWhenMinimized = Key<Bool>("playerButtonShowsControlButtonsWhenMinimized", default: false)
|
||||
static let playerButtonIsExpanded = Key<Bool>("playerButtonIsExpanded", default: false)
|
||||
static let playerButtonDoubleTapGesture = Key<PlayerTapGestureAction>("playerButtonDoubleTapGesture", default: .togglePlayerVisibility)
|
||||
static let playerButtonShowsControlButtonsWhenMinimized = Key<Bool>("playerButtonShowsControlButtonsWhenMinimized", default: true)
|
||||
static let playerButtonIsExpanded = Key<Bool>("playerButtonIsExpanded", default: true)
|
||||
static let playerBarMaxWidth = Key<String>("playerBarMaxWidth", default: "600")
|
||||
static let channelOnThumbnail = Key<Bool>("channelOnThumbnail", default: false)
|
||||
static let timeOnThumbnail = Key<Bool>("timeOnThumbnail", default: true)
|
||||
@ -64,7 +65,7 @@ extension Defaults.Keys {
|
||||
static let closeVideoOnEOF = Key<Bool>("closeVideoOnEOF", default: false)
|
||||
|
||||
#if !os(macOS)
|
||||
static let pauseOnEnteringBackground = Key<Bool>("pauseOnEnteringBackground", default: true)
|
||||
static let pauseOnEnteringBackground = Key<Bool>("pauseOnEnteringBackground", default: false)
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
@ -79,7 +80,7 @@ extension Defaults.Keys {
|
||||
|
||||
static let showChapters = Key<Bool>("showChapters", default: true)
|
||||
static let showChapterThumbnails = Key<Bool>("showChapterThumbnails", default: true)
|
||||
static let showChapterThumbnailsOnlyWhenDifferent = Key<Bool>("showChapterThumbnailsOnlyWhenDifferent", default: true)
|
||||
static let showChapterThumbnailsOnlyWhenDifferent = Key<Bool>("showChapterThumbnailsOnlyWhenDifferent", default: false)
|
||||
static let expandChapters = Key<Bool>("expandChapters", default: true)
|
||||
static let showRelated = Key<Bool>("showRelated", default: true)
|
||||
static let showInspector = Key<ShowInspectorSetting>("showInspector", default: .onlyLocal)
|
||||
@ -93,12 +94,9 @@ extension Defaults.Keys {
|
||||
static let enableReturnYouTubeDislike = Key<Bool>("enableReturnYouTubeDislike", default: false)
|
||||
|
||||
#if os(iOS)
|
||||
static let honorSystemOrientationLock = Key<Bool>("honorSystemOrientationLock", default: true)
|
||||
static let enterFullscreenInLandscape = Key<Bool>("enterFullscreenInLandscape", default: UIDevice.current.userInterfaceIdiom == .phone)
|
||||
static let rotateToLandscapeOnEnterFullScreen = Key<FullScreenRotationSetting>(
|
||||
"rotateToLandscapeOnEnterFullScreen",
|
||||
default: UIDevice.current.userInterfaceIdiom == .phone ? .landscapeRight : .disabled
|
||||
)
|
||||
static let isOrientationLocked = Key<Bool>("isOrientationLocked", default: Constants.isIPhone)
|
||||
static let enterFullscreenInLandscape = Key<Bool>("enterFullscreenInLandscape", default: Constants.isIPhone)
|
||||
static let rotateToLandscapeOnEnterFullScreen = Key<FullScreenRotationSetting>("rotateToLandscapeOnEnterFullScreen", default: .landscapeRight)
|
||||
#endif
|
||||
|
||||
static let closePiPOnNavigation = Key<Bool>("closePiPOnNavigation", default: false)
|
||||
@ -116,14 +114,15 @@ extension Defaults.Keys {
|
||||
|
||||
// MARK: GROUP - Controls
|
||||
|
||||
static let avPlayerUsesSystemControls = Key<Bool>("avPlayerUsesSystemControls", default: true)
|
||||
static let avPlayerUsesSystemControls = Key<Bool>("avPlayerUsesSystemControls", default: Constants.isTvOS)
|
||||
static let horizontalPlayerGestureEnabled = Key<Bool>("horizontalPlayerGestureEnabled", default: true)
|
||||
static let fullscreenPlayerGestureEnabled = Key<Bool>("fullscreenPlayerGestureEnabled", default: true)
|
||||
static let seekGestureSensitivity = Key<Double>("seekGestureSensitivity", default: 30.0)
|
||||
static let seekGestureSpeed = Key<Double>("seekGestureSpeed", default: 0.5)
|
||||
|
||||
#if os(iOS)
|
||||
static let playerControlsLayoutDefault = UIDevice.current.userInterfaceIdiom == .pad ? PlayerControlsLayout.medium : .small
|
||||
static let fullScreenPlayerControlsLayoutDefault = UIDevice.current.userInterfaceIdiom == .pad ? PlayerControlsLayout.medium : .small
|
||||
static let playerControlsLayoutDefault = Constants.isIPad ? PlayerControlsLayout.medium : .small
|
||||
static let fullScreenPlayerControlsLayoutDefault = Constants.isIPad ? PlayerControlsLayout.medium : .small
|
||||
#elseif os(tvOS)
|
||||
static let playerControlsLayoutDefault = PlayerControlsLayout.tvRegular
|
||||
static let fullScreenPlayerControlsLayoutDefault = PlayerControlsLayout.tvRegular
|
||||
@ -134,6 +133,7 @@ extension Defaults.Keys {
|
||||
|
||||
static let playerControlsLayout = Key<PlayerControlsLayout>("playerControlsLayout", default: playerControlsLayoutDefault)
|
||||
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)
|
||||
|
||||
@ -175,61 +175,152 @@ extension Defaults.Keys {
|
||||
|
||||
// MARK: GROUP - Quality
|
||||
|
||||
static let hd2160pMPVProfile = QualityProfile(id: "hd2160pMPVProfile", backend: .mpv, resolution: .hd2160p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let hd1080pMPVProfile = QualityProfile(id: "hd1080pMPVProfile", backend: .mpv, resolution: .hd1080p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let hd720pMPVProfile = QualityProfile(id: "hd720pMPVProfile", backend: .mpv, resolution: .hd720p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let hd720pAVPlayerProfile = QualityProfile(id: "hd720pAVPlayerProfile", backend: .appleAVPlayer, resolution: .hd720p30, formats: [.hls, .stream], order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let sd360pAVPlayerProfile = QualityProfile(id: "sd360pAVPlayerProfile", backend: .appleAVPlayer, resolution: .sd360p30, formats: [.hls, .stream], order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let hd2160p60MPVProfile = QualityProfile(id: "hd2160p60MPVProfile", backend: .mpv, resolution: .hd2160p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let hd1080p60MPVProfile = QualityProfile(id: "hd1080p60MPVProfile", backend: .mpv, resolution: .hd1080p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let hd1080pMPVProfile = QualityProfile(id: "hd1080pMPVProfile", backend: .mpv, resolution: .hd1080p30, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let hd720p60MPVProfile = QualityProfile(id: "hd720p60MPVProfile", backend: .mpv, resolution: .hd720p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let hd720pMPVProfile = QualityProfile(id: "hd720pMPVProfile", backend: .mpv, resolution: .hd720p30, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let sd360pMPVProfile = QualityProfile(id: "sd360pMPVProfile", backend: .mpv, resolution: .sd360p30, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let hd720pAVPlayerProfile = QualityProfile(id: "hd720pAVPlayerProfile", backend: .appleAVPlayer, resolution: .hd720p30, formats: [.stream, .hls], order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let sd360pAVPlayerProfile = QualityProfile(id: "sd360pAVPlayerProfile", backend: .appleAVPlayer, resolution: .sd360p30, formats: [.stream, .hls], order: Array(QualityProfile.Format.allCases.indices))
|
||||
|
||||
#if os(iOS)
|
||||
static let qualityProfilesDefault = UIDevice.current.userInterfaceIdiom == .pad ? [
|
||||
hd2160pMPVProfile,
|
||||
hd1080pMPVProfile,
|
||||
hd720pMPVProfile,
|
||||
hd720pAVPlayerProfile,
|
||||
sd360pAVPlayerProfile
|
||||
] : [
|
||||
hd1080pMPVProfile,
|
||||
hd720pMPVProfile,
|
||||
hd720pAVPlayerProfile,
|
||||
sd360pAVPlayerProfile
|
||||
]
|
||||
enum QualityProfiles {
|
||||
// iPad-specific settings
|
||||
enum iPad {
|
||||
static let qualityProfilesDefault = [
|
||||
hd1080p60MPVProfile,
|
||||
hd1080pMPVProfile,
|
||||
hd720p60MPVProfile,
|
||||
hd720pMPVProfile
|
||||
]
|
||||
|
||||
static let batteryCellularProfileDefault = hd720pMPVProfile.id
|
||||
static let batteryNonCellularProfileDefault = hd720p60MPVProfile.id
|
||||
static let chargingCellularProfileDefault = hd1080pMPVProfile.id
|
||||
static let chargingNonCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
}
|
||||
|
||||
// iPhone-specific settings
|
||||
enum iPhone {
|
||||
static let qualityProfilesDefault = [
|
||||
hd1080p60MPVProfile,
|
||||
hd1080pMPVProfile,
|
||||
hd720p60MPVProfile,
|
||||
hd720pMPVProfile,
|
||||
sd360pMPVProfile
|
||||
]
|
||||
|
||||
static let batteryCellularProfileDefault = sd360pMPVProfile.id
|
||||
static let batteryNonCellularProfileDefault = hd720p60MPVProfile.id
|
||||
static let chargingCellularProfileDefault = hd720pMPVProfile.id
|
||||
static let chargingNonCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
}
|
||||
|
||||
// Access the correct profile based on device type
|
||||
static var currentProfile: (qualityProfilesDefault: [QualityProfile], batteryCellularProfileDefault: String, batteryNonCellularProfileDefault: String, chargingCellularProfileDefault: String, chargingNonCellularProfileDefault: String) {
|
||||
if Constants.isIPad {
|
||||
return (
|
||||
qualityProfilesDefault: iPad.qualityProfilesDefault,
|
||||
batteryCellularProfileDefault: iPad.batteryCellularProfileDefault,
|
||||
batteryNonCellularProfileDefault: iPad.batteryNonCellularProfileDefault,
|
||||
chargingCellularProfileDefault: iPad.chargingCellularProfileDefault,
|
||||
chargingNonCellularProfileDefault: iPad.chargingNonCellularProfileDefault
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
qualityProfilesDefault: iPhone.qualityProfilesDefault,
|
||||
batteryCellularProfileDefault: iPhone.batteryCellularProfileDefault,
|
||||
batteryNonCellularProfileDefault: iPhone.batteryNonCellularProfileDefault,
|
||||
chargingCellularProfileDefault: iPhone.chargingCellularProfileDefault,
|
||||
chargingNonCellularProfileDefault: iPhone.chargingNonCellularProfileDefault
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
static let batteryCellularProfileDefault = hd720pAVPlayerProfile.id
|
||||
static let batteryNonCellularProfileDefault = hd720pAVPlayerProfile.id
|
||||
static let chargingCellularProfileDefault = hd720pAVPlayerProfile.id
|
||||
static let chargingNonCellularProfileDefault = hd1080pMPVProfile.id
|
||||
#elseif os(tvOS)
|
||||
static let qualityProfilesDefault = [
|
||||
hd2160pMPVProfile,
|
||||
hd1080pMPVProfile,
|
||||
hd720pMPVProfile,
|
||||
hd720pAVPlayerProfile
|
||||
]
|
||||
static let batteryCellularProfileDefault = hd1080pMPVProfile.id
|
||||
static let batteryNonCellularProfileDefault = hd1080pMPVProfile.id
|
||||
static let chargingCellularProfileDefault = hd1080pMPVProfile.id
|
||||
static let chargingNonCellularProfileDefault = hd1080pMPVProfile.id
|
||||
enum QualityProfiles {
|
||||
// tvOS-specific settings
|
||||
enum tvOS {
|
||||
static let qualityProfilesDefault = [
|
||||
hd2160p60MPVProfile,
|
||||
hd1080p60MPVProfile,
|
||||
hd720p60MPVProfile,
|
||||
hd720pAVPlayerProfile
|
||||
]
|
||||
|
||||
static let batteryCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
static let batteryNonCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
static let chargingCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
static let chargingNonCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
}
|
||||
|
||||
// Access the correct profile based on device type
|
||||
static var currentProfile: (qualityProfilesDefault: [QualityProfile], batteryCellularProfileDefault: String, batteryNonCellularProfileDefault: String, chargingCellularProfileDefault: String, chargingNonCellularProfileDefault: String) {
|
||||
(
|
||||
qualityProfilesDefault: tvOS.qualityProfilesDefault,
|
||||
batteryCellularProfileDefault: tvOS.batteryCellularProfileDefault,
|
||||
batteryNonCellularProfileDefault: tvOS.batteryNonCellularProfileDefault,
|
||||
chargingCellularProfileDefault: tvOS.chargingCellularProfileDefault,
|
||||
chargingNonCellularProfileDefault: tvOS.chargingNonCellularProfileDefault
|
||||
)
|
||||
}
|
||||
}
|
||||
#else
|
||||
static let qualityProfilesDefault = [
|
||||
hd2160pMPVProfile,
|
||||
hd1080pMPVProfile,
|
||||
hd720pMPVProfile,
|
||||
hd720pAVPlayerProfile
|
||||
]
|
||||
static let batteryCellularProfileDefault = hd1080pMPVProfile.id
|
||||
static let batteryNonCellularProfileDefault = hd1080pMPVProfile.id
|
||||
static let chargingCellularProfileDefault = hd1080pMPVProfile.id
|
||||
static let chargingNonCellularProfileDefault = hd1080pMPVProfile.id
|
||||
enum QualityProfiles {
|
||||
// macOS-specific settings
|
||||
enum macOS {
|
||||
static let qualityProfilesDefault = [
|
||||
hd2160p60MPVProfile,
|
||||
hd1080p60MPVProfile,
|
||||
hd1080pMPVProfile,
|
||||
hd720p60MPVProfile
|
||||
]
|
||||
|
||||
static let batteryCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
static let batteryNonCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
static let chargingCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
static let chargingNonCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
}
|
||||
|
||||
// Access the correct profile for other platforms
|
||||
static var currentProfile: (qualityProfilesDefault: [QualityProfile], batteryCellularProfileDefault: String, batteryNonCellularProfileDefault: String, chargingCellularProfileDefault: String, chargingNonCellularProfileDefault: String) {
|
||||
(
|
||||
qualityProfilesDefault: macOS.qualityProfilesDefault,
|
||||
batteryCellularProfileDefault: macOS.batteryCellularProfileDefault,
|
||||
batteryNonCellularProfileDefault: macOS.batteryNonCellularProfileDefault,
|
||||
chargingCellularProfileDefault: macOS.chargingCellularProfileDefault,
|
||||
chargingNonCellularProfileDefault: macOS.chargingNonCellularProfileDefault
|
||||
)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static let batteryCellularProfile = Key<QualityProfile.ID>("batteryCellularProfile", default: batteryCellularProfileDefault)
|
||||
static let batteryNonCellularProfile = Key<QualityProfile.ID>("batteryNonCellularProfile", default: batteryNonCellularProfileDefault)
|
||||
static let chargingCellularProfile = Key<QualityProfile.ID>("chargingCellularProfile", default: chargingCellularProfileDefault)
|
||||
static let chargingNonCellularProfile = Key<QualityProfile.ID>("chargingNonCellularProfile", default: chargingNonCellularProfileDefault)
|
||||
static let forceAVPlayerForLiveStreams = Key<Bool>("forceAVPlayerForLiveStreams", default: true)
|
||||
|
||||
static let qualityProfiles = Key<[QualityProfile]>("qualityProfiles", default: qualityProfilesDefault)
|
||||
static let batteryCellularProfile = Key<QualityProfile.ID>(
|
||||
"batteryCellularProfile",
|
||||
default: QualityProfiles.currentProfile.batteryCellularProfileDefault
|
||||
)
|
||||
static let batteryNonCellularProfile = Key<QualityProfile.ID>(
|
||||
"batteryNonCellularProfile",
|
||||
default: QualityProfiles.currentProfile.batteryNonCellularProfileDefault
|
||||
)
|
||||
static let chargingCellularProfile = Key<QualityProfile.ID>(
|
||||
"chargingCellularProfile",
|
||||
default: QualityProfiles.currentProfile.chargingCellularProfileDefault
|
||||
)
|
||||
static let chargingNonCellularProfile = Key<QualityProfile.ID>(
|
||||
"chargingNonCellularProfile",
|
||||
default: QualityProfiles.currentProfile.chargingNonCellularProfileDefault
|
||||
)
|
||||
static let forceAVPlayerForLiveStreams = Key<Bool>(
|
||||
"forceAVPlayerForLiveStreams",
|
||||
default: true
|
||||
)
|
||||
static let qualityProfiles = Key<[QualityProfile]>(
|
||||
"qualityProfiles",
|
||||
default: QualityProfiles.currentProfile.qualityProfilesDefault
|
||||
)
|
||||
|
||||
// MARK: GROUP - History
|
||||
|
||||
@ -269,6 +360,7 @@ extension Defaults.Keys {
|
||||
// MARK: Group - Advanced
|
||||
|
||||
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 mpvEnableLogging = Key<Bool>("mpvEnableLogging", default: false)
|
||||
@ -279,6 +371,7 @@ extension Defaults.Keys {
|
||||
static let mpvHWdec = Key<String>("hwdec", default: "auto-safe")
|
||||
static let mpvDemuxerLavfProbeInfo = Key<String>("mpvDemuxerLavfProbeInfo", default: "no")
|
||||
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 feedCacheSize = Key<String>("feedCacheSize", default: "50")
|
||||
@ -335,18 +428,34 @@ enum ResolutionSetting: String, CaseIterable, Defaults.Serializable {
|
||||
case sd240p30
|
||||
case sd144p30
|
||||
|
||||
var value: Stream.Resolution! {
|
||||
.init(rawValue: rawValue)
|
||||
var value: Stream.Resolution {
|
||||
if let predefined = Stream.Resolution.PredefinedResolution(rawValue: rawValue) {
|
||||
return .predefined(predefined)
|
||||
}
|
||||
// Provide a default value of 720p 30
|
||||
return .custom(height: 720, refreshRate: 30)
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .hd2160p60:
|
||||
return "4K, 60fps"
|
||||
case .hd2160p30:
|
||||
return "4K"
|
||||
let resolution = value
|
||||
let height = resolution.height
|
||||
let refreshRate = resolution.refreshRate
|
||||
|
||||
// Superscript labels
|
||||
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:
|
||||
return value.name
|
||||
// Default formatting for other resolutions
|
||||
return refreshRate == 30 ? "\(height)p" : "\(height)p\(refreshRate)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -521,26 +630,19 @@ enum PlayerTapGestureAction: String, CaseIterable, Defaults.Serializable {
|
||||
}
|
||||
|
||||
enum FullScreenRotationSetting: String, CaseIterable, Defaults.Serializable {
|
||||
case disabled
|
||||
case landscapeLeft
|
||||
case landscapeRight
|
||||
|
||||
#if os(iOS)
|
||||
var interaceOrientation: UIInterfaceOrientation {
|
||||
var interfaceOrientation: UIInterfaceOrientation {
|
||||
switch self {
|
||||
case .landscapeLeft:
|
||||
return .landscapeLeft
|
||||
case .landscapeRight:
|
||||
return .landscapeRight
|
||||
default:
|
||||
return .portrait
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
var isRotating: Bool {
|
||||
self != .disabled
|
||||
}
|
||||
}
|
||||
|
||||
struct WidgetSettings: Defaults.Serializable {
|
||||
|
@ -152,7 +152,7 @@ struct HomeView: View {
|
||||
#endif
|
||||
#if os(macOS)
|
||||
.background(Color.secondaryBackground)
|
||||
.frame(minWidth: 360)
|
||||
.frame(minWidth: Constants.contentViewMinWidth)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .automatic) {
|
||||
HideWatchedButtons()
|
||||
|
@ -15,7 +15,7 @@ struct AppSidebarNavigation: View {
|
||||
|
||||
var body: some View {
|
||||
#if os(iOS)
|
||||
content.introspect(.viewController, on: .iOS(.v15, .v16, .v17)) { viewController in
|
||||
content.introspect(.viewController, on: .iOS(.v15, .v16, .v17, .v18)) { viewController in
|
||||
// workaround for an empty supplementary view on launch
|
||||
// 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
|
||||
|
@ -169,7 +169,7 @@ struct ContentView: View {
|
||||
.statusBarHidden(player.playingFullScreen)
|
||||
#endif
|
||||
#if os(macOS)
|
||||
.frame(minWidth: 1200)
|
||||
.frame(minWidth: 1200, minHeight: 600)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -4,11 +4,6 @@ import SwiftUI
|
||||
|
||||
#if !os(macOS)
|
||||
final class AppleAVPlayerViewControllerDelegate: NSObject, AVPlayerViewControllerDelegate {
|
||||
#if os(iOS)
|
||||
@Default(.rotateToLandscapeOnEnterFullScreen) private var rotateToLandscapeOnEnterFullScreen
|
||||
@Default(.avPlayerUsesSystemControls) private var avPlayerUsesSystemControls
|
||||
#endif
|
||||
|
||||
var player: PlayerModel { .shared }
|
||||
|
||||
func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_: AVPlayerViewController) -> Bool {
|
||||
@ -17,15 +12,23 @@ import SwiftUI
|
||||
|
||||
#if os(iOS)
|
||||
func playerViewController(_: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator) {
|
||||
guard rotateToLandscapeOnEnterFullScreen.isRotating else { return }
|
||||
if PlayerModel.shared.currentVideoIsLandscape {
|
||||
let delay = PlayerModel.shared.activeBackend == .appleAVPlayer && avPlayerUsesSystemControls ? 0.8 : 0
|
||||
// not sure why but first rotation call is ignore so doing rotate to same orientation first
|
||||
Delay.by(delay) {
|
||||
let orientation = OrientationTracker.shared.currentDeviceOrientation.isLandscape ? OrientationTracker.shared.currentInterfaceOrientation : self.rotateToLandscapeOnEnterFullScreen.interaceOrientation
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: OrientationTracker.shared.currentInterfaceOrientation)
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: orientation)
|
||||
let lockOrientation = player.rotateToLandscapeOnEnterFullScreen.interfaceOrientation
|
||||
if player.currentVideoIsLandscape {
|
||||
if player.fullscreenInitiatedByButton {
|
||||
Orientation.lockOrientation(player.isOrientationLocked
|
||||
? (lockOrientation == .landscapeRight ? .landscapeRight : .landscapeLeft)
|
||||
: .landscape)
|
||||
}
|
||||
let orientation = OrientationTracker.shared.currentDeviceOrientation.isLandscape
|
||||
? OrientationTracker.shared.currentInterfaceOrientation
|
||||
: player.rotateToLandscapeOnEnterFullScreen.interfaceOrientation
|
||||
|
||||
Orientation.lockOrientation(
|
||||
player.isOrientationLocked
|
||||
? (lockOrientation == .landscapeRight ? .landscapeRight : .landscapeLeft)
|
||||
: .all,
|
||||
andRotateTo: orientation
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,11 +40,11 @@ import SwiftUI
|
||||
}
|
||||
if !context.isCancelled {
|
||||
#if os(iOS)
|
||||
self.player.lockedOrientation = nil
|
||||
|
||||
if Constants.isIPhone {
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
|
||||
if self.player.lockPortraitWhenBrowsing {
|
||||
self.player.lockedOrientation = UIInterfaceOrientationMask.portrait
|
||||
}
|
||||
let rotationOrientation = self.player.lockPortraitWhenBrowsing ? UIInterfaceOrientation.portrait : nil
|
||||
Orientation.lockOrientation(self.player.lockPortraitWhenBrowsing ? .portrait : .all, andRotateTo: rotationOrientation)
|
||||
|
||||
if wasPlaying {
|
||||
self.player.play()
|
||||
|
@ -5,6 +5,8 @@ struct ControlsOverlay: View {
|
||||
@ObservedObject private var player = PlayerModel.shared
|
||||
private var model = PlayerControlsModel.shared
|
||||
|
||||
@State private var availableCaptions: [Captions] = []
|
||||
@State private var isLoadingCaptions = true
|
||||
@State private var contentSize: CGSize = .zero
|
||||
|
||||
@Default(.showMPVPlaybackStats) private var showMPVPlaybackStats
|
||||
@ -335,7 +337,6 @@ struct ControlsOverlay: View {
|
||||
Image(systemName: "text.bubble")
|
||||
if let captions = captionsBinding.wrappedValue,
|
||||
let language = LanguageCodes(rawValue: captions.code)
|
||||
|
||||
{
|
||||
Text("\(language.description.capitalized) (\(language.rawValue))")
|
||||
.foregroundColor(.accentColor)
|
||||
@ -380,17 +381,16 @@ struct ControlsOverlay: View {
|
||||
.contextMenu {
|
||||
Button("Disabled") { captionsBinding.wrappedValue = nil }
|
||||
|
||||
ForEach(player.currentVideo?.captions ?? []) { caption in
|
||||
ForEach(availableCaptions) { caption in
|
||||
Button(caption.description) { captionsBinding.wrappedValue = caption }
|
||||
}
|
||||
Button("Cancel", role: .cancel) {}
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder private var captionsPicker: some View {
|
||||
let captions = player.currentVideo?.captions ?? []
|
||||
let captions = availableCaptions
|
||||
Picker("Captions", selection: captionsBinding) {
|
||||
if captions.isEmpty {
|
||||
Text("Not available").tag(Captions?.none)
|
||||
@ -402,6 +402,31 @@ struct ControlsOverlay: View {
|
||||
}
|
||||
}
|
||||
.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?> {
|
||||
|
@ -29,6 +29,7 @@ struct PlayerControls: View {
|
||||
|
||||
@Default(.playerControlsLayout) private var regularPlayerControlsLayout
|
||||
@Default(.fullScreenPlayerControlsLayout) private var fullScreenPlayerControlsLayout
|
||||
@Default(.playerControlsBackgroundOpacity) private var playerControlsBackgroundOpacity
|
||||
@Default(.buttonBackwardSeekDuration) private var buttonBackwardSeekDuration
|
||||
@Default(.buttonForwardSeekDuration) private var buttonForwardSeekDuration
|
||||
|
||||
@ -248,31 +249,36 @@ struct PlayerControls: View {
|
||||
return [player.playerSize.height - inset, 500].min()!
|
||||
}
|
||||
|
||||
@ViewBuilder var controlsBackground: some View {
|
||||
ZStack {
|
||||
if player.musicMode,
|
||||
let url = controlsBackgroundURL
|
||||
{
|
||||
ThumbnailView(url: url)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.transition(.opacity)
|
||||
.animation(.default)
|
||||
} else if player.videoForDisplay == nil {
|
||||
Color.black
|
||||
@ViewBuilder
|
||||
var controlsBackground: some View {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
if player.musicMode,
|
||||
let video = player.videoForDisplay
|
||||
{
|
||||
let thumbnail = thumbnails.best(video)
|
||||
if let url = thumbnail.url,
|
||||
let quality = thumbnail.quality
|
||||
{
|
||||
let aspectRatio = (quality == .default || quality == .high) ? Constants.aspectRatio4x3 : Constants.aspectRatio16x9
|
||||
|
||||
ThumbnailView(url: url)
|
||||
.aspectRatio(aspectRatio, contentMode: .fill)
|
||||
.frame(width: geometry.size.width, height: geometry.size.height)
|
||||
.transition(.opacity)
|
||||
.animation(.default)
|
||||
.clipped()
|
||||
}
|
||||
} else if player.videoForDisplay == nil {
|
||||
Color.black
|
||||
} 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 {
|
||||
TimelineView(context: .player).foregroundColor(.primary)
|
||||
}
|
||||
@ -381,13 +387,13 @@ struct PlayerControls: View {
|
||||
}
|
||||
|
||||
private var pipButton: some View {
|
||||
button("PiP", systemImage: player.pipImage, action: player.togglePiPAction)
|
||||
button("PiP", systemImage: player.pipImage, active: player.playingInPictureInPicture, action: player.togglePiPAction)
|
||||
.disabled(!player.pipPossible)
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
private var lockOrientationButton: some View {
|
||||
button("Lock Rotation", systemImage: player.lockOrientationImage, active: !player.lockedOrientation.isNil, action: player.lockOrientationAction)
|
||||
button("Lock Rotation", systemImage: player.lockOrientationImage, active: player.isOrientationLocked, action: player.lockOrientationAction)
|
||||
}
|
||||
#endif
|
||||
|
||||
|
@ -6,10 +6,12 @@ import OpenGLES
|
||||
final class MPVOGLView: GLKView {
|
||||
private var logger = Logger(label: "stream.yattee.mpv.oglview")
|
||||
private var defaultFBO: GLint?
|
||||
private var displayLink: CADisplayLink?
|
||||
|
||||
var mpvGL: UnsafeMutableRawPointer?
|
||||
var queue = DispatchQueue(label: "stream.yattee.opengl")
|
||||
var queue = DispatchQueue(label: "stream.yattee.opengl", qos: .userInteractive)
|
||||
var needsDrawing = true
|
||||
private var dirtyRegion: CGRect?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
guard let context = EAGLContext(api: .openGLES2) else {
|
||||
@ -29,6 +31,70 @@ final class MPVOGLView: GLKView {
|
||||
enableSetNeedsDisplay = false
|
||||
|
||||
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() {
|
||||
@ -36,36 +102,99 @@ final class MPVOGLView: GLKView {
|
||||
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) {
|
||||
guard needsDrawing, let mpvGL else {
|
||||
guard needsDrawing, let mpvGL else { return }
|
||||
|
||||
// Ensure the correct context is set
|
||||
guard EAGLContext.setCurrent(context) else {
|
||||
logger.error("Failed to set current OpenGL context.")
|
||||
return
|
||||
}
|
||||
|
||||
// Bind the default framebuffer
|
||||
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]
|
||||
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(
|
||||
fbo: Int32(defaultFBO!),
|
||||
w: Int32(dims[2]),
|
||||
h: Int32(dims[3]),
|
||||
internal_format: 0
|
||||
)
|
||||
|
||||
// Flip Y coordinate for proper rendering
|
||||
var flip: CInt = 1
|
||||
withUnsafeMutablePointer(to: &flip) { flip in
|
||||
withUnsafeMutablePointer(to: &data) { data in
|
||||
|
||||
// Render with the provided OpenGL FBO parameters
|
||||
withUnsafeMutablePointer(to: &flip) { flipPtr in
|
||||
withUnsafeMutablePointer(to: &data) { dataPtr in
|
||||
var params = [
|
||||
mpv_render_param(type: MPV_RENDER_PARAM_OPENGL_FBO, data: data),
|
||||
mpv_render_param(type: MPV_RENDER_PARAM_FLIP_Y, data: flip),
|
||||
mpv_render_param(type: MPV_RENDER_PARAM_OPENGL_FBO, data: dataPtr),
|
||||
mpv_render_param(type: MPV_RENDER_PARAM_FLIP_Y, data: flipPtr),
|
||||
mpv_render_param()
|
||||
]
|
||||
mpv_render_context_render(OpaquePointer(mpvGL), ¶ms)
|
||||
// Call the render function and check for errors
|
||||
let result = mpv_render_context_render(OpaquePointer(mpvGL), ¶ms)
|
||||
if result < 0 {
|
||||
logger.error("mpv_render_context_render() failed with error code: \(result)")
|
||||
} else {
|
||||
logger.info("mpv_render_context_render() called successfully.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
// Disable the scissor test after rendering if it was enabled
|
||||
if needsPartialUpdate() {
|
||||
glDisable(GLenum(GL_SCISSOR_TEST))
|
||||
}
|
||||
|
||||
// Clear dirty region after drawing
|
||||
clearDirtyRegion()
|
||||
}
|
||||
}
|
||||
|
||||
extension Notification.Name {
|
||||
static let updateDisplayLinkFrameRate = Notification.Name("updateDisplayLinkFrameRate")
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ struct PlayerBackendView: View {
|
||||
Color.clear
|
||||
.onAppear { player.playerSize = proxy.size }
|
||||
.onChange(of: proxy.size) { _ in player.playerSize = proxy.size }
|
||||
.onChange(of: player.controls.presentingOverlays) { _ in player.playerSize = proxy.size }
|
||||
.onChange(of: player.currentItem?.id) { _ in player.playerSize = proxy.size }
|
||||
})
|
||||
|
||||
#if !os(tvOS)
|
||||
|
@ -8,7 +8,7 @@ extension VideoPlayerView {
|
||||
.updating($dragGestureOffset) { value, state, _ in
|
||||
guard isVerticalDrag else { return }
|
||||
var translation = value.translation
|
||||
translation.height = max(0, translation.height)
|
||||
translation.height = max(-translation.height, translation.height)
|
||||
state = translation
|
||||
}
|
||||
#endif
|
||||
@ -18,7 +18,8 @@ extension VideoPlayerView {
|
||||
.onChanged { value in
|
||||
guard player.presentingPlayer,
|
||||
!controlsOverlayModel.presenting,
|
||||
dragGestureState else { return }
|
||||
dragGestureState,
|
||||
!disableToggleGesture else { return }
|
||||
|
||||
if player.controls.presentingControls, !player.musicMode {
|
||||
player.controls.presentingControls = false
|
||||
@ -55,47 +56,83 @@ extension VideoPlayerView {
|
||||
player.seek.gestureStart = time
|
||||
}
|
||||
let timeSeek = (time / player.playerSize.width) * horizontalDrag * seekGestureSpeed
|
||||
|
||||
player.seek.gestureSeek = timeSeek
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard verticalDrag > 0 else { return }
|
||||
viewDragOffset = verticalDrag
|
||||
|
||||
if verticalDrag > 60,
|
||||
player.playingFullScreen
|
||||
{
|
||||
player.exitFullScreen(showControls: false)
|
||||
#if os(iOS)
|
||||
if Constants.isIPhone {
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
|
||||
}
|
||||
#endif
|
||||
// Toggle fullscreen on upward drag only when not disabled
|
||||
if fullscreenPlayerGestureEnabled, verticalDrag < -50 {
|
||||
player.toggleFullScreenAction()
|
||||
disableGestureTemporarily()
|
||||
return
|
||||
}
|
||||
|
||||
// Ignore downward swipes when in fullscreen
|
||||
guard verticalDrag > 0 && !player.playingFullScreen else {
|
||||
return
|
||||
}
|
||||
viewDragOffset = verticalDrag
|
||||
}
|
||||
.onEnded { _ in
|
||||
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() {
|
||||
if horizontalPlayerGestureEnabled, isHorizontalDrag {
|
||||
isHorizontalDrag = false
|
||||
player.seek.onSeekGestureEnd()
|
||||
}
|
||||
|
||||
if viewDragOffset > 60,
|
||||
player.playingFullScreen
|
||||
{
|
||||
#if os(iOS)
|
||||
player.lockedOrientation = nil
|
||||
#endif
|
||||
player.exitFullScreen(showControls: false)
|
||||
viewDragOffset = 0
|
||||
return
|
||||
}
|
||||
isVerticalDrag = false
|
||||
|
||||
guard player.presentingPlayer,
|
||||
@ -117,4 +154,11 @@ extension VideoPlayerView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func disableGestureTemporarily() {
|
||||
disableToggleGesture = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
|
||||
disableToggleGesture = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -155,10 +155,10 @@ struct VideoActions: View {
|
||||
case .fullScreen:
|
||||
actionButton("Fullscreen", systemImage: player.fullscreenImage, action: player.toggleFullScreenAction)
|
||||
case .pip:
|
||||
actionButton("PiP", systemImage: player.pipImage, action: player.togglePiPAction)
|
||||
actionButton("PiP", systemImage: player.pipImage, active: player.playingInPictureInPicture, action: player.togglePiPAction)
|
||||
#if os(iOS)
|
||||
case .lockOrientation:
|
||||
actionButton("Lock", systemImage: player.lockOrientationImage, active: player.lockedOrientation != nil, action: player.lockOrientationAction)
|
||||
actionButton("Lock", systemImage: player.lockOrientationImage, active: player.isOrientationLocked, action: player.lockOrientationAction)
|
||||
#endif
|
||||
case .restart:
|
||||
actionButton("Replay", systemImage: "backward.end.fill", action: player.replayAction)
|
||||
|
@ -223,7 +223,7 @@ struct VideoDetails: View {
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
.padding(.horizontal, 16)
|
||||
// swiftlint:disable trailing_closure
|
||||
|
||||
// TODO: when setting tvOS minimum to 16, the platform modifier can be removed
|
||||
#if !os(tvOS)
|
||||
.simultaneousGesture( // Simultaneous gesture to prioritize button tap
|
||||
@ -234,7 +234,7 @@ struct VideoDetails: View {
|
||||
}
|
||||
)
|
||||
#endif
|
||||
// swiftlint:enable trailing_closure
|
||||
|
||||
if VideoActions().isAnyActionVisible() {
|
||||
VideoActions(video: player.videoForDisplay)
|
||||
.padding(.vertical, 5)
|
||||
|
@ -24,13 +24,12 @@ struct VideoPlayerView: View {
|
||||
#if os(macOS)
|
||||
335
|
||||
#else
|
||||
200
|
||||
140
|
||||
#endif
|
||||
}
|
||||
|
||||
@State private var playerSize: CGSize = .zero { didSet { updateSidebarQueue() } }
|
||||
@State private var hoveringPlayer = false
|
||||
@State private var fullScreenDetails = false
|
||||
@State private var sidebarQueue = defaultSidebarQueueValue
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@ -47,11 +46,20 @@ struct VideoPlayerView: View {
|
||||
#if !os(tvOS)
|
||||
@GestureState var dragGestureState = false
|
||||
@GestureState var dragGestureOffset = CGSize.zero
|
||||
@State var isHorizontalDrag = false // swiftlint:disable:this swiftui_state_private
|
||||
@State var isVerticalDrag = false // swiftlint:disable:this swiftui_state_private
|
||||
@State var viewDragOffset = Self.hiddenOffset // swiftlint:disable:this swiftui_state_private
|
||||
// swiftlint:disable private_swiftui_state
|
||||
@State var isHorizontalDrag = false
|
||||
@State var isVerticalDrag = false
|
||||
@State var viewDragOffset = Self.hiddenOffset
|
||||
@State var detailViewDragOffset: Double = 0
|
||||
// swiftlint:enable private_swiftui_state
|
||||
|
||||
#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
|
||||
|
||||
#if os(macOS)
|
||||
@ -59,6 +67,7 @@ struct VideoPlayerView: View {
|
||||
#endif
|
||||
|
||||
@Default(.horizontalPlayerGestureEnabled) var horizontalPlayerGestureEnabled
|
||||
@Default(.fullscreenPlayerGestureEnabled) var fullscreenPlayerGestureEnabled
|
||||
@Default(.seekGestureSpeed) var seekGestureSpeed
|
||||
@Default(.seekGestureSensitivity) var seekGestureSensitivity
|
||||
@Default(.playerSidebar) var playerSidebar
|
||||
@ -104,9 +113,6 @@ struct VideoPlayerView: View {
|
||||
.onChange(of: geometry.size) { _ in
|
||||
self.playerSize = geometry.size
|
||||
}
|
||||
.onChange(of: fullScreenDetails) { value in
|
||||
player.backend.setNeedsDrawing(!value)
|
||||
}
|
||||
#if os(iOS)
|
||||
.onChange(of: player.presentingPlayer) { newValue in
|
||||
if newValue {
|
||||
@ -120,19 +126,6 @@ struct VideoPlayerView: View {
|
||||
}
|
||||
#endif
|
||||
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) {
|
||||
guard !dragGestureState else { return }
|
||||
@ -306,13 +299,18 @@ struct VideoPlayerView: View {
|
||||
playerSize: player.playerSize,
|
||||
fullScreen: fullScreenDetails
|
||||
))
|
||||
#if os(macOS)
|
||||
// TODO: Check whether this is needed on macOS.
|
||||
.onDisappear {
|
||||
if player.presentingPlayer {
|
||||
player.setNeedsDrawing(true)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
.id(player.currentVideo?.cacheKey)
|
||||
.transition(.opacity)
|
||||
.offset(y: detailViewDragOffset)
|
||||
.gesture(detailsDragGesture)
|
||||
} else {
|
||||
VStack {}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ struct FocusableSearchTextField: View {
|
||||
var body: some View {
|
||||
SearchTextField()
|
||||
#if os(macOS)
|
||||
.introspect(.textField, on: .macOS(.v12, .v13, .v14)) { textField in
|
||||
.introspect(.textField, on: .macOS(.v12, .v13, .v14, .v15)) { textField in
|
||||
state.textField = textField
|
||||
}
|
||||
.onAppear {
|
||||
@ -18,7 +18,7 @@ struct FocusableSearchTextField: View {
|
||||
}
|
||||
}
|
||||
#elseif os(iOS)
|
||||
.introspect(.textField, on: .iOS(.v15, .v16, .v17)) { textField in
|
||||
.introspect(.textField, on: .iOS(.v15, .v16, .v17, .v18)) { textField in
|
||||
state.textField = textField
|
||||
}
|
||||
.onChange(of: state.focused) { newValue in
|
||||
|
@ -1,64 +1,99 @@
|
||||
import Repeat
|
||||
import SwiftUI
|
||||
|
||||
struct SearchTextField: View {
|
||||
private var navigation = NavigationModel.shared
|
||||
@ObservedObject private var state = SearchModel.shared
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
#if os(macOS)
|
||||
#if os(macOS)
|
||||
var body: some View {
|
||||
ZStack {
|
||||
fieldBorder
|
||||
#endif
|
||||
|
||||
HStack(spacing: 0) {
|
||||
#if os(macOS)
|
||||
HStack(spacing: 0) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 12, height: 12)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.horizontal, 6)
|
||||
.opacity(0.8)
|
||||
#endif
|
||||
TextField("Search...", text: $state.queryText) {
|
||||
state.changeQuery { query in
|
||||
query.query = state.queryText
|
||||
navigation.hideKeyboard()
|
||||
}
|
||||
RecentsModel.shared.addQuery(state.queryText)
|
||||
}
|
||||
.disableAutocorrection(true)
|
||||
#if os(macOS)
|
||||
.frame(maxWidth: 190)
|
||||
.textFieldStyle(.plain)
|
||||
#else
|
||||
.frame(minWidth: 200)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.horizontal, 5)
|
||||
.padding(.trailing, state.queryText.isEmpty ? 0 : 10)
|
||||
#endif
|
||||
|
||||
if !state.queryText.isEmpty {
|
||||
clearButton
|
||||
} else {
|
||||
#if os(macOS)
|
||||
GeometryReader { geometry in
|
||||
TextField("Search...", text: $state.queryText) {
|
||||
state.changeQuery { query in
|
||||
query.query = state.queryText
|
||||
navigation.hideKeyboard()
|
||||
}
|
||||
RecentsModel.shared.addQuery(state.queryText)
|
||||
}
|
||||
.disableAutocorrection(true)
|
||||
.frame(maxWidth: geometry.size.width - 5)
|
||||
.textFieldStyle(.plain)
|
||||
.padding(.vertical, 8)
|
||||
.frame(height: 27, alignment: .center)
|
||||
}
|
||||
|
||||
if !state.queryText.isEmpty {
|
||||
clearButton
|
||||
} else {
|
||||
clearButton
|
||||
.opacity(0)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
.transaction { t in t.animation = nil }
|
||||
}
|
||||
.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
|
||||
|
||||
private var fieldBorder: some View {
|
||||
RoundedRectangle(cornerRadius: 5, style: .continuous)
|
||||
.fill(Color.background)
|
||||
.frame(width: 250, height: 32)
|
||||
.frame(width: 250, height: 27)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 5, style: .continuous)
|
||||
.stroke(Color.gray.opacity(0.4), lineWidth: 1)
|
||||
.frame(width: 250, height: 31)
|
||||
.frame(width: 250, height: 27)
|
||||
)
|
||||
}
|
||||
|
||||
@ -67,15 +102,14 @@ struct SearchTextField: View {
|
||||
self.state.queryText = ""
|
||||
}) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
#if os(macOS)
|
||||
.imageScale(.small)
|
||||
#else
|
||||
.imageScale(.medium)
|
||||
#endif
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
#if os(macOS)
|
||||
.padding(.trailing, 10)
|
||||
.padding(.trailing, 5)
|
||||
#elseif os(iOS)
|
||||
.padding(.trailing, 5)
|
||||
.foregroundColor(.gray)
|
||||
#endif
|
||||
.opacity(0.7)
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ struct SearchView: View {
|
||||
@Default(.saveRecents) private var saveRecents
|
||||
@Default(.showHome) private var showHome
|
||||
@Default(.searchListingStyle) private var searchListingStyle
|
||||
@Default(.showSearchSuggestions) private var showSearchSuggestions
|
||||
|
||||
private var videos = [Video]()
|
||||
|
||||
@ -38,9 +39,9 @@ struct SearchView: View {
|
||||
self.videos = videos
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
#if os(iOS)
|
||||
#if os(iOS)
|
||||
var body: some View {
|
||||
VStack {
|
||||
VStack {
|
||||
if accounts.app.supportsSearchSuggestions, state.query.query != state.queryText {
|
||||
SearchSuggestions()
|
||||
@ -51,27 +52,155 @@ struct SearchView: View {
|
||||
}
|
||||
.backport
|
||||
.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 {
|
||||
results
|
||||
|
||||
#if os(macOS)
|
||||
if accounts.app.supportsSearchSuggestions, state.query.query != state.queryText {
|
||||
HStack {
|
||||
Spacer()
|
||||
SearchSuggestions()
|
||||
.borderLeading(width: 1, color: Color("ControlsBorderColor"))
|
||||
.frame(maxWidth: 280)
|
||||
.opacity(state.queryText.isEmpty ? 0 : 1)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.environment(\.listingStyle, searchListingStyle)
|
||||
.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()
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.environment(\.listingStyle, searchListingStyle)
|
||||
.toolbar {
|
||||
#if os(macOS)
|
||||
|
||||
#elseif os(macOS)
|
||||
var body: some View {
|
||||
ZStack {
|
||||
results
|
||||
if accounts.app.supportsSearchSuggestions, state.query.query != state.queryText, showSearchSuggestions {
|
||||
HStack {
|
||||
Spacer()
|
||||
SearchSuggestions()
|
||||
.borderLeading(width: 1, color: Color("ControlsBorderColor"))
|
||||
.frame(maxWidth: 262)
|
||||
.opacity(state.queryText.isEmpty ? 0 : 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
.environment(\.listingStyle, searchListingStyle)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: toolbarPlacement) {
|
||||
ListingStyleButtons(listingStyle: $searchListingStyle)
|
||||
HideWatchedButtons()
|
||||
@ -84,7 +213,6 @@ struct SearchView: View {
|
||||
HStack {
|
||||
Text("Sort:")
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
searchSortOrderPicker
|
||||
}
|
||||
}
|
||||
@ -101,94 +229,52 @@ struct SearchView: View {
|
||||
SearchTextField()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.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)
|
||||
|
||||
#if os(tvOS)
|
||||
searchDebounce.invalidate()
|
||||
recentsDebounce.invalidate()
|
||||
|
||||
searchDebounce.debouncing(2) {
|
||||
state.changeQuery { query in
|
||||
query.query = newQuery
|
||||
}
|
||||
.onAppear {
|
||||
if let query {
|
||||
state.queryText = query.query
|
||||
state.resetQuery(query)
|
||||
updateFavoriteItem()
|
||||
}
|
||||
|
||||
recentsDebounce.debouncing(10) {
|
||||
recents.addQuery(newQuery)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.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()
|
||||
}
|
||||
}
|
||||
#if os(tvOS)
|
||||
.searchable(text: $state.queryText) {
|
||||
if !state.queryText.isEmpty {
|
||||
ForEach(state.querySuggestions, id: \.self) { suggestion in
|
||||
Text(suggestion)
|
||||
.searchCompletion(suggestion)
|
||||
if !videos.isEmpty {
|
||||
state.store.replace(ContentItem.array(of: videos))
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
.ignoresSafeArea(.keyboard, edges: .bottom)
|
||||
.navigationTitle("Search")
|
||||
#endif
|
||||
#if os(iOS)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
searchMenu
|
||||
.onChange(of: accounts.current) { _ in
|
||||
state.reloadQuery()
|
||||
}
|
||||
ToolbarItem(placement: .principal) {
|
||||
if #available(iOS 15, *) {
|
||||
FocusableSearchTextField()
|
||||
.onChange(of: state.queryText) { newQuery in
|
||||
if newQuery.isEmpty {
|
||||
favoriteItem = nil
|
||||
state.resetQuery()
|
||||
} else {
|
||||
SearchTextField()
|
||||
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()
|
||||
}
|
||||
}
|
||||
.frame(minWidth: Constants.contentViewMinWidth)
|
||||
.navigationTitle("Search")
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
var searchMenu: some View {
|
||||
@ -230,11 +316,10 @@ struct SearchView: View {
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "magnifyingglass")
|
||||
Image(systemName: "chevron.down.circle.fill")
|
||||
.foregroundColor(.accentColor)
|
||||
.imageScale(.large)
|
||||
}
|
||||
.foregroundColor(.accentColor)
|
||||
.imageScale(.medium)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
@ -11,9 +11,11 @@ struct AdvancedSettings: View {
|
||||
@Default(.mpvHWdec) private var mpvHWdec
|
||||
@Default(.mpvDemuxerLavfProbeInfo) private var mpvDemuxerLavfProbeInfo
|
||||
@Default(.mpvInitialAudioSync) private var mpvInitialAudioSync
|
||||
@Default(.mpvSetRefreshToContentFPS) private var mpvSetRefreshToContentFPS
|
||||
@Default(.showCacheStatus) private var showCacheStatus
|
||||
@Default(.feedCacheSize) private var feedCacheSize
|
||||
@Default(.showPlayNowInBackendContextMenu) private var showPlayNowInBackendContextMenu
|
||||
@Default(.videoLoadingRetryCount) private var videoLoadingRetryCount
|
||||
|
||||
@State private var filesToShare = [MPVClient.logFile]
|
||||
@State private var presentingShareSheet = false
|
||||
@ -64,6 +66,7 @@ struct AdvancedSettings: View {
|
||||
@ViewBuilder var advancedSettings: some View {
|
||||
Section(header: SettingsHeader(text: "Advanced")) {
|
||||
showPlayNowInBackendButtonsToggle
|
||||
videoLoadingRetryCountField
|
||||
}
|
||||
|
||||
Section(header: SettingsHeader(text: "MPV"), footer: mpvFooter) {
|
||||
@ -245,6 +248,12 @@ struct AdvancedSettings: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
Toggle(isOn: $mpvSetRefreshToContentFPS) {
|
||||
HStack {
|
||||
Text("Sync refresh rate with content FPS – EXPERIMENTAL")
|
||||
}
|
||||
}
|
||||
|
||||
if mpvEnableLogging {
|
||||
logButton
|
||||
}
|
||||
@ -281,6 +290,19 @@ struct AdvancedSettings: View {
|
||||
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 {
|
||||
Toggle("Show playback statistics", isOn: $showMPVPlaybackStats)
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ struct BrowsingSettings: View {
|
||||
@Default(.showUnwatchedFeedBadges) private var showUnwatchedFeedBadges
|
||||
@Default(.keepChannelsWithUnwatchedFeedOnTop) private var keepChannelsWithUnwatchedFeedOnTop
|
||||
#if os(iOS)
|
||||
@Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape
|
||||
@Default(.lockPortraitWhenBrowsing) private var lockPortraitWhenBrowsing
|
||||
@Default(.showDocuments) private var showDocuments
|
||||
#endif
|
||||
@ -19,6 +20,7 @@ struct BrowsingSettings: View {
|
||||
@Default(.showOpenActionsToolbarItem) private var showOpenActionsToolbarItem
|
||||
@Default(.visibleSections) private var visibleSections
|
||||
@Default(.startupSection) private var startupSection
|
||||
@Default(.showSearchSuggestions) private var showSearchSuggestions
|
||||
@Default(.playerButtonSingleTapGesture) private var playerButtonSingleTapGesture
|
||||
@Default(.playerButtonDoubleTapGesture) private var playerButtonDoubleTapGesture
|
||||
@Default(.playerButtonShowsControlButtonsWhenMinimized) private var playerButtonShowsControlButtonsWhenMinimized
|
||||
@ -66,6 +68,7 @@ struct BrowsingSettings: View {
|
||||
homeSettings
|
||||
if !accounts.isEmpty {
|
||||
startupSectionPicker
|
||||
showSearchSuggestionsToggle
|
||||
visibleSectionsSettings
|
||||
}
|
||||
let interface = interfaceSettings
|
||||
@ -161,14 +164,18 @@ struct BrowsingSettings: View {
|
||||
#if os(iOS)
|
||||
Toggle("Show Documents", isOn: $showDocuments)
|
||||
|
||||
Toggle("Lock portrait mode", isOn: $lockPortraitWhenBrowsing)
|
||||
.onChange(of: lockPortraitWhenBrowsing) { lock in
|
||||
if lock {
|
||||
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
||||
} else {
|
||||
Orientation.lockOrientation(.allButUpsideDown)
|
||||
if Constants.isIPad {
|
||||
Toggle("Lock portrait mode", isOn: $lockPortraitWhenBrowsing)
|
||||
.onChange(of: lockPortraitWhenBrowsing) { lock in
|
||||
if lock {
|
||||
enterFullscreenInLandscape = true
|
||||
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
||||
} else {
|
||||
enterFullscreenInLandscape = false
|
||||
Orientation.lockOrientation(.all)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if !accounts.isEmpty {
|
||||
@ -241,6 +248,10 @@ struct BrowsingSettings: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var showSearchSuggestionsToggle: some View {
|
||||
Toggle("Show search suggestions", isOn: $showSearchSuggestions)
|
||||
}
|
||||
|
||||
private func toggleSection(_ section: VisibleSection, value: Bool) {
|
||||
if value {
|
||||
visibleSections.insert(section)
|
||||
|
@ -86,6 +86,7 @@ struct InstanceForm: View {
|
||||
.autocapitalization(.none)
|
||||
.keyboardType(.URL)
|
||||
#endif
|
||||
.disableAutocorrection(true)
|
||||
|
||||
#if os(tvOS)
|
||||
VStack {
|
||||
|
@ -8,6 +8,7 @@ struct InstanceSettings: View {
|
||||
|
||||
@State private var frontendURL = ""
|
||||
@State private var proxiesVideos = false
|
||||
@State private var invidiousCompanion = false
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
@ -87,6 +88,16 @@ struct InstanceSettings: View {
|
||||
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)
|
||||
.frame(maxWidth: 1000)
|
||||
@ -101,6 +112,10 @@ struct InstanceSettings: View {
|
||||
Toggle("Proxy videos", isOn: $proxiesVideos)
|
||||
}
|
||||
|
||||
private var invidiousCompanionToggle: some View {
|
||||
Toggle("Invidious companion", isOn: $invidiousCompanion)
|
||||
}
|
||||
|
||||
private func removeAccount(_ account: Account) {
|
||||
AccountsModel.remove(account)
|
||||
accountsChanged.toggle()
|
||||
|
@ -8,6 +8,7 @@ struct PlayerControlsSettings: View {
|
||||
@Default(.playerControlsLayout) private var playerControlsLayout
|
||||
@Default(.fullScreenPlayerControlsLayout) private var fullScreenPlayerControlsLayout
|
||||
@Default(.horizontalPlayerGestureEnabled) private var horizontalPlayerGestureEnabled
|
||||
@Default(.fullscreenPlayerGestureEnabled) private var fullscreenPlayerGestureEnabled
|
||||
@Default(.seekGestureSpeed) private var seekGestureSpeed
|
||||
@Default(.seekGestureSensitivity) private var seekGestureSensitivity
|
||||
@Default(.buttonBackwardSeekDuration) private var buttonBackwardSeekDuration
|
||||
@ -38,6 +39,7 @@ struct PlayerControlsSettings: View {
|
||||
@Default(.playerControlsAdvanceToNextEnabled) private var playerControlsAdvanceToNextEnabled
|
||||
@Default(.playerControlsPlaybackModeEnabled) private var playerControlsPlaybackModeEnabled
|
||||
@Default(.playerControlsMusicModeEnabled) private var playerControlsMusicModeEnabled
|
||||
@Default(.playerControlsBackgroundOpacity) private var playerControlsBackgroundOpacity
|
||||
|
||||
private var player = PlayerModel.shared
|
||||
|
||||
@ -63,9 +65,10 @@ struct PlayerControlsSettings: View {
|
||||
|
||||
@ViewBuilder var sections: some View {
|
||||
#if !os(tvOS)
|
||||
Section(header: SettingsHeader(text: "Controls".localized()), footer: controlsLayoutFooter) {
|
||||
#if !os(tvOS)
|
||||
avPlayerUsesSystemControlsToggle
|
||||
Section(header: SettingsHeader(text: "Player Controls".localized()), footer: controlsLayoutFooter) {
|
||||
avPlayerUsesSystemControlsToggle
|
||||
#if os(iOS)
|
||||
fullscreenPlayerGestureEnabledToggle
|
||||
#endif
|
||||
horizontalPlayerGestureEnabledToggle
|
||||
SettingsHeader(text: "Seek gesture sensitivity".localized(), secondary: true)
|
||||
@ -76,6 +79,8 @@ struct PlayerControlsSettings: View {
|
||||
playerControlsLayoutPicker
|
||||
SettingsHeader(text: "Fullscreen size".localized(), secondary: true)
|
||||
fullScreenPlayerControlsLayoutPicker
|
||||
SettingsHeader(text: "Background opacity".localized(), secondary: true)
|
||||
playerControlsBackgroundOpacityPicker
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -109,7 +114,7 @@ struct PlayerControlsSettings: View {
|
||||
}
|
||||
|
||||
var controlsButtonsSection: some View {
|
||||
Section(header: SettingsHeader(text: "Controls Buttons".localized())) {
|
||||
Section(header: SettingsHeader(text: "Player Control Buttons".localized())) {
|
||||
controlButtonToggles
|
||||
}
|
||||
}
|
||||
@ -154,8 +159,12 @@ struct PlayerControlsSettings: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
private var fullscreenPlayerGestureEnabledToggle: some View {
|
||||
Toggle("Swipe up toggles fullscreen", isOn: $fullscreenPlayerGestureEnabled)
|
||||
}
|
||||
|
||||
private var horizontalPlayerGestureEnabledToggle: some View {
|
||||
Toggle("Seek with horizontal swipe on video", isOn: $horizontalPlayerGestureEnabled)
|
||||
Toggle("Seek with horizontal swipe", isOn: $horizontalPlayerGestureEnabled)
|
||||
}
|
||||
|
||||
private var avPlayerUsesSystemControlsToggle: some View {
|
||||
@ -202,6 +211,15 @@ struct PlayerControlsSettings: View {
|
||||
.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 {
|
||||
seekingDurationSetting("System controls", $systemControlsSeekDuration)
|
||||
.foregroundColor(systemControlsCommands == .restartAndAdvanceToNext ? .secondary : .primary)
|
||||
|
@ -18,8 +18,8 @@ struct PlayerSettings: View {
|
||||
@Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer
|
||||
@Default(.closeVideoOnEOF) private var closeVideoOnEOF
|
||||
#if os(iOS)
|
||||
@Default(.honorSystemOrientationLock) private var honorSystemOrientationLock
|
||||
@Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape
|
||||
@Default(.lockPortraitWhenBrowsing) private var lockPortraitWhenBrowsing
|
||||
@Default(.rotateToLandscapeOnEnterFullScreen) private var rotateToLandscapeOnEnterFullScreen
|
||||
#endif
|
||||
@Default(.closePiPOnNavigation) private var closePiPOnNavigation
|
||||
@ -87,7 +87,7 @@ struct PlayerSettings: View {
|
||||
}
|
||||
pauseOnHidingPlayerToggle
|
||||
closeVideoOnEOFToggle
|
||||
#if !os(tvOS)
|
||||
#if os(macOS)
|
||||
exitFullscreenOnEOFToggle
|
||||
#endif
|
||||
#if !os(macOS)
|
||||
@ -202,11 +202,12 @@ struct PlayerSettings: View {
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
Section(header: SettingsHeader(text: "Orientation".localized())) {
|
||||
if idiom == .pad {
|
||||
Section(header: SettingsHeader(text: "Fullscreen".localized())) {
|
||||
if Constants.isIPad {
|
||||
enterFullscreenInLandscapeToggle
|
||||
}
|
||||
honorSystemOrientationLockToggle
|
||||
|
||||
exitFullscreenOnEOFToggle
|
||||
rotateToLandscapeOnEnterFullScreenPicker
|
||||
}
|
||||
#endif
|
||||
@ -318,20 +319,15 @@ struct PlayerSettings: View {
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
private var honorSystemOrientationLockToggle: some View {
|
||||
Toggle("Honor orientation lock", isOn: $honorSystemOrientationLock)
|
||||
.disabled(!enterFullscreenInLandscape)
|
||||
}
|
||||
|
||||
private var enterFullscreenInLandscapeToggle: some View {
|
||||
Toggle("Enter fullscreen in landscape", isOn: $enterFullscreenInLandscape)
|
||||
Toggle("Enter fullscreen in landscape orientation", isOn: $enterFullscreenInLandscape)
|
||||
.disabled(lockPortraitWhenBrowsing)
|
||||
}
|
||||
|
||||
private var rotateToLandscapeOnEnterFullScreenPicker: some View {
|
||||
Picker("Rotate when entering fullscreen on landscape video", selection: $rotateToLandscapeOnEnterFullScreen) {
|
||||
Text("Landscape left").tag(FullScreenRotationSetting.landscapeRight)
|
||||
Text("Landscape right").tag(FullScreenRotationSetting.landscapeLeft)
|
||||
Text("No rotation").tag(FullScreenRotationSetting.disabled)
|
||||
Picker("Default orientation", selection: $rotateToLandscapeOnEnterFullScreen) {
|
||||
Text("Landscape left").tag(FullScreenRotationSetting.landscapeLeft)
|
||||
Text("Landscape right").tag(FullScreenRotationSetting.landscapeRight)
|
||||
}
|
||||
.modifier(SettingsPickerModifier())
|
||||
}
|
||||
|
@ -301,7 +301,7 @@ struct QualityProfileForm: View {
|
||||
func isFormatDisabled(_ format: QualityProfile.Format) -> Bool {
|
||||
guard backend == .appleAVPlayer else { return false }
|
||||
|
||||
let avPlayerFormats = [QualityProfile.Format.hls, .stream, .mp4]
|
||||
let avPlayerFormats = [.stream, QualityProfile.Format.hls]
|
||||
|
||||
return !avPlayerFormats.contains(format)
|
||||
}
|
||||
@ -315,7 +315,9 @@ struct QualityProfileForm: View {
|
||||
func isResolutionDisabled(_ resolution: ResolutionSetting) -> Bool {
|
||||
guard backend == .appleAVPlayer else { return false }
|
||||
|
||||
return resolution.value > .hd720p30
|
||||
let hd720p30 = Stream.Resolution.predefined(.hd720p30)
|
||||
|
||||
return resolution.value > hd720p30
|
||||
}
|
||||
|
||||
func initializeForm() {
|
||||
|
@ -38,12 +38,14 @@ struct SubscriptionsView: View {
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.labelStyle(.titleOnly)
|
||||
|
||||
subscriptionsMenu
|
||||
}
|
||||
.frame(maxWidth: 500)
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
subscriptionsMenu
|
||||
}
|
||||
|
||||
ToolbarItem {
|
||||
RequestErrorButton(error: requestError)
|
||||
}
|
||||
@ -88,7 +90,7 @@ struct SubscriptionsView: View {
|
||||
SettingsButtons()
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 12) {
|
||||
HStack {
|
||||
Image(systemName: "chevron.down.circle.fill")
|
||||
.foregroundColor(.accentColor)
|
||||
.imageScale(.large)
|
||||
|
@ -52,7 +52,7 @@ struct VerticalCells<Header: View>: View {
|
||||
.edgesIgnoringSafeArea(edgesIgnoringSafeArea)
|
||||
#if os(macOS)
|
||||
.background(Color.secondaryBackground)
|
||||
.frame(minWidth: 360)
|
||||
.frame(minWidth: Constants.contentViewMinWidth)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -24,14 +24,42 @@ struct VideoContextMenuView: View {
|
||||
|
||||
private var backgroundContext = PersistenceController.shared.container.newBackgroundContext()
|
||||
|
||||
@State private var isOverlayVisible = false
|
||||
|
||||
init(video: Video) {
|
||||
self.video = video
|
||||
_watchRequest = video.watchFetchRequest
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if video.videoID != Video.fixtureID {
|
||||
contextMenu
|
||||
ZStack {
|
||||
// Conditional overlay to block taps on underlying views
|
||||
if isOverlayVisible {
|
||||
Color.clear
|
||||
.contentShape(Rectangle())
|
||||
#if !os(tvOS)
|
||||
// This is not available on tvOS < 16 so we leave out.
|
||||
// TODO: remove #if when setting the minimum deployment target to >= 16
|
||||
.onTapGesture {
|
||||
// Dismiss overlay without triggering other interactions
|
||||
isOverlayVisible = false
|
||||
}
|
||||
#endif
|
||||
.ignoresSafeArea() // Ensure overlay covers the entire screen
|
||||
.accessibilityLabel("Dismiss context menu")
|
||||
.accessibilityHint("Tap to close the context")
|
||||
.accessibilityAddTraits(.isButton)
|
||||
}
|
||||
|
||||
if video.videoID != Video.fixtureID {
|
||||
contextMenu
|
||||
.onAppear {
|
||||
isOverlayVisible = true
|
||||
}
|
||||
.onDisappear {
|
||||
isOverlayVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -204,9 +204,14 @@ struct YatteeApp: App {
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
if Defaults[.lockPortraitWhenBrowsing] {
|
||||
Orientation.lockOrientation(.all, andRotateTo: .portrait)
|
||||
Orientation.lockOrientation(.portrait, 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
|
||||
@ -225,6 +230,17 @@ struct YatteeApp: App {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
self.migrateQualityProfiles()
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
self.migrateRotateToLandscapeOnEnterFullScreen()
|
||||
}
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
self.migrateLockPortraitWhenBrowsing()
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@ -253,6 +269,22 @@ 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 {
|
||||
#if os(iOS)
|
||||
return horizontalSizeClass == .compact ? .tab : .sidebar
|
||||
|
@ -54,7 +54,7 @@
|
||||
"Connected successfully (%@)" = "تم الاتصال بنجاح (%@)";
|
||||
"Country" = "البلد";
|
||||
"Country Name or Code" = "اسم الدولة أو الرمز";
|
||||
"Copy %@ link" = "نسخ رابط %@";
|
||||
"Copy %@ link" = "نسخ رابط%@";
|
||||
"Contributing" = "المساهمة";
|
||||
"Contact" = "التواصل";
|
||||
"Continue from %@" = "الإستمرار من %@";
|
||||
|
@ -4,7 +4,7 @@
|
||||
"Accounts" = "Konten";
|
||||
"Add Account" = "Konto hinzufügen";
|
||||
"Add Location" = "Ort hinzufügen";
|
||||
"Add Location..." = "Ort hinzufügen …";
|
||||
"Add Location..." = "Standort hinzufügen …";
|
||||
"Add to Playlist" = "Zu Wiedergabeliste hinzufügen";
|
||||
"Backend" = "Backend";
|
||||
"Badge color" = "Markierungsfarbe";
|
||||
|
@ -398,7 +398,7 @@
|
||||
"Hardware decoder" = "Décodeur matériel";
|
||||
"Stream FPS" = "IPS du flux";
|
||||
"Cached time" = "Temps mis en cache";
|
||||
"Dropped frames" = "Images perdus";
|
||||
"Dropped frames" = "Images perdues";
|
||||
"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\"";
|
||||
"Comments are disabled" = "Les commentaires sont désactivés";
|
||||
|
564
Shared/hu.lproj/Localizable.strings
Normal file
564
Shared/hu.lproj/Localizable.strings
Normal file
@ -0,0 +1,564 @@
|
||||
|
||||
|
||||
" 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";
|
98
Shared/kab.lproj/Localizable.strings
Normal file
98
Shared/kab.lproj/Localizable.strings
Normal file
@ -0,0 +1,98 @@
|
||||
|
||||
|
||||
"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";
|
@ -68,3 +68,20 @@
|
||||
"Close PiP and open player when application enters foreground" = "애플리케이션이 포그라운드에 진입하면 PiP를 닫고 플레이어를 열기";
|
||||
"Close PiP when player is opened" = "플레이어가 열리면 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" = "다음";
|
||||
|
@ -405,75 +405,75 @@
|
||||
"No chapters information available" = "Ingen tilgjengelig kapittelinfo";
|
||||
"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";
|
||||
"Paste" = "";
|
||||
"Codec" = "";
|
||||
"Open Videos" = "";
|
||||
"Files" = "";
|
||||
"Open Video" = "";
|
||||
"Show only icons" = "";
|
||||
"Show Open Videos toolbar button" = "";
|
||||
"Channels" = "";
|
||||
"Buttons labels" = "";
|
||||
"Could not open Files" = "";
|
||||
"Reload manifest" = "";
|
||||
"Right" = "";
|
||||
"Show Favorites" = "";
|
||||
"Only for local files and URLs" = "";
|
||||
"Enter link to open" = "";
|
||||
"Left" = "";
|
||||
"Are you sure you want to remove this document?" = "";
|
||||
"Recent Documents" = "";
|
||||
"Share files from Finder on a Mac\nor iTunes on Windows" = "";
|
||||
"Address" = "";
|
||||
"File" = "";
|
||||
"Share" = "";
|
||||
"Could not delete document" = "";
|
||||
"Are you sure you want to remove %@ location?" = "";
|
||||
"Size" = "";
|
||||
"Always" = "";
|
||||
"Video actions buttons" = "";
|
||||
"Edit Favorites…" = "";
|
||||
"Sample Rate" = "";
|
||||
"Show Inspector" = "";
|
||||
"Remove Location" = "";
|
||||
"Format" = "";
|
||||
"Verified" = "";
|
||||
"Show icons and text when space permits" = "";
|
||||
"Paste" = "Lim";
|
||||
"Codec" = "Kodeks";
|
||||
"Open Videos" = "Åpne Videoer";
|
||||
"Files" = "Filer";
|
||||
"Open Video" = "Åpne Video";
|
||||
"Show only icons" = "Vis kun ikoner";
|
||||
"Show Open Videos toolbar button" = "Vis Åpne Videoer verktøylinje knapp";
|
||||
"Channels" = "Kanaler";
|
||||
"Buttons labels" = "Knappe ettiketter";
|
||||
"Could not open Files" = "Kunne ikke åpne fil";
|
||||
"Reload manifest" = "Last manifest på nytt";
|
||||
"Right" = "Høyre";
|
||||
"Show Favorites" = "Vis Favoritter";
|
||||
"Only for local files and URLs" = "Kun for lokale filer og URLer";
|
||||
"Enter link to open" = "Gå inn i link for å åpne";
|
||||
"Left" = "Venstre";
|
||||
"Are you sure you want to remove this document?" = "Ønsker du virkelig å fjerne dette dokumentet";
|
||||
"Recent Documents" = "Nylige Dokumenter";
|
||||
"Share files from Finder on a Mac\nor iTunes on Windows" = "Del filer fra Finder på Mac\neller iTunes på Windows";
|
||||
"Address" = "Addresse";
|
||||
"File" = "Fil";
|
||||
"Share" = "Del";
|
||||
"Could not delete document" = "Klarte ikke å slette dokumentet";
|
||||
"Are you sure you want to remove %@ location?" = "Er du sikker på at du ønsker å fjerne %@ lokalisjon?";
|
||||
"Size" = "Størrelse";
|
||||
"Always" = "Alltid";
|
||||
"Video actions buttons" = "Knapper for video valg";
|
||||
"Edit Favorites…" = "Rediger Favoritter...";
|
||||
"Sample Rate" = "Stikkprøve rate";
|
||||
"Show Inspector" = "Vis inspektør";
|
||||
"Remove Location" = "Fjern lokasjon";
|
||||
"Format" = "Format";
|
||||
"Verified" = "Verifisert";
|
||||
"Show icons and text when space permits" = "Vis ikoner og tekst når det er plass";
|
||||
"Could not extract video ID" = "Kunne ikke pakke ut video-ID";
|
||||
"Open Files" = "";
|
||||
"Driver" = "";
|
||||
"Show Open Videos quick actions" = "";
|
||||
"Enter links to open, one per line" = "";
|
||||
"No locations available at the moment" = "";
|
||||
"Video Details" = "";
|
||||
"Add" = "";
|
||||
"Show Home" = "";
|
||||
"Pages buttons" = "";
|
||||
"Center" = "";
|
||||
"Shorts" = "";
|
||||
"Open" = "";
|
||||
"Locations Manifest" = "";
|
||||
"FPS" = "";
|
||||
"Inspector visibility" = "";
|
||||
"Show Documents" = "";
|
||||
"Open Files" = "Åpne Filer";
|
||||
"Driver" = "Driver";
|
||||
"Show Open Videos quick actions" = "Vis hurtigvalg for åpning av Videoer";
|
||||
"Enter links to open, one per line" = "Legg til lenker som skal åpnes, en per linje";
|
||||
"No locations available at the moment" = "Ingen lokasjoner tilgjengelig for øyeblikket";
|
||||
"Video Details" = "Video detaljer";
|
||||
"Add" = "Legg til";
|
||||
"Show Home" = "Vis Hjem";
|
||||
"Pages buttons" = "Side knapper";
|
||||
"Center" = "Senter";
|
||||
"Shorts" = "Shorts";
|
||||
"Open" = "Åpne";
|
||||
"Locations Manifest" = "Lokasjonsmanifest";
|
||||
"FPS" = "FPS";
|
||||
"Inspector visibility" = "Inspektør synlighet";
|
||||
"Show Documents" = "Vis Dokumenter";
|
||||
"Open logs in Finder" = "Åpne loggføring i Finder";
|
||||
"Documents" = "";
|
||||
"Documents" = "Dokumenter";
|
||||
"Could not update your token." = "Kunne ikke oppdatere symbolet ditt.";
|
||||
"Remove…" = "";
|
||||
"Hide" = "";
|
||||
"Actions buttons" = "";
|
||||
"Audio" = "";
|
||||
"Remove…" = "Fjern";
|
||||
"Hide" = "Gjem";
|
||||
"Actions buttons" = "Handlings knapper";
|
||||
"Audio" = "Lyd";
|
||||
"Could not extract SID from received cookies: %@" = "Kunne ikke hente ut SID fra mottatte informasjonskapsler: %@";
|
||||
"Playback Mode" = "";
|
||||
"Clear Queue before opening" = "";
|
||||
"Playback Mode" = "Avspilling Modus";
|
||||
"Clear Queue before opening" = "Tøm kø før åpning";
|
||||
"Could not create share link" = "Kunne ikke opprette delingslenke";
|
||||
"Could not refresh Playlists" = "";
|
||||
"Could not refresh Playlists" = "Kunne ikke gjenoppfriske spillelister";
|
||||
"Could not refresh Subscriptions" = "Kunne ikke gjenoppfriske abonnementer";
|
||||
"Translations" = "";
|
||||
"This URL could not be opened" = "";
|
||||
"For custom locations you can configure Frontend URL in Locations settings" = "";
|
||||
"Could not refresh Trending" = "";
|
||||
"If you want this app to be available in your language, join translation project." = "";
|
||||
"Translations" = "Oversettelser";
|
||||
"This URL could not be opened" = "Denne URL kunne ikke åpnes";
|
||||
"For custom locations you can configure Frontend URL in Locations settings" = "For tilpassede lokasjoner kan du konfigurere Frontend URL i lokasjons instillinger";
|
||||
"Could not refresh Trending" = "Kunne ikke gjenoppfriske trendende";
|
||||
"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";
|
||||
"This video could not be opened" = "Kunne ikke åpne videoen";
|
||||
"Could not open channel" = "Kunne ikke åpne kanal";
|
||||
"Could not open playlist" = "Kunne ikke åpne spilleliste";
|
||||
@ -482,21 +482,21 @@
|
||||
"Could not extract channel information" = "Kunne ikke hente kanalinfo";
|
||||
"Could not load video" = "Kunne ikke laste inn video";
|
||||
"Could not extract playlist ID" = "Kunne ikke hente ut spilleliste-ID";
|
||||
"Could not refresh Popular" = "";
|
||||
"Could not refresh Popular" = "Kunne ikke gjenoppfriske populært";
|
||||
"Channel could not be found" = "Fant ikke kanalen";
|
||||
"Live Streams" = "";
|
||||
"Channel" = "";
|
||||
"No documents" = "";
|
||||
"\"%@\" will be irreversibly removed from this device." = "";
|
||||
"Recent History" = "";
|
||||
"Home" = "";
|
||||
"Pages toolbar position" = "";
|
||||
"URL to Open" = "";
|
||||
"Video" = "";
|
||||
"Could not find any links to open in your clipboard" = "";
|
||||
"Show sidebar" = "";
|
||||
"Default Profile" = "";
|
||||
"Playback history is empty" = "";
|
||||
"Copy%@link" = "";
|
||||
"Share%@link" = "";
|
||||
"Live Streams" = "Direkte strømmer";
|
||||
"Channel" = "Kanal";
|
||||
"No documents" = "Ingen dokumenter";
|
||||
"\"%@\" will be irreversibly removed from this device." = "\"%@\" vil irreversibelt bli fjernet fra denne enheten";
|
||||
"Recent History" = "Nylig Historie";
|
||||
"Home" = "Hjem";
|
||||
"Pages toolbar position" = "Verktøylinjeposisjon for sider";
|
||||
"URL to Open" = "Åpne URL";
|
||||
"Video" = "Video";
|
||||
"Could not find any links to open in your clipboard" = "Kunne ikke finnen noen linker å åpne i utklippstavlen";
|
||||
"Show sidebar" = "Vis sidebar";
|
||||
"Default Profile" = "Standard profil";
|
||||
"Playback history is empty" = "Avspillingshisotrikk er tom";
|
||||
"Copy%@link" = "Kopier%@lenke";
|
||||
"Share%@link" = "Del%@lenke";
|
||||
"Share Logs..." = "Del logger …";
|
||||
|
@ -9,7 +9,7 @@
|
||||
"Add Account..." = "Добавить аккаунт...";
|
||||
"Add Location" = "Добавить локацию";
|
||||
"Add profile..." = "Добавить профиль...";
|
||||
"Add to %@" = "Добавить к %@";
|
||||
"Add to %@" = "Добавить к «%@»";
|
||||
"Add to Favorites" = "Добавить в избранное";
|
||||
"Add to Playlist" = "Добавить в плейлист";
|
||||
"Anonymous" = "Анонимный";
|
||||
|
564
Shared/ta.lproj/Localizable.strings
Normal file
564
Shared/ta.lproj/Localizable.strings
Normal file
@ -0,0 +1,564 @@
|
||||
|
||||
|
||||
"%@ 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" = "இறக்குமதி கோப்பில் கடவுச்சொல் சேமிக்கப்பட்டது";
|
@ -266,11 +266,11 @@
|
||||
"Sections" = "Bölümler";
|
||||
"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...";
|
||||
"Visibility" = "";
|
||||
"Visibility" = "Görünürlük";
|
||||
"Translations" = "Çeviriler";
|
||||
"Enter links to open, one per line" = "";
|
||||
"Open Videos" = "";
|
||||
"Playback Mode" = "";
|
||||
"Enter links to open, one per line" = "Açmak için bağlantıları girin, satır başı bir tane";
|
||||
"Open Videos" = "Videolar aç";
|
||||
"Playback Mode" = "Oynatma Modu";
|
||||
|
||||
/* Selected video was played on given date */
|
||||
"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.";
|
||||
"Thumbnails" = "Küçük Resimler";
|
||||
"Dropped frames" = "";
|
||||
"SponsorBlock API Instance" = "";
|
||||
"SponsorBlock API Instance" = "SponsorBlock API Oluşumu";
|
||||
|
||||
/* Selected video is being played */
|
||||
"Watching now" = "Şu an izlenen";
|
||||
"Video Details" = "Video Bilgileri";
|
||||
"Live Streams" = "";
|
||||
"Live Streams" = "Canlı yayınlar";
|
||||
"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" = "";
|
||||
"Default Profile" = "Varsayılan profil";
|
||||
|
||||
/* Player controls layout size for TV */
|
||||
"TV" = "TV";
|
||||
"Add" = "";
|
||||
"This URL could not be opened" = "";
|
||||
"Hide" = "";
|
||||
"Playing Next" = "";
|
||||
"Are you sure you want to remove this document?" = "";
|
||||
"Add" = "Ekle";
|
||||
"This URL could not be opened" = "Bu URL açılamadı";
|
||||
"Hide" = "Sakla";
|
||||
"Playing Next" = "Sonra Oynatılacak";
|
||||
"Are you sure you want to remove this document?" = "Bu belgeyi kaldırmak istediğinizden emin misiniz?";
|
||||
"Show channel name" = "Kanal adını göster";
|
||||
"Unlisted" = "";
|
||||
"Paste" = "";
|
||||
"Rate & Captions" = "";
|
||||
"Format" = "";
|
||||
"Unlisted" = "Liste dışı";
|
||||
"Paste" = "Yapıştır";
|
||||
"Rate & Captions" = "Puan ve Altyazılar";
|
||||
"Format" = "Format";
|
||||
"Right" = "Sağ";
|
||||
"Stream FPS" = "";
|
||||
"Cached time" = "";
|
||||
"Stream FPS" = "Yayın FPS’i";
|
||||
"Cached time" = "Önbelleğe alınan zaman";
|
||||
"Sign In Required" = "Giriş yapmanız gerekiyor";
|
||||
"Could not create share link" = "";
|
||||
"Could not create share link" = "Paylaş linki oluşturulamadı";
|
||||
"Locations Manifest" = "";
|
||||
"When partially watched video is played" = "Video kısmi olarak izlendiyse";
|
||||
"Open Video" = "";
|
||||
"Add Channels, Playlists and Searches to Favorites using" = "";
|
||||
"Always" = "";
|
||||
"Open Video" = "Video aç";
|
||||
"Add Channels, Playlists and Searches to Favorites using" = "Kanallar, oynatma listeleri ve favorileri aramayı ekleyin";
|
||||
"Always" = "Her zaman";
|
||||
|
||||
/* Video date filter in search */
|
||||
"Year" = "Yıl";
|
||||
"Playback queue is empty" = "";
|
||||
"Playback queue is empty" = "Oynatma kuyruğu boş";
|
||||
"Show Favorites" = "Favorileri Göster";
|
||||
"Driver" = "";
|
||||
"Driver" = "Sürücü";
|
||||
"Show progress of watching on thumbnails" = "İzlenme durumu görsellerde görünsün";
|
||||
"Left" = "";
|
||||
"URL to Open" = "";
|
||||
"Left" = "Sol";
|
||||
"URL to Open" = "Açılıcak URL";
|
||||
"Subscribe" = "Üye ol";
|
||||
"Yattee" = "Yattee";
|
||||
"Show Documents" = "Belgeleri Göster";
|
||||
"Press and hold remote button to open captions and quality menus" = "";
|
||||
"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";
|
||||
"No locations available at the moment" = "";
|
||||
"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";
|
||||
"Size" = "";
|
||||
"Size" = "Boyut";
|
||||
"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: %@";
|
||||
"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\"" = "";
|
||||
"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\"";
|
||||
|
||||
/* Video duration filter in search */
|
||||
"Short" = "Kısa";
|
||||
@ -337,29 +337,29 @@
|
||||
"Remove Location" = "";
|
||||
"Edit Favorites…" = "Favorileri düzenle…";
|
||||
"Show Open Videos toolbar button" = "Video Aç arayüzünü göster";
|
||||
"Sample Rate" = "";
|
||||
"Private" = "";
|
||||
"Sample Rate" = "Örnek hızı";
|
||||
"Private" = "Gizli";
|
||||
"Browsing" = "Gezinti";
|
||||
"Documents" = "";
|
||||
"FPS" = "";
|
||||
"Only for local files and URLs" = "";
|
||||
"Show sidebar" = "";
|
||||
"Current Playlist" = "";
|
||||
"Center" = "";
|
||||
"Address" = "";
|
||||
"Documents" = "Belgeler";
|
||||
"FPS" = "FPS";
|
||||
"Only for local files and URLs" = "Yalnızca yerel dosyalar ve URL'ler için";
|
||||
"Show sidebar" = "Kenar çubuğunu göster";
|
||||
"Current Playlist" = "Şuanki oynatma listesi";
|
||||
"Center" = "Orta";
|
||||
"Address" = "Adres";
|
||||
"Video actions buttons" = "Video eylem butonları";
|
||||
"Keep last played video in the queue after restart" = "";
|
||||
"Remove…" = "";
|
||||
"Keep last played video in the queue after restart" = "Yeniden başlatıldığında son izlenen videoyu sırada bırak";
|
||||
"Remove…" = "Kaldır…";
|
||||
"Trending" = "Trendler";
|
||||
"Statistics" = "";
|
||||
"Copy%@link" = "";
|
||||
"Now Playing" = "";
|
||||
"Could not delete document" = "";
|
||||
"No comments" = "";
|
||||
"Could not open Files" = "";
|
||||
"You need to select an account\nto access %@ section" = "";
|
||||
"Statistics" = "İstatistikler";
|
||||
"Copy%@link" = "%@bağlantıyı kopyala";
|
||||
"Now Playing" = "Şuanda çalıyor";
|
||||
"Could not delete document" = "Belge silinemedi";
|
||||
"No comments" = "Yorum yok";
|
||||
"Could not open Files" = "Dosyalar açılamadı";
|
||||
"You need to select an account\nto access %@ section" = "%@ kesitine erişebilmek için \nbir hesap seçmeniz gerekiyor";
|
||||
"Reload manifest" = "Yeniden Yükle";
|
||||
"Could not refresh Subscriptions" = "";
|
||||
"Could not refresh Subscriptions" = "Abonelikler yenilenemedi";
|
||||
|
||||
/* Subscriptions title */
|
||||
"Subscriptions" = "Üyelik";
|
||||
@ -368,29 +368,29 @@
|
||||
"Shuffle" = "Karıştır";
|
||||
"Buttons labels" = "Eylem düğmeleri etiketi";
|
||||
"Share %@ link" = "%@ bağlantısını paylaş";
|
||||
"Could not load streams" = "";
|
||||
"Playback history is empty" = "";
|
||||
"Show icons and text when space permits" = "";
|
||||
"Could not load streams" = "Akışlar yüklenemedi";
|
||||
"Playback history is empty" = "Oynatma geçmişi boş";
|
||||
"Show icons and text when space permits" = "Alan yeterliyse simgeleri ve metni göster";
|
||||
"unknown" = "Bilinmeyen";
|
||||
"Share..." = "Paylaş...";
|
||||
|
||||
/* Video sort order in search */
|
||||
"Views" = "İzlenme";
|
||||
"You need to create an instance and accounts\nto access %@ section" = "";
|
||||
"Verified" = "";
|
||||
"Open Files" = "";
|
||||
"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.";
|
||||
"Verified" = "Doğrulanmış";
|
||||
"Open Files" = "Dosyaları Aç";
|
||||
"Could not refresh Playlists" = "Çalma listesi güncellenemedi";
|
||||
"Actions buttons" = "";
|
||||
"Any format" = "";
|
||||
"Actions buttons" = "Eylem düğmeleri";
|
||||
"Any format" = "Herhangi bir format";
|
||||
"Show playback statistics" = "Oynatma istatistiklerini göster";
|
||||
"Pages buttons" = "Sayfa butonları";
|
||||
"Videos" = "Videolar";
|
||||
"Codec" = "";
|
||||
"Comments are disabled" = "";
|
||||
"Audio" = "";
|
||||
"Codec" = "Kodek";
|
||||
"Comments are disabled" = "Yorumlar devre dışı";
|
||||
"Audio" = "Ses";
|
||||
|
||||
|
||||
"Public" = "";
|
||||
"Public" = "Halka açık";
|
||||
"Files" = "Dosyalar";
|
||||
"Show Home" = "Ana Sayfayı Göster";
|
||||
"Open" = "Aç";
|
||||
@ -400,30 +400,30 @@
|
||||
"Clear Queue before opening" = "Açmadan önce Bekleme Listesini temizle";
|
||||
"Copy %@ link with time" = "Bağlantıyı %@ zaman ile kopyala";
|
||||
"Show Inspector" = "Denetleyiciyi Göster";
|
||||
"Make default" = "";
|
||||
"Are you sure you want to remove %@ location?" = "";
|
||||
"No chapters information available" = "";
|
||||
"Share Logs..." = "";
|
||||
"Enter link to open" = "";
|
||||
"Make default" = "Varsayılan yap";
|
||||
"Are you sure you want to remove %@ location?" = "%@ konumunu kaldırmak istediğinizden emin misiniz?";
|
||||
"No chapters information available" = "Bölüm bilgisi mevcut değil";
|
||||
"Share Logs..." = "Hata kayıtlarını paylaş…";
|
||||
"Enter link to open" = "Açılıcak bağlantıyı girin";
|
||||
"No documents" = "Belge yok";
|
||||
"Inspector visibility" = "Denetleyici görünümü";
|
||||
"Could not update your token." = "";
|
||||
"Could not find any links to open in your clipboard" = "";
|
||||
"Could not find any links to open in your clipboard" = "Panonuzda açılacak hiçbir bağlantı bulunamadı";
|
||||
|
||||
/* Video date filter in search */
|
||||
"Week" = "Hafta";
|
||||
"Sidebar" = "Kenar çubuğu";
|
||||
"Show only icons" = "";
|
||||
"Show only icons" = "Sadece ikonları göster";
|
||||
"Current: %@\n%@" = "Şuan: %@\n%@";
|
||||
"Show anonymous accounts" = "Anonim hesapları göster";
|
||||
"Could not open playlist" = "Çalma listesi açılamadı";
|
||||
"Round corners" = "";
|
||||
"Round corners" = "Yuvarlak köşeler";
|
||||
"URL" = "URL";
|
||||
"Recents" = "";
|
||||
"Recents" = "Yakın Zamandakiler";
|
||||
"Show sidebar when space permits" = "Alan olduğu sürece kenar çubuğunu göster";
|
||||
"System controls buttons" = "Sistem kontrol butonları";
|
||||
"Could not extract channel information" = "";
|
||||
"Public Locations" = "";
|
||||
"Could not extract channel information" = "Kanal bilgisi çıkarılamadı";
|
||||
"Public Locations" = "Herkese Açık Alanlar";
|
||||
"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 */
|
||||
@ -440,16 +440,16 @@
|
||||
"Smaller" = "Küçült";
|
||||
"Sort" = "Sırala";
|
||||
"This cannot be reverted" = "Geriye alınamaz";
|
||||
"Public Manifest" = "";
|
||||
"Public Manifest" = "Herkese Açık Bildiri";
|
||||
"You have no Playlists" = "Çalma listeniz bulunmamaktadır";
|
||||
"Watched" = "İzlendi";
|
||||
"Could not open video" = "";
|
||||
"Channel could not be found" = "";
|
||||
"Could not open video" = "Video açılamadı";
|
||||
"Channel could not be found" = "Kanal bulunamadı";
|
||||
"Show video length" = "Video uzunluğunu göster";
|
||||
"Source" = "Kaynak";
|
||||
"Welcome" = "Hoşgeldiniz";
|
||||
"Wi-Fi" = "Wi-Fi";
|
||||
"Could not open channel" = "";
|
||||
"Could not open channel" = "Kanal açılamadı";
|
||||
"This video could not be opened" = "Bu video oynatılamadı";
|
||||
"Could not extract playlist ID" = "Çalma listesi ID bilgisi alınamadı";
|
||||
"Could not load video" = "Video yüklenemedi";
|
||||
@ -458,50 +458,168 @@
|
||||
"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.";
|
||||
"Unsubscribe" = "Abonelikten çık";
|
||||
"Current Location" = "";
|
||||
"Stream & Player" = "";
|
||||
"Current Location" = "Şuanki konum";
|
||||
"Stream & Player" = "Yayın ve Oynatıcı";
|
||||
"Hardware decoder" = "Donanımsal çözücü";
|
||||
"Honor orientation lock" = "";
|
||||
"Honor orientation lock" = "Yön kilidine sadık kal";
|
||||
"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." = "";
|
||||
"Switch to public locations" = "";
|
||||
"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.";
|
||||
"Switch to public locations" = "Herkese açık konumlara geç";
|
||||
"%@ formats" = "";
|
||||
"Open logs in Finder" = "";
|
||||
"Could not refresh Popular" = "";
|
||||
"Open logs in Finder" = "Hata kayıtlarını Finder'da aç";
|
||||
"Could not refresh Popular" = "Popüler yenilenemedi";
|
||||
|
||||
/* SponsorBlock category name */
|
||||
"Self-promotion" = "";
|
||||
"Self-promotion" = "Kendi reklamını yapma";
|
||||
|
||||
/* Player controls layout size */
|
||||
"Small" = "Küçük";
|
||||
|
||||
/* SponsorBlock category name */
|
||||
"Sponsor" = "Sponsor";
|
||||
"System controls show buttons for %@" = "";
|
||||
"System controls show buttons for %@" = "%@ için sistem denetleme tuşlarını göster";
|
||||
"Show history" = "Kullanım geçmişini göster";
|
||||
"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.";
|
||||
"Switch to other public location" = "";
|
||||
"SponsorBlock" = "";
|
||||
"Switch to other public location" = "Başka herkese açık konuma geç";
|
||||
"SponsorBlock" = "SponsorBlock";
|
||||
"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.";
|
||||
"Could not refresh Trending" = "";
|
||||
"Could not refresh Trending" = "Trendde olanlar yenilenemedi";
|
||||
|
||||
/* Video date filter in search */
|
||||
"Today" = "Bugün";
|
||||
"Shorts" = "";
|
||||
"Channel" = "";
|
||||
"Channel" = "Kanal";
|
||||
"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." = "";
|
||||
"\"%@\" will be irreversibly removed from this device." = "\"%@\" bu cihazdan geri alınamaz şekilde silinecektir.";
|
||||
"Recent Documents" = "Son kullanılan belgeler";
|
||||
"Recent History" = "Yakın zamanda izlenilenler";
|
||||
"Show Open Videos quick actions" = "Video Aç hızlı eylemlerini göster";
|
||||
"Pages toolbar position" = "";
|
||||
"Video" = "";
|
||||
"Channels" = "";
|
||||
"Share" = "";
|
||||
"File" = "";
|
||||
"Share%@link" = "";
|
||||
"Video" = "Video";
|
||||
"Channels" = "Kanallar";
|
||||
"Share" = "Paylaş";
|
||||
"File" = "Dosya";
|
||||
"Share%@link" = "%@bağlantıyı paylaş";
|
||||
"Cache" = "Önbellek";
|
||||
"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";
|
||||
"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 %@";
|
||||
|
@ -1306,6 +1306,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -1500,6 +1501,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -2802,6 +2804,8 @@
|
||||
tr,
|
||||
ru,
|
||||
"zh-Hant",
|
||||
ta,
|
||||
hu,
|
||||
);
|
||||
mainGroup = 37D4B0BC2671614700C925CA;
|
||||
packageReferences = (
|
||||
@ -4089,6 +4093,8 @@
|
||||
3767F3332B25058300F257BC /* tr */,
|
||||
3767F3342B2505EF00F257BC /* ru */,
|
||||
37367E582B8F63C200436163 /* zh-Hant */,
|
||||
37E21DC52CDE528A008DF47C /* ta */,
|
||||
376EC9D82D1DD39800EC4500 /* hu */,
|
||||
);
|
||||
name = Localizable.strings;
|
||||
sourceTree = "<group>";
|
||||
@ -4103,7 +4109,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 192;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
|
||||
@ -4134,7 +4140,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 192;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||
@ -4165,7 +4171,7 @@
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 192;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
@ -4185,7 +4191,7 @@
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 192;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
@ -4326,6 +4332,7 @@
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 3;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
@ -4348,7 +4355,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "iOS/Yattee (iOS).entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 192;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
@ -4365,7 +4372,8 @@
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UIRequiresFullScreen = YES;
|
||||
INFOPLIST_KEY_UIStatusBarHidden = NO;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortraitUpsideDown";
|
||||
INFOPLIST_KEY_UIStatusBarStyle = "";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@ -4400,7 +4408,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 192;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1";
|
||||
@ -4414,7 +4422,8 @@
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UIRequiresFullScreen = YES;
|
||||
INFOPLIST_KEY_UIStatusBarHidden = NO;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortraitUpsideDown";
|
||||
INFOPLIST_KEY_UIStatusBarStyle = "";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@ -4452,7 +4461,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 192;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@ -4491,13 +4500,14 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 192;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = 78Z5H3M6RJ;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SELECTED_FILES = readonly;
|
||||
GCC_OPTIMIZATION_LEVEL = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = macOS/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.video";
|
||||
@ -4525,7 +4535,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 192;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -4548,7 +4558,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 192;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -4573,7 +4583,7 @@
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 192;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -4597,7 +4607,7 @@
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 192;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -4623,7 +4633,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 192;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@ -4663,7 +4673,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 192;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
"DEVELOPMENT_TEAM[sdk=appletvos*]" = 78Z5H3M6RJ;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@ -4703,7 +4713,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 192;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@ -4726,7 +4736,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 192;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@ -4909,7 +4919,7 @@
|
||||
repositoryURL = "https://github.com/sindresorhus/Defaults";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 7.0.0;
|
||||
minimumVersion = 7.3.1;
|
||||
};
|
||||
};
|
||||
372AA40E286D067B0000B1DC /* XCRemoteSwiftPackageReference "Repeat" */ = {
|
||||
@ -4924,8 +4934,8 @@
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/hyperoslo/Cache.git";
|
||||
requirement = {
|
||||
branch = master;
|
||||
kind = branch;
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 7.4.0;
|
||||
};
|
||||
};
|
||||
375B8AAF28B57F4200397B31 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = {
|
||||
@ -4940,16 +4950,16 @@
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/pinterest/PINCache";
|
||||
requirement = {
|
||||
branch = master;
|
||||
kind = branch;
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 3.0.4;
|
||||
};
|
||||
};
|
||||
379325D329A265A300181CF1 /* XCRemoteSwiftPackageReference "swift-log" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/yattee/swift-log.git";
|
||||
repositoryURL = "https://github.com/apple/swift-log.git";
|
||||
requirement = {
|
||||
branch = main;
|
||||
kind = branch;
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 1.6.1;
|
||||
};
|
||||
};
|
||||
3797104728D3D10600D5F53C /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */ = {
|
||||
@ -4957,7 +4967,7 @@
|
||||
repositoryURL = "https://github.com/SDWebImage/SDWebImageSwiftUI.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 2.1.0;
|
||||
minimumVersion = 2.2.7;
|
||||
};
|
||||
};
|
||||
3797757B268922D100DD52A8 /* XCRemoteSwiftPackageReference "siesta" */ = {
|
||||
@ -4965,7 +4975,7 @@
|
||||
repositoryURL = "https://github.com/bustoutsolutions/siesta";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 1.5.0;
|
||||
minimumVersion = 1.5.2;
|
||||
};
|
||||
};
|
||||
3799AC0728B03CEC001376F9 /* XCRemoteSwiftPackageReference "ActiveLabel.swift" */ = {
|
||||
@ -4981,7 +4991,7 @@
|
||||
repositoryURL = "https://github.com/Alamofire/Alamofire.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 5.0.0;
|
||||
minimumVersion = 5.9.1;
|
||||
};
|
||||
};
|
||||
37BD07C52698B27B003EBB87 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = {
|
||||
@ -4989,7 +4999,7 @@
|
||||
repositoryURL = "https://github.com/siteline/SwiftUI-Introspect.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 0.1.3;
|
||||
minimumVersion = 1.3.0;
|
||||
};
|
||||
};
|
||||
37CF8B8228535E4F00B71E37 /* XCRemoteSwiftPackageReference "SDWebImage" */ = {
|
||||
@ -4997,7 +5007,7 @@
|
||||
repositoryURL = "https://github.com/SDWebImage/SDWebImage";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 5.19.1;
|
||||
minimumVersion = 5.19.7;
|
||||
};
|
||||
};
|
||||
37D4B19B2671817900C925CA /* XCRemoteSwiftPackageReference "SwiftyJSON" */ = {
|
||||
@ -5005,7 +5015,7 @@
|
||||
repositoryURL = "https://github.com/SwiftyJSON/SwiftyJSON.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 5.0.0;
|
||||
minimumVersion = 5.0.2;
|
||||
};
|
||||
};
|
||||
37EE6DC328A305AD00BFD632 /* XCRemoteSwiftPackageReference "Reachability" */ = {
|
||||
@ -5013,7 +5023,7 @@
|
||||
repositoryURL = "https://github.com/ashleymills/Reachability.swift";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 5.1.0;
|
||||
minimumVersion = 5.2.3;
|
||||
};
|
||||
};
|
||||
37FB2847272207F000A57617 /* XCRemoteSwiftPackageReference "SDWebImageWebPCoder" */ = {
|
||||
@ -5021,7 +5031,7 @@
|
||||
repositoryURL = "https://github.com/SDWebImage/SDWebImageWebPCoder.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 0.8.4;
|
||||
minimumVersion = 0.14.6;
|
||||
};
|
||||
};
|
||||
37FB285227220D8400A57617 /* XCRemoteSwiftPackageReference "SDWebImagePINPlugin" */ = {
|
||||
@ -5036,8 +5046,8 @@
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/mpvkit/MPVKit.git";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = "0.38.0-fix";
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 0.39.0;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "193e96313b1796c618e74a8b1c36659d0f16f66278ff8045f9e02c42590ae5aa",
|
||||
"originHash" : "173de1b718eb898698eaba0221b46be9781899a652725709c8400d3ddfb01980",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "activelabel.swift",
|
||||
@ -15,8 +15,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Alamofire/Alamofire.git",
|
||||
"state" : {
|
||||
"revision" : "f455c2975872ccd2d9c81594c658af65716e9b9a",
|
||||
"version" : "5.9.1"
|
||||
"revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5",
|
||||
"version" : "5.10.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -24,8 +24,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/hyperoslo/Cache.git",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "d2e8f5a53c601b43371fdc90277d7f64b0e89a25"
|
||||
"revision" : "24e47109e31b2031cb26e25cc1b81b607496066c",
|
||||
"version" : "7.4.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -51,8 +51,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SDWebImage/libwebp-Xcode.git",
|
||||
"state" : {
|
||||
"revision" : "b2b1d20a90b14d11f6ef4241da6b81c1d3f171e4",
|
||||
"version" : "1.3.2"
|
||||
"revision" : "0d60654eeefd5d7d2bef3835804892c40225e8b2",
|
||||
"version" : "1.5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -60,8 +60,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/mpvkit/MPVKit.git",
|
||||
"state" : {
|
||||
"revision" : "ee72059235566df8b455bff15e3f83a1c9053e78",
|
||||
"version" : "0.38.0-fix"
|
||||
"revision" : "839dfa34b96029daef10b32d401c98edf17f04ae",
|
||||
"version" : "0.39.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -69,8 +69,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pinterest/PINCache",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "2fb85948463292c2e824148cf17dc62a4c217a94"
|
||||
"revision" : "2fb85948463292c2e824148cf17dc62a4c217a94",
|
||||
"version" : "3.0.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -87,8 +87,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/ashleymills/Reachability.swift",
|
||||
"state" : {
|
||||
"revision" : "7cbd73f46a7dfaeca079e18df7324c6de6d1834a",
|
||||
"version" : "5.2.3"
|
||||
"revision" : "21d1dc412cfecbe6e34f1f4c4eb88d3f912654a6",
|
||||
"version" : "5.2.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -105,8 +105,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SDWebImage/SDWebImage",
|
||||
"state" : {
|
||||
"revision" : "8a1be70a625683bc04d6903e2935bf23f3c6d609",
|
||||
"version" : "5.19.7"
|
||||
"revision" : "cac9a55a3ae92478a2c95042dcc8d9695d2129ca",
|
||||
"version" : "5.21.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -148,10 +148,10 @@
|
||||
{
|
||||
"identity" : "swift-log",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/yattee/swift-log.git",
|
||||
"location" : "https://github.com/apple/swift-log.git",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "3f3dc1390a2f116894887c352792dc8d5fa9e875"
|
||||
"revision" : "3d8596ed08bd13520157f0355e35caed215ffbfa",
|
||||
"version" : "1.6.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -168,8 +168,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/siteline/SwiftUI-Introspect.git",
|
||||
"state" : {
|
||||
"revision" : "121c146fe591b1320238d054ae35c81ffa45f45a",
|
||||
"version" : "0.12.0"
|
||||
"revision" : "807f73ce09a9b9723f12385e592b4e0aaebd3336",
|
||||
"version" : "1.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -66,6 +66,11 @@
|
||||
value = "Yes"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "IDELogRedirectionPolicy"
|
||||
value = "oslogToStdio"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
|
@ -1,22 +1,38 @@
|
||||
import AVFoundation
|
||||
import Defaults
|
||||
import Foundation
|
||||
import Logging
|
||||
import UIKit
|
||||
|
||||
final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var orientationLock = UIInterfaceOrientationMask.all
|
||||
|
||||
private var logger = Logger(label: "stream.yattee.app.delegate")
|
||||
private(set) static var instance: AppDelegate!
|
||||
|
||||
func application(_: UIApplication, supportedInterfaceOrientationsFor _: UIWindow?) -> UIInterfaceOrientationMask {
|
||||
orientationLock
|
||||
return orientationLock
|
||||
}
|
||||
|
||||
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { // swiftlint:disable:this discouraged_optional_collection
|
||||
Self.instance = self
|
||||
#if os(iOS)
|
||||
UIViewController.swizzleHomeIndicatorProperty()
|
||||
|
||||
#if !os(macOS)
|
||||
UIViewController.swizzleHomeIndicatorProperty()
|
||||
OrientationTracker.shared.startDeviceOrientationTracking()
|
||||
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
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
import CoreMotion
|
||||
import Defaults
|
||||
import Logging
|
||||
import UIKit
|
||||
|
||||
@ -35,7 +34,7 @@ enum Orientation {
|
||||
let rotateOrientationMask = rotateOrientation == .portrait ? UIInterfaceOrientationMask.portrait :
|
||||
rotateOrientation == .landscapeLeft ? .landscapeLeft :
|
||||
rotateOrientation == .landscapeRight ? .landscapeRight :
|
||||
.allButUpsideDown
|
||||
.all
|
||||
|
||||
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: rotateOrientationMask)) { error in
|
||||
print("denied rotation \(error)")
|
||||
|
@ -1,91 +1,86 @@
|
||||
import Defaults
|
||||
import Foundation
|
||||
import Logging
|
||||
import Repeat
|
||||
import SwiftUI
|
||||
|
||||
final class OrientationModel {
|
||||
static var shared = OrientationModel()
|
||||
let logger = Logger(label: "stream.yattee.orientation.model")
|
||||
|
||||
var orientation = UIInterfaceOrientation.portrait
|
||||
var lastOrientation: UIInterfaceOrientation?
|
||||
var orientationDebouncer = Debouncer(.milliseconds(300))
|
||||
var orientationObserver: Any?
|
||||
|
||||
@Default(.lockPortraitWhenBrowsing) private var lockPortraitWhenBrowsing
|
||||
@Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape
|
||||
|
||||
private var player = PlayerModel.shared
|
||||
|
||||
func configureOrientationUpdatesBasedOnAccelerometer() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func startOrientationUpdates() {
|
||||
// Ensure the orientation observer is active
|
||||
orientationObserver = NotificationCenter.default.addObserver(
|
||||
forName: OrientationTracker.deviceOrientationChangedNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { _ in
|
||||
guard !Defaults[.honorSystemOrientationLock],
|
||||
self.player.presentingPlayer,
|
||||
!self.player.playingInPictureInPicture,
|
||||
self.player.lockedOrientation.isNil
|
||||
self.logger.info("Notification received: Device orientation changed.")
|
||||
|
||||
// We only allow .portrait and are not showing the player
|
||||
guard (!self.player.presentingPlayer && !self.lockPortraitWhenBrowsing) || self.player.presentingPlayer
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
let orientation = OrientationTracker.shared.currentInterfaceOrientation
|
||||
self.logger.info("Current interface orientation: \(orientation)")
|
||||
|
||||
guard self.lastOrientation != orientation else {
|
||||
// Always update lastOrientation to keep track of the latest state
|
||||
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
|
||||
}
|
||||
|
||||
self.lastOrientation = orientation
|
||||
|
||||
DispatchQueue.main.async {
|
||||
guard Defaults[.enterFullscreenInLandscape],
|
||||
self.player.presentingPlayer
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
self.orientationDebouncer.callback = {
|
||||
DispatchQueue.main.async {
|
||||
if orientation.isLandscape {
|
||||
self.player.controls.presentingControls = false
|
||||
self.player.enterFullScreen(showControls: false)
|
||||
if self.enterFullscreenInLandscape, self.player.presentingPlayer {
|
||||
self.logger.info("Entering fullscreen because orientation is landscape.")
|
||||
self.player.controls.presentingControls = false
|
||||
self.player.enterFullScreen(showControls: false)
|
||||
}
|
||||
Orientation.lockOrientation(OrientationTracker.shared.currentInterfaceOrientationMask, andRotateTo: orientation)
|
||||
} else {
|
||||
self.player.exitFullScreen(showControls: false)
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
|
||||
self.logger.info("Exiting fullscreen because orientation is portrait.")
|
||||
if self.player.playingFullScreen {
|
||||
self.player.exitFullScreen(showControls: false)
|
||||
}
|
||||
if self.lockPortraitWhenBrowsing {
|
||||
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
||||
} else {
|
||||
Orientation.lockOrientation(OrientationTracker.shared.currentInterfaceOrientationMask, andRotateTo: orientation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.orientationDebouncer.call()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stopOrientationUpdates() {
|
||||
guard let observer = orientationObserver else { return }
|
||||
NotificationCenter.default.removeObserver(observer)
|
||||
}
|
||||
|
||||
func lockOrientation(_ orientation: UIInterfaceOrientationMask, andRotateTo rotateOrientation: UIInterfaceOrientation? = nil) {
|
||||
logger.info("Locking orientation to: \(orientation), rotating to: \(String(describing: rotateOrientation)).")
|
||||
if let rotateOrientation {
|
||||
self.orientation = rotateOrientation
|
||||
lastOrientation = rotateOrientation
|
||||
|
@ -11,6 +11,7 @@ struct InstancesSettings: View {
|
||||
|
||||
@State private var frontendURL = ""
|
||||
@State private var proxiesVideos = false
|
||||
@State private var invidiousCompanion = false
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@ObservedObject private var accounts = AccountsModel.shared
|
||||
@ -105,6 +106,16 @@ 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 {
|
||||
Spacer()
|
||||
Text("Accounts are not supported for the application of this instance")
|
||||
@ -191,6 +202,10 @@ struct InstancesSettings: View {
|
||||
private var proxiesVideosToggle: some View {
|
||||
Toggle("Proxy videos", isOn: $proxiesVideos)
|
||||
}
|
||||
|
||||
private var invidiousCompanionToggle: some View {
|
||||
Toggle("Invidious companion", isOn: $invidiousCompanion)
|
||||
}
|
||||
}
|
||||
|
||||
struct InstancesSettingsView_Previews: PreviewProvider {
|
||||
|
Loading…
x
Reference in New Issue
Block a user