mirror of
https://github.com/yattee/yattee.git
synced 2025-04-27 23:40:35 +05:30
Compare commits
114 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 |
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
|
||||
|
53
CHANGELOG.md
53
CHANGELOG.md
@ -1,13 +1,8 @@
|
||||
## Build 193
|
||||
* Invidious: propper HTTP basic auth support by @stonerl in https://github.com/yattee/yattee/pull/762
|
||||
* Apply correct orientation by @stonerl in https://github.com/yattee/yattee/pull/770
|
||||
* Circular Invidious logo by @stonerl in https://github.com/yattee/yattee/pull/769
|
||||
* Video Thumbnails: retry 3 times fetching from URL by @stonerl in https://github.com/yattee/yattee/pull/768
|
||||
* Make thumbnail fill the view in music mode by @stonerl in https://github.com/yattee/yattee/pull/766
|
||||
* Changes to defaults by @stonerl in https://github.com/yattee/yattee/pull/767
|
||||
* Fixed fullscreen handling for backgrounding by @stonerl in https://github.com/yattee/yattee/pull/772
|
||||
* Update now playing info when using system controls – Partial fix for 503 by @stonerl in https://github.com/yattee/yattee/pull/765
|
||||
* Fix crash on HLS live playback by @stonerl in https://github.com/yattee/yattee/pull/775
|
||||
## 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)
|
||||
@ -26,6 +21,44 @@
|
||||
* 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
|
||||
|
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.970.0)
|
||||
aws-sdk-core (3.202.2)
|
||||
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,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 }) {
|
||||
|
@ -498,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,
|
||||
@ -655,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 ?? ""
|
||||
@ -677,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 {
|
||||
@ -692,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 {
|
||||
|
@ -181,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
|
||||
@ -196,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()
|
||||
}
|
||||
@ -210,6 +214,9 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
}
|
||||
|
||||
func stop() {
|
||||
#if !os(macOS)
|
||||
model.setAudioSessionActive(false)
|
||||
#endif
|
||||
avPlayer.replaceCurrentItem(with: nil)
|
||||
hasStarted = false
|
||||
}
|
||||
@ -364,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)
|
||||
|
@ -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) {
|
||||
func addSubTrack(_ url: URL) async {
|
||||
await Task {
|
||||
command("sub-add", args: [url.absoluteString])
|
||||
}.value
|
||||
}
|
||||
|
||||
func removeSubs() {
|
||||
func removeSubs() async {
|
||||
await Task {
|
||||
command("sub-remove")
|
||||
}.value
|
||||
}
|
||||
|
||||
func setVideoToAuto() {
|
||||
|
@ -153,8 +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
|
||||
// Safely unwrap resolution and maxResolution.value to avoid crashes
|
||||
let isWithinResolution = ($0.resolution != nil && maxResolution.value != nil) ? $0.resolution! <= maxResolution.value! : false
|
||||
// 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
|
||||
@ -188,8 +189,8 @@ extension PlayerBackend {
|
||||
}
|
||||
|
||||
let filteredStreams = adjustedStreams.filter { stream in
|
||||
// Safely unwrap resolution and maxResolution.value to avoid crashes
|
||||
let isWithinResolution = (stream.resolution != nil && maxResolution.value != nil) ? stream.resolution! <= maxResolution.value! : false
|
||||
// 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?
|
||||
|
||||
@ -56,7 +56,6 @@ final class PlayerModel: ObservableObject {
|
||||
@Published var presentingPlayer = false { didSet { handlePresentationChange() } }
|
||||
@Published var activeBackend = PlayerBackendType.mpv
|
||||
@Published var forceBackendOnPlay: PlayerBackendType?
|
||||
@Published var wasFullscreen = false
|
||||
|
||||
var avPlayerBackend = AVPlayerBackend()
|
||||
var mpvBackend = MPVBackend()
|
||||
@ -131,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?
|
||||
@ -196,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]
|
||||
@ -220,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 {
|
||||
@ -503,7 +553,10 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
|
||||
private func handlePresentationChange() {
|
||||
#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 {
|
||||
@ -532,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
|
||||
}
|
||||
}
|
||||
@ -645,32 +696,37 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
|
||||
func closeCurrentItem(finished: Bool = false) {
|
||||
guard !closing else { return }
|
||||
closing = true
|
||||
|
||||
if playingFullScreen { exitFullScreen() }
|
||||
|
||||
Delay.by(0.3) { [weak self] in
|
||||
guard let self else { return }
|
||||
pause()
|
||||
videoBeingOpened = nil
|
||||
advancing = false
|
||||
forceBackendOnPlay = nil
|
||||
|
||||
closing = true
|
||||
controls.presentingControls = false
|
||||
|
||||
self.prepareCurrentItemForHistory(finished: finished)
|
||||
|
||||
self.hide()
|
||||
|
||||
Delay.by(0.8) { [weak self] in
|
||||
Delay.by(0.7) { [weak self] in
|
||||
guard let self else { return }
|
||||
self.closePiP()
|
||||
if playingInPictureInPicture { self.closePiP() }
|
||||
|
||||
withAnimation {
|
||||
self.currentItem = nil
|
||||
}
|
||||
self.updateNowPlayingInfo()
|
||||
|
||||
self.updateNowPlayingInfo()
|
||||
self.backend.closeItem()
|
||||
self.aspectRatio = VideoPlayerView.defaultAspectRatio
|
||||
self.resetAutoplay()
|
||||
self.closing = false
|
||||
self.playingFullScreen = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -679,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] = [.stream, .hls]
|
||||
|
||||
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
|
||||
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 retryCount < 3, self?.activeBackend == .appleAVPlayer, self?.avPlayerBackend.startPictureInPictureOnSwitch == false {
|
||||
// If PiP didn't start, try starting it again up to 3 times,
|
||||
} else if self?.activeBackend == .appleAVPlayer, self?.avPlayerBackend.startPictureInPictureOnSwitch == false {
|
||||
self?.avPlayerBackend.startPictureInPictureOnSwitch = true
|
||||
self?.avPlayerBackend.tryStartingPictureInPicture()
|
||||
retryCount += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -740,19 +782,27 @@ final class PlayerModel: ObservableObject {
|
||||
show()
|
||||
#endif
|
||||
|
||||
if previousActiveBackend == .mpv {
|
||||
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: self.activeBackend, to: .mpv, isInClosePip: true)
|
||||
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 {
|
||||
self?.backend.closePiP()
|
||||
self?.controls.resetTimer()
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -765,7 +815,7 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
|
||||
func toggleFullScreenAction() {
|
||||
toggleFullscreen(playingFullScreen, showControls: false)
|
||||
toggleFullscreen(playingFullScreen, showControls: false, initiatedByButton: true)
|
||||
}
|
||||
|
||||
func togglePiPAction() {
|
||||
@ -778,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
|
||||
@ -977,21 +1028,19 @@ 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)
|
||||
if wasFullscreen {
|
||||
wasFullscreen = false
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
Delay.by(0.3) {
|
||||
self?.enterFullScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
OrientationTracker.shared.startDeviceOrientationTracking()
|
||||
#endif
|
||||
|
||||
guard closePiPAndOpenPlayerOnEnteringForeground, playingInPictureInPicture else {
|
||||
@ -999,24 +1048,24 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
#if os(iOS)
|
||||
guard playingFullScreen else { return }
|
||||
wasFullscreen = playingFullScreen
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
Delay.by(0.3) {
|
||||
self?.exitFullScreen(showControls: false)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -1107,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)
|
||||
@ -1119,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 {
|
||||
@ -1138,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
|
||||
}
|
||||
|
||||
@ -1231,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 {
|
||||
|
@ -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"],
|
||||
|
@ -76,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)
|
||||
|
||||
enum PredefinedResolution: String, CaseIterable, Codable {
|
||||
// 8K UHD (16:9) Resolutions
|
||||
case hd4320p60
|
||||
case hd4320p50
|
||||
case hd4320p48
|
||||
case hd4320p30
|
||||
case hd4320p25
|
||||
case hd4320p24
|
||||
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
|
||||
|
||||
// 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
|
||||
// 720p (HD, 16:9) Resolutions
|
||||
case hd720p60, hd720p30
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated by Pixelmator Pro 3.6.7 -->
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Group">
|
||||
<path id="Path" fill="#f0f0f0" stroke="none" d="M 244.186371 511.752167 C 219.045975 510.71109 195.004303 506.137482 171.587616 497.941071 C 94.144188 470.833344 33.538929 407.477814 10.268302 329.279663 C 0.239193 295.592224 -2.512759 258.122925 2.318441 221.024231 C 7.031626 184.829193 19.597385 150.432068 39.58955 118.998993 C 54.919968 94.894897 76.601517 71.145599 99.579987 53.286163 C 146.440094 16.865601 208.748688 -2.762817 267.733124 0.314728 C 300.60672 2.029694 331.167175 9.238464 360.594604 22.219849 C 371.003937 26.811676 386.029724 34.994751 395.774933 41.379883 C 413.748718 53.155853 424.186218 61.823517 439.575043 77.75174 C 456.410675 95.178497 467.682678 109.774475 478.1875 127.753906 C 487.343475 143.423645 496.096527 163.56778 501.34256 181.042023 C 503.374359 187.809723 506.984924 202.749298 508.564056 210.923828 C 511.600952 226.643677 511.993439 231.662842 511.999939 254.866028 C 512.007507 279.289337 511.412323 287.069458 508.295135 303.353882 C 496.447205 365.24649 463.100311 419.655823 413.19043 458.533966 C 384.211426 481.106567 349.644592 497.493866 313.417664 505.834595 C 292.186981 510.723083 268.424774 512.753723 244.192581 511.750305 Z M 199.601273 407.824738 C 199.600616 407.13028 199.507141 405.112122 199.394073 403.339905 L 199.188583 400.117706 L 193.216202 399.771149 C 188.074692 399.472839 187.123169 399.331085 186.376404 398.752106 C 183.806091 396.759216 184.51181 390.745789 189.233658 374.405304 C 190.33078 370.608765 193.472549 359.471619 196.215607 349.656189 C 198.958557 339.840759 202.82106 326.12854 204.798935 319.183411 C 206.776825 312.238525 210.127289 300.343872 212.2444 292.751038 C 214.361496 285.15802 216.835648 276.394104 217.742447 273.275696 C 218.649307 270.157227 221.881256 258.716736 224.924591 247.853851 C 231.209076 225.419739 235.292999 211.284149 236.285294 208.529846 C 236.943924 206.701843 236.981201 206.664764 237.55249 207.272522 C 237.876221 207.616882 242.438049 216.990021 247.689819 228.101257 C 252.941574 239.212921 264.315857 263.153992 272.964874 281.302307 C 294.797607 327.11499 321.04184 382.317078 327.916321 396.885345 L 333.677551 409.096344 L 348.10614 408.978271 C 356.041901 408.913391 362.859833 408.719421 363.258698 408.547302 C 363.971802 408.238831 363.946777 408.156982 361.515564 402.851898 C 360.158997 399.891571 351.171295 380.953369 341.54248 360.767029 C 279.69873 231.107727 263.778931 197.38205 255.30777 178.09668 C 249.3349 164.497955 246.53923 158.564606 245.509338 157.30484 C 244.455933 156.015533 243.436447 155.901581 242.498398 156.96814 C 240.974991 158.700165 237.284607 170.24234 230.574875 194.259399 C 227.962112 203.611725 222.271103 223.840454 217.928177 239.210693 C 209.49437 269.060883 207.108093 277.513733 199.725769 303.692749 C 197.14035 312.859924 193.631577 325.285278 191.928467 331.303101 C 190.225357 337.321899 186.805634 349.519958 184.329178 358.409424 C 178.862122 378.033875 176.535034 385.964355 174.94397 390.397858 C 172.229355 397.960846 171.676529 398.746796 168.692398 399.28656 C 167.563736 399.490662 165.63089 399.658478 164.39711 399.659515 C 161.603485 399.663513 159.888535 400.138885 159.245316 401.092468 C 158.709564 401.88678 158.528641 407.530029 159.013474 408.322784 C 159.274811 408.750031 162.147385 408.816345 188.66066 409.00708 L 199.603806 409.085815 L 199.602936 407.82312 Z M 246.283508 136.628906 C 251.781326 135.410889 257.030548 130.108551 258.271179 124.519989 C 258.735718 122.427612 258.68457 117.95636 258.17337 115.97229 C 257.092316 111.775818 254.02124 107.673767 250.502441 105.726105 C 245.661484 103.0466 238.49118 103.04895 233.643967 105.732697 C 226.044434 109.939087 223.284454 120.360321 227.562363 128.69577 C 230.991348 135.376801 238.182877 138.424713 246.28302 136.630219 Z"/>
|
||||
<path id="Circle" fill="#575757" stroke="none" d="M 256 0 C 114.61525 0 0 114.615257 0 256 C 0 397.384735 114.61525 512 256 512 C 397.384735 512 512 397.384735 512 256 C 512 114.615257 397.384735 0 256 0 Z M 256 4 C 395.175446 4 508 116.824524 508 256 C 508 395.175446 395.175446 508 256 508 C 116.824524 508 4 395.175446 4 256 C 4 116.824524 116.824524 4 256 4 Z"/>
|
||||
</g>
|
||||
<g id="g1">
|
||||
<path id="path1" fill="#00b6f0" stroke="#00b6f0" stroke-width="0.297331" d="M 234.067764 106.178009 C 223.288239 112.003052 223.375183 129.030151 234.328568 134.765594 C 241.804688 138.70871 251.367157 136.199432 255.800674 129.209381 C 260.842682 121.41275 258.060883 110.300354 249.976257 106.088379 C 245.54274 103.758362 238.501282 103.758362 234.067764 106.178009 Z"/>
|
||||
<path id="path2" fill="#575757" stroke="none" d="M 242.34436 157.257843 C 241.282883 158.735199 236.77153 172.585571 233.321655 185.235535 C 230.667953 194.83847 224.387421 217.55304 218.72612 237.405212 C 216.956955 243.776398 213.595551 255.779999 211.207184 264.182556 C 208.907288 272.585114 205.545883 284.588745 203.688263 290.9599 C 201.919098 297.331055 198.557724 309.334686 196.169357 317.737244 C 193.869431 326.139801 190.508026 338.143433 188.650406 344.514587 C 186.881271 350.885742 183.608307 362.52005 181.485321 370.368591 C 176.266296 389.482056 173.258743 397.976929 171.312653 398.992645 C 170.428085 399.546631 168.216629 399.915985 166.359024 399.915985 C 159.901581 399.915985 158.928543 400.654663 159.193924 404.9021 L 159.459305 408.687897 L 179.627701 408.964874 L 199.796112 409.149567 L 199.530731 404.809784 L 199.265381 400.377686 L 192.807953 400.100647 C 186.969711 399.823669 186.262039 399.638977 185.377472 397.607605 C 184.227524 395.022217 185.377472 388.0047 188.650406 376.832092 C 189.800354 373.046326 192.807953 362.427704 195.28476 353.286499 C 197.761581 344.145264 201.122986 332.049255 202.803696 326.509125 C 204.395935 320.876648 207.757339 308.872986 210.322601 299.731781 C 212.799438 290.590576 216.160843 278.494598 217.841522 272.954437 C 219.433777 267.32196 222.795181 255.318329 225.360458 246.177094 C 232.879379 218.753387 236.240784 207.488464 236.948441 206.565094 C 237.390732 206.103394 238.45224 207.58078 239.425278 209.796844 C 240.309845 212.012909 256.40918 246.084747 275.073822 285.419769 C 293.738434 324.754761 314.614532 368.706543 321.337311 383.110901 L 333.632965 409.149567 L 348.493896 409.149567 C 356.632019 409.149567 363.354828 408.780212 363.354828 408.410889 C 363.354828 408.041534 356.72049 393.821838 348.670807 376.832092 C 296.657532 267.598999 262.955078 196.038818 257.293793 182.927185 C 254.728485 177.110016 251.19017 168.984467 249.421021 164.921692 C 245.52887 156.149841 244.290451 154.764771 242.34436 157.257843 Z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 6.6 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
|
||||
}
|
||||
}
|
@ -63,6 +63,14 @@ enum Constants {
|
||||
#endif
|
||||
}
|
||||
|
||||
static var detailsVisibility: Bool {
|
||||
#if os(iOS)
|
||||
false
|
||||
#else
|
||||
true
|
||||
#endif
|
||||
}
|
||||
|
||||
static var progressViewScale: Double {
|
||||
#if os(macOS)
|
||||
0.4
|
||||
@ -95,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,6 +15,7 @@ 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)
|
||||
@ -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 isOrientationLocked = Key<Bool>("isOrientationLocked", default: Constants.isIPhone)
|
||||
static let enterFullscreenInLandscape = Key<Bool>("enterFullscreenInLandscape", default: Constants.isIPhone)
|
||||
static let rotateToLandscapeOnEnterFullScreen = Key<FullScreenRotationSetting>(
|
||||
"rotateToLandscapeOnEnterFullScreen",
|
||||
default: Constants.isIPhone ? .landscapeRight : .disabled
|
||||
)
|
||||
static let rotateToLandscapeOnEnterFullScreen = Key<FullScreenRotationSetting>("rotateToLandscapeOnEnterFullScreen", default: .landscapeRight)
|
||||
#endif
|
||||
|
||||
static let closePiPOnNavigation = Key<Bool>("closePiPOnNavigation", default: false)
|
||||
@ -118,6 +116,7 @@ extension Defaults.Keys {
|
||||
|
||||
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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -360,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)
|
||||
@ -370,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")
|
||||
@ -426,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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -612,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
|
||||
|
||||
@ -270,6 +271,9 @@ struct PlayerControls: View {
|
||||
}
|
||||
} else if player.videoForDisplay == nil {
|
||||
Color.black
|
||||
} else if model.presentingControls {
|
||||
Color.black.opacity(playerControlsBackgroundOpacity)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -383,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
|
||||
// Toggle fullscreen on upward drag only when not disabled
|
||||
if fullscreenPlayerGestureEnabled, verticalDrag < -50 {
|
||||
player.toggleFullScreenAction()
|
||||
disableGestureTemporarily()
|
||||
return
|
||||
}
|
||||
|
||||
if verticalDrag > 60,
|
||||
player.playingFullScreen
|
||||
{
|
||||
player.exitFullScreen(showControls: false)
|
||||
#if os(iOS)
|
||||
if Constants.isIPhone {
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
|
||||
}
|
||||
#endif
|
||||
// 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,25 +1,23 @@
|
||||
import Repeat
|
||||
import SwiftUI
|
||||
|
||||
struct SearchTextField: View {
|
||||
private var navigation = NavigationModel.shared
|
||||
@ObservedObject private var state = SearchModel.shared
|
||||
|
||||
#if os(macOS)
|
||||
var body: some View {
|
||||
ZStack {
|
||||
#if os(macOS)
|
||||
fieldBorder
|
||||
#endif
|
||||
|
||||
HStack(spacing: 0) {
|
||||
#if os(macOS)
|
||||
Image(systemName: "magnifyingglass")
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 12, height: 12)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.horizontal, 6)
|
||||
.opacity(0.8)
|
||||
#endif
|
||||
|
||||
GeometryReader { geometry in
|
||||
TextField("Search...", text: $state.queryText) {
|
||||
state.changeQuery { query in
|
||||
query.query = state.queryText
|
||||
@ -28,37 +26,74 @@ struct SearchTextField: View {
|
||||
RecentsModel.shared.addQuery(state.queryText)
|
||||
}
|
||||
.disableAutocorrection(true)
|
||||
#if os(macOS)
|
||||
.frame(maxWidth: 190)
|
||||
.frame(maxWidth: geometry.size.width - 5)
|
||||
.textFieldStyle(.plain)
|
||||
#else
|
||||
.frame(minWidth: 200)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.horizontal, 5)
|
||||
.padding(.trailing, state.queryText.isEmpty ? 0 : 10)
|
||||
#endif
|
||||
.padding(.vertical, 8)
|
||||
.frame(height: 27, alignment: .center)
|
||||
}
|
||||
|
||||
if !state.queryText.isEmpty {
|
||||
clearButton
|
||||
} else {
|
||||
#if os(macOS)
|
||||
clearButton
|
||||
.opacity(0)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
.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
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
var body: some View {
|
||||
VStack {
|
||||
#if os(iOS)
|
||||
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
|
||||
}
|
||||
}
|
||||
.environment(\.listingStyle, searchListingStyle)
|
||||
.onAppear {
|
||||
if let query {
|
||||
state.queryText = query.query
|
||||
state.resetQuery(query)
|
||||
updateFavoriteItem()
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
if accounts.app.supportsSearchSuggestions, state.query.query != state.queryText {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#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: 280)
|
||||
.frame(maxWidth: 262)
|
||||
.opacity(state.queryText.isEmpty ? 0 : 1)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.environment(\.listingStyle, searchListingStyle)
|
||||
.toolbar {
|
||||
#if os(macOS)
|
||||
ToolbarItemGroup(placement: toolbarPlacement) {
|
||||
ListingStyleButtons(listingStyle: $searchListingStyle)
|
||||
HideWatchedButtons()
|
||||
@ -84,7 +213,6 @@ struct SearchView: View {
|
||||
HStack {
|
||||
Text("Sort:")
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
searchSortOrderPicker
|
||||
}
|
||||
}
|
||||
@ -101,7 +229,6 @@ struct SearchView: View {
|
||||
SearchTextField()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.onAppear {
|
||||
if let query {
|
||||
@ -124,23 +251,7 @@ struct SearchView: View {
|
||||
} else {
|
||||
updateFavoriteItem()
|
||||
}
|
||||
|
||||
state.loadSuggestions(newQuery)
|
||||
|
||||
#if os(tvOS)
|
||||
searchDebounce.invalidate()
|
||||
recentsDebounce.invalidate()
|
||||
|
||||
searchDebounce.debouncing(2) {
|
||||
state.changeQuery { query in
|
||||
query.query = newQuery
|
||||
}
|
||||
}
|
||||
|
||||
recentsDebounce.debouncing(10) {
|
||||
recents.addQuery(newQuery)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.onChange(of: searchSortOrder) { order in
|
||||
state.changeQuery { query in
|
||||
@ -160,35 +271,10 @@ struct SearchView: View {
|
||||
updateFavoriteItem()
|
||||
}
|
||||
}
|
||||
#if os(tvOS)
|
||||
.searchable(text: $state.queryText) {
|
||||
if !state.queryText.isEmpty {
|
||||
ForEach(state.querySuggestions, id: \.self) { suggestion in
|
||||
Text(suggestion)
|
||||
.searchCompletion(suggestion)
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
.ignoresSafeArea(.keyboard, edges: .bottom)
|
||||
.frame(minWidth: Constants.contentViewMinWidth)
|
||||
.navigationTitle("Search")
|
||||
}
|
||||
#endif
|
||||
#if os(iOS)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
searchMenu
|
||||
}
|
||||
ToolbarItem(placement: .principal) {
|
||||
if #available(iOS 15, *) {
|
||||
FocusableSearchTextField()
|
||||
} else {
|
||||
SearchTextField()
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
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(.medium)
|
||||
.imageScale(.large)
|
||||
}
|
||||
}
|
||||
}
|
||||
#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,12 +164,16 @@ struct BrowsingSettings: View {
|
||||
#if os(iOS)
|
||||
Toggle("Show Documents", isOn: $showDocuments)
|
||||
|
||||
if Constants.isIPad {
|
||||
Toggle("Lock portrait mode", isOn: $lockPortraitWhenBrowsing)
|
||||
.onChange(of: lockPortraitWhenBrowsing) { lock in
|
||||
if lock {
|
||||
enterFullscreenInLandscape = true
|
||||
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
||||
} else {
|
||||
Orientation.lockOrientation(.allButUpsideDown)
|
||||
enterFullscreenInLandscape = false
|
||||
Orientation.lockOrientation(.all)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -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)
|
||||
|
@ -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)
|
||||
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) {
|
||||
Picker("Default orientation", selection: $rotateToLandscapeOnEnterFullScreen) {
|
||||
Text("Landscape left").tag(FullScreenRotationSetting.landscapeLeft)
|
||||
Text("Landscape right").tag(FullScreenRotationSetting.landscapeRight)
|
||||
Text("No rotation").tag(FullScreenRotationSetting.disabled)
|
||||
}
|
||||
.modifier(SettingsPickerModifier())
|
||||
}
|
||||
|
@ -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 {
|
||||
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 = 193;
|
||||
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 = 193;
|
||||
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 = 193;
|
||||
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 = 193;
|
||||
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 = 193;
|
||||
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 = 193;
|
||||
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 = 193;
|
||||
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 = 193;
|
||||
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 = 193;
|
||||
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 = 193;
|
||||
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 = 193;
|
||||
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 = 193;
|
||||
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 = 193;
|
||||
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 = 193;
|
||||
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 = 193;
|
||||
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 = 193;
|
||||
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" : "515d8e68c4a31658288fb3f94789ee539399b042082c08c39f4c03c27fd8860c",
|
||||
"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" : "81a0277cbc6b63f4e0cd6f42c4abefa1011bbfa9"
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -1,4 +1,5 @@
|
||||
import AVFoundation
|
||||
import Defaults
|
||||
import Foundation
|
||||
import Logging
|
||||
import UIKit
|
||||
@ -6,11 +7,11 @@ import UIKit
|
||||
final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var orientationLock = UIInterfaceOrientationMask.all
|
||||
|
||||
private var logger = Logger(label: "stream.yattee.app.delegalate")
|
||||
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
|
||||
@ -19,6 +20,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
#if !os(macOS)
|
||||
UIViewController.swizzleHomeIndicatorProperty()
|
||||
OrientationTracker.shared.startDeviceOrientationTracking()
|
||||
OrientationModel.shared.startOrientationUpdates()
|
||||
|
||||
// Configure the audio session for playback
|
||||
do {
|
||||
|
@ -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 {
|
||||
return
|
||||
// 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.")
|
||||
}
|
||||
|
||||
self.lastOrientation = orientation
|
||||
// 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
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
guard Defaults[.enterFullscreenInLandscape],
|
||||
self.player.presentingPlayer
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
self.orientationDebouncer.callback = {
|
||||
DispatchQueue.main.async {
|
||||
if orientation.isLandscape {
|
||||
if self.enterFullscreenInLandscape, self.player.presentingPlayer {
|
||||
self.logger.info("Entering fullscreen because orientation is landscape.")
|
||||
self.player.controls.presentingControls = false
|
||||
self.player.enterFullScreen(showControls: false)
|
||||
}
|
||||
Orientation.lockOrientation(OrientationTracker.shared.currentInterfaceOrientationMask, andRotateTo: orientation)
|
||||
} else {
|
||||
self.logger.info("Exiting fullscreen because orientation is portrait.")
|
||||
if self.player.playingFullScreen {
|
||||
self.player.exitFullScreen(showControls: false)
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
|
||||
}
|
||||
if self.lockPortraitWhenBrowsing {
|
||||
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
||||
} else {
|
||||
Orientation.lockOrientation(OrientationTracker.shared.currentInterfaceOrientationMask, andRotateTo: orientation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.orientationDebouncer.call()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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