Compare commits

...

145 Commits

Author SHA1 Message Date
Arkadiusz Fal
2a597ab3cb Add missing bundle platform 2025-03-23 15:08:04 +01:00
Arkadiusz Fal
4d662115e4 Update GitHub release workflow 2025-03-23 15:07:15 +01:00
Arkadiusz Fal
e068257f14 Bump build number to 200 2025-03-23 15:04:56 +01:00
Arkadiusz Fal
8b809fb0f1 Update GitHub release workflow 2025-03-23 15:04:16 +01:00
Arkadiusz Fal
d3e80f500e Update GitHub release workflow 2025-03-23 13:38:51 +01:00
Arkadiusz Fal
9343e9d023 Bump build number to 199 2025-03-23 13:35:52 +01:00
Arkadiusz Fal
e4b25b0f80 Update CHANGELOG 2025-03-23 13:35:52 +01:00
Arkadiusz Fal
09c2fb19a9 Fix swiftformat offenses 2025-03-23 13:32:46 +01:00
Arkadiusz Fal
043b07274e Update packages 2025-03-23 13:32:19 +01:00
Arkadiusz Fal
7f7e12d719
Merge pull request #851 from weblate/weblate-yattee-localizable-strings
Translations update from Hosted Weblate
2025-03-23 13:27:47 +01:00
Arkadiusz Fal
d990c6630e
Merge pull request #863 from lifo9/add-support-for-invidious-companion
Add support for invidious companion
2025-03-23 13:27:33 +01:00
Jakub Filo
5239b36cfe Add support for invidious companion 2025-03-18 22:56:45 +01:00
Ghost of Sparta
addc13ebfb
Translated using Weblate (Hungarian)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/hu/
2025-03-01 17:01:56 +00:00
azazaazur
2a6f26ec68
Translated using Weblate (Turkish)
Currently translated at 97.3% (547 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/tr/
2025-02-15 17:01:58 +01:00
Mohammed Al Otaibi
2e2f502d97
Translated using Weblate (Arabic)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ar/
2025-01-30 15:02:02 +01:00
Sophia Park
59afc2f4c7
Translated using Weblate (Korean)
Currently translated at 14.2% (80 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ko/
2025-01-06 08:07:37 +01:00
Finn
2f902e74bb
Translated using Weblate (German)
Currently translated at 94.8% (533 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/de/
2025-01-03 12:01:20 +00:00
Ghost of Sparta
500b75da4f
Translated using Weblate (Hungarian)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/hu/
2024-12-28 13:00:32 +01:00
Arkadiusz Fal
3a17cc4dee Add missing bundle platform 2024-12-26 19:43:42 +01:00
Arkadiusz Fal
16897338e6 Bump build number to 198 2024-12-26 19:41:35 +01:00
Arkadiusz Fal
7c870b8e61 Update changelog 2024-12-26 19:41:26 +01:00
Arkadiusz Fal
75d9c5c747 Add Hungarian locale 2024-12-26 19:10:51 +01:00
Arkadiusz Fal
9e0f1a72ab Update packages 2024-12-26 19:09:28 +01:00
Arkadiusz Fal
7f3b3ac0ab
Merge pull request #849 from derspyy/0length
Stop making videos with unknown length shorts.
2024-12-26 18:57:13 +01:00
Arkadiusz Fal
84b70b794b
Merge pull request #845 from weblate/weblate-yattee-localizable-strings
Translations update from Hosted Weblate
2024-12-26 18:56:34 +01:00
Ghost of Sparta
e6bae84162
Translated using Weblate (Hungarian)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/hu/
2024-12-25 21:00:36 +01:00
piuvas
9efbac3d15
make 0-length videos not shorts. 2024-12-21 12:55:44 -03:00
Blueberry
1289f57f60
Translated using Weblate (Russian)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ru/
2024-12-13 23:00:53 +01:00
Ghost of Sparta
cc03ab059b
Translated using Weblate (Hungarian)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/hu/
2024-12-10 18:00:28 +00:00
Ghost of Sparta
17484f65fd
Translated using Weblate (Hungarian)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/hu/
2024-12-09 16:00:36 +01:00
Ghost of Sparta
65247227e7
Translated using Weblate (Hungarian)
Currently translated at 93.2% (524 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/hu/
2024-12-08 11:00:44 +00:00
Ghost of Sparta
625c01aaac
Translated using Weblate (Hungarian)
Currently translated at 56.5% (318 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/hu/
2024-12-06 20:00:30 +01:00
Twig
7465ff9c5c
Translated using Weblate (Turkish)
Currently translated at 96.0% (540 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/tr/
2024-11-24 18:00:26 +01:00
Arkadiusz Fal
41de28a698 Bump build number to 197 2024-11-08 16:29:59 +01:00
Arkadiusz Fal
7baab7a88a Update github actions checkout version 2024-11-08 15:26:55 +01:00
Arkadiusz Fal
43599632b2 Update changelog 2024-11-08 15:20:06 +01:00
Arkadiusz Fal
e4f413ed2d Update packages 2024-11-08 15:20:06 +01:00
Arkadiusz Fal
661b7547c5 Fix application groups 2024-11-08 15:09:59 +01:00
Arkadiusz Fal
d69f410d92 Add Tamil language 2024-11-08 15:07:29 +01:00
Arkadiusz Fal
db7abe31ea
Merge pull request #836 from weblate/weblate-yattee-localizable-strings
Translations update from Hosted Weblate
2024-11-08 15:05:22 +01:00
ButterflyOfFire
fff36ece26
Translated using Weblate (Kabyle)
Currently translated at 17.0% (96 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/kab/
2024-11-08 14:04:18 +00:00
ButterflyOfFire
8a0e9ae75a
Added translation using Weblate (Kabyle) 2024-11-08 14:04:17 +00:00
trilame
d6e5b5ed76
Translated using Weblate (French)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/fr/
2024-11-08 14:04:16 +00:00
தமிழ்நேரம்
cef1a1caea
Translated using Weblate (Tamil)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ta/
2024-11-08 14:04:16 +00:00
தமிழ்நேரம்
6c6abe8c84
Added translation using Weblate (Tamil) 2024-11-08 14:04:15 +00:00
Istvan Tanczos
9732537602
Translated using Weblate (Hungarian)
Currently translated at 1.0% (6 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/hu/
2024-11-08 14:04:15 +00:00
Istvan Tanczos
c0deeabaed
Added translation using Weblate (Hungarian) 2024-11-08 14:04:14 +00:00
Arkadiusz Fal
f29dbcbe36
Merge pull request #834 from mmaalo/norwegian100
Norwegian Language
2024-11-08 15:04:05 +01:00
Arkadiusz Fal
c7e1a50e56
Merge pull request #833 from blennster/main
Fix uneven playback when using MPV and not syncing refreshrate
2024-11-08 15:03:51 +01:00
Arkadiusz Fal
dd205db15f
Merge pull request #824 from yattee/mpvkit-0-39-0
update MPVKit to v0.39.0
2024-11-08 15:03:18 +01:00
Arkadiusz Fal
9ca5d292ec
Merge pull request #820 from yattee/video-details-gestures
add drag gestures to video details
2024-11-08 15:03:07 +01:00
Arkadiusz Fal
748bc16342
Merge pull request #818 from yattee/changes-to-opengl-view
improvements to MPVGLView
2024-11-08 15:02:15 +01:00
Arkadiusz Fal
798d2fc67f
Merge pull request #817 from yattee/fix-subtitles
improved subtitle handling
2024-11-08 15:01:56 +01:00
Arkadiusz Fal
a5a88f8890
Merge pull request #815 from yattee/refined-audio-ducking
proper audio interrupt and route change handling
2024-11-08 15:01:27 +01:00
Arkadiusz Fal
f69ccb6bd6
Merge pull request #814 from yattee/fullscreen-gesture-toggle
allow users to disable fullscreen swipe gesture
2024-11-08 15:01:06 +01:00
Arkadiusz Fal
892b3dea17
Merge pull request #813 from yattee/update-introspect
Update SwiftUI-Introspect
2024-11-08 15:00:35 +01:00
mmaalo
9a11e9f9f5 completed Localizable.strings for norwegian language 2024-10-07 21:23:12 +02:00
Emil Blennow
055d5575ba
fix uneven playback when using MPV and not syncing refreshrate 2024-10-06 17:32:02 +02:00
Toni Förster
28b6a517b6
update MPVKit to v0.39.0
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-24 10:41:02 +02:00
Toni Förster
b4bcd0c0a0
add drag gestures to video details 2024-09-15 20:56:04 +02:00
Toni Förster
e62010d5d5
improvements to MPVGLView
- add Pixel Buffer Object to (PBO)
- add some debug logging
- add scissor testing
- add dirty region checking

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-14 15:04:17 +02:00
Toni Förster
3339e8cb1f
improved subtitle handling
- fix subtitle disabling not working
- make subtitle adding/removing async
- make subtitle menu non blocking

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-14 11:33:12 +02:00
Toni Förster
4855f9bead
fix tvOS build
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-13 11:48:40 +02:00
Toni Förster
a65ed67751
proper audio interrupt and route change handling
- set AVAudioSession inactive on pause and stop
- handle audio route changes
2024-09-13 11:21:07 +02:00
Toni Förster
72dcbe4515
allow user to disable fullscreen swipe gesture
furthermore, some rewording

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-12 16:57:48 +02:00
Toni Förster
7e02b08933
update SwiftUI-Introspect 2024-09-11 20:55:00 +02:00
Arkadiusz Fal
8596ee8811 Bump build number to 196 2024-09-11 09:37:13 +02:00
Arkadiusz Fal
894439ad5e Update CHANGELOG 2024-09-11 09:35:32 +02:00
Arkadiusz Fal
5dad7a1b47 Update dependencies 2024-09-11 09:31:24 +02:00
Arkadiusz Fal
6d48a825cd
Merge pull request #809 from yattee/refactor-search
refactor Search
2024-09-11 09:29:43 +02:00
Arkadiusz Fal
ed11e593ff
Merge pull request #810 from yattee/auto-retry-video-loading
Retry loading video before presenting error
2024-09-11 09:29:28 +02:00
Arkadiusz Fal
102dfba751
Merge pull request #805 from yattee/mpv-better-performance
MPV: improved A/V sync
2024-09-11 09:29:14 +02:00
Arkadiusz Fal
4202b27c03
Merge pull request #807 from yattee/more-robust-resolution-handling
more robust resolution handling
2024-09-11 09:29:00 +02:00
Arkadiusz Fal
2f937f74fa
Merge pull request #806 from yattee/orientation-location-cleanup
Orientation/Fullscreen fixes and cleanup
2024-09-11 09:28:39 +02:00
Toni Förster
34a957b28e
use system background color for background
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-11 08:55:18 +02:00
Toni Förster
0bef798341
add border around search field
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-10 19:01:41 +02:00
Toni Förster
28a7b6e981
auto retry loading the video before showing error
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-10 11:07:20 +02:00
Toni Förster
4663aab3da
refactor Search
- have a separate body view for each os
- macOS: set navigation title for search
- macOS: set min width to 835 for main content
- macOS: set main window min height to 600
- macOS: don’t have text behind the cancel button
- split SearchTextField into macOS and iOS/tvOS
- iOS: move search menu to the right
- iOS: unified search field
- make min width a constant
- add option to disable search suggestions

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-10 09:38:14 +02:00
Toni Förster
0de0445805
check if subtitles are added before removing them
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-09 14:18:49 +02:00
Toni Förster
9cb0325503
more robust resolution handling
Currently, we have a hard-coded list of resolutions. Since Invidious reports the actual resolution of a stream and does not hard-code them to a fixed value anymore, resolutions that are not in the list won’t be handled, and the stream cannot be played back.

Instead of hard-coding even more resolutions (and inadvertently might not cover all), we revert the list back to a finite set of resolutions, the users can select from. All other resolutions are handled dynamically and compared to the existing set of defined resolutions when selecting the best stream for playback.

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-09 12:59:39 +02:00
Toni Förster
5e85fd294c
MPV: improved A/V sync
- use displays refresh rate
- execute needs drawing with higher priority
- run create() with higher priority
- determine the number of threads used for rendering
- enable VSYNC and change video-sync  to display-resample
- iOS/tvOS: set new display refresh rate on change
- run setSize with higher priority
- add more options to MPVClient
- get refresh rate updates
- sync refresh rate to fps
- update CADisplayLink to current refresh rate
- update refresh rate on macOS
- Add experimental feature to sync display  with content fps

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-08 15:59:42 +02:00
Toni Förster
b2421da95d
apply new fullscreen logic to AppleAVPlayerView
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-06 17:13:08 +02:00
Toni Förster
4e4add3c42
fix double rotation
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-06 16:47:15 +02:00
Toni Förster
2185718d50
orientation fullscreen code cleanup
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-06 15:46:58 +02:00
Arkadiusz Fal
b0264aaabe Bump build number to 195 2024-09-05 23:01:29 +02:00
Arkadiusz Fal
035f3503c4 Update CHANGELOG 2024-09-05 23:01:19 +02:00
Arkadiusz Fal
e3ac11c172
Merge pull request #786 from stonerl/simplified-fullscreen-and-orientation
iOS: Simplified fullscreen and orientation
2024-09-05 22:59:54 +02:00
Arkadiusz Fal
7aed6ac0d9
Merge pull request #799 from stonerl/controls-background
player controls: add background opacity selection
2024-09-05 22:54:30 +02:00
Arkadiusz Fal
457c0ce7b3
Merge pull request #797 from stonerl/shorts-resolutions
add missing Shorts resolutions
2024-09-05 22:53:42 +02:00
Arkadiusz Fal
747baf3edd
Merge pull request #801 from stonerl/O2-for-macOS
use -O1 on macOS
2024-09-05 22:53:26 +02:00
Arkadiusz Fal
cd24a0322f
Merge pull request #802 from stonerl/buttons-interfere-with-search
macOS: only apply player shortcuts when window is active
2024-09-05 22:53:16 +02:00
Toni Förster
d525a22215
macOS only apply player shortcuts when window is active 2024-09-05 21:53:25 +02:00
Toni Förster
322a550666
simplified fullscreen and orientation handling
- iPad: rotate to device orientation on startup
- fixed controls in portrait fullscreen
- iOS: don’t call setNeedsDrawing multiple times
- On iOS we call set needs drawing only once.
- Added cooldown time to MPV.Client setNeedsDrawing to avoid multiple successive calls
- make fullscreen animation smoother
- dragGesture now calls toggleFullScreenAction
- fix tvOS and macOS build

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-05 18:17:14 +02:00
Toni Förster
98fa0b98e5
use -O1 on macOS
On macOS optimisation level -O3 seems to be a bit aggressive and can cause crashes when opening MPV.

- fixes #783

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-05 17:35:52 +02:00
Toni Förster
5313e4ead0
player controls: add background opacity selection
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-05 15:14:39 +02:00
Toni Förster
fa7b897e76
add missing Shorts resolutions
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-04 12:44:43 +02:00
Arkadiusz Fal
9bf3df1a29 Bump build number to 194 2024-09-04 09:38:15 +02:00
Arkadiusz Fal
34a805b986 Fix build issue 2024-09-04 09:37:38 +02:00
Arkadiusz Fal
36f680be62 Update CHANGELOG 2024-09-04 09:36:05 +02:00
Arkadiusz Fal
a27ab02433 Update dependencies 2024-09-04 09:33:23 +02:00
Arkadiusz Fal
59dd0785b3
Merge pull request #778 from stonerl/swipe-up-for-fullscreen
Gestures: swipe up toggles fullscreen
2024-09-04 09:16:23 +02:00
Arkadiusz Fal
d7be915e7e
Merge pull request #779 from stonerl/better-audio-ducking
Better audio ducking
2024-09-04 09:15:35 +02:00
Arkadiusz Fal
3752f67630
Merge pull request #780 from stonerl/add-overlay-to-video-context-menu
don’t open video when dismissing context menu
2024-09-04 09:15:03 +02:00
Arkadiusz Fal
dfe7565138
Merge pull request #789 from stonerl/fix-picture-in-picture
fix picture in picture
2024-09-04 09:14:34 +02:00
Arkadiusz Fal
4d02538cb9
Merge pull request #793 from stonerl/mpv-remove-video-layer
mpv: remove video layer when entering background
2024-09-04 09:14:05 +02:00
Arkadiusz Fal
3229528a09
Merge pull request #794 from stonerl/enable-o3-optimization
enable -O3
2024-09-04 09:13:23 +02:00
Arkadiusz Fal
fffc4f4a5f
Merge pull request #791 from stonerl/hi-res-invidious-logo
hi-res invidious logos
2024-09-04 09:13:01 +02:00
Toni Förster
e85bfe5007
enable -O3
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-03 21:40:48 +02:00
Toni Förster
b00b733fd5
don’t open video when dismissing context menu
fixes #510

fix tvOS build

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-03 21:21:34 +02:00
Toni Förster
119c663436
Gestures: swipe up toggles fullscreen 2024-09-03 21:20:56 +02:00
Toni Förster
e8fcee23ef
make audio ducking and interruption more robust
Signed-off-by: Toni Förster <toni.foerster@gmail.com>

fix audio ducking and bluetooth play/pause

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-03 21:19:30 +02:00
Toni Förster
d56ef74a99
fix picture in picture
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-03 21:17:20 +02:00
Toni Förster
98f5b1a22b
mpv: remove video layer when entering background
- fixes #792

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-03 14:58:18 +02:00
Toni Förster
f0b7bd3ab8
hi-res invidious logos
second try

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-03 01:01:52 +02:00
Arkadiusz Fal
2d7a101ce0 Bump build number to 193 2024-08-31 17:08:23 +02:00
Arkadiusz Fal
b2114174b4 Update CHANGELOG 2024-08-31 17:08:09 +02:00
Arkadiusz Fal
e9f502a486
Merge pull request #775 from stonerl/fix-crash-on-hls-live-playback 2024-08-31 14:54:08 +02:00
Toni Förster
6978e9437c
fix crash on HLS live playback
fixes #774

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-31 14:22:28 +02:00
Arkadiusz Fal
2026201a5f
Merge pull request #765 from stonerl/partial-fix-for-503
Update now playing info when using system controls – Partial fix for 503
2024-08-31 13:21:41 +02:00
Toni Förster
633af02577
don’t activate AVAudioSession on app start
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-31 13:19:54 +02:00
Arkadiusz Fal
1fd62f04aa Update dependencies 2024-08-31 13:08:46 +02:00
Arkadiusz Fal
e749307a0e
Merge pull request #772 from stonerl/re-enter-fullscreen-on-background-return
fixed fullscreen handling for backgrounding
2024-08-31 13:08:40 +02:00
Toni Förster
d76ec881be
fixed fullscreen handling for backgrounding
Currently when returning from background, the app is fullscreen but the video is in portrait mode.

Now when entering background we leave fullscreen and enter it again the the app is in foreground.

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-31 13:00:50 +02:00
Arkadiusz Fal
72a39a2c75
Merge pull request #767 from stonerl/new-defaults
Changes to defaults
2024-08-31 12:48:13 +02:00
Arkadiusz Fal
8a84db5a2d
Merge pull request #766 from stonerl/music-mode-thumbnail
make thumbnail fill the view in music mode
2024-08-31 12:47:52 +02:00
Arkadiusz Fal
663c37e3d2
Merge pull request #768 from stonerl/improved-image-loading
Video Thumbnails: retry 3 times fetching from URL
2024-08-31 12:47:31 +02:00
Arkadiusz Fal
ea2b329df2
Merge pull request #769 from stonerl/new-invidious-logo
circular invidious logo
2024-08-31 12:46:55 +02:00
Arkadiusz Fal
bd79f56800
Merge pull request #770 from stonerl/correct-landscape-setting
apply correct orientation
2024-08-31 12:46:41 +02:00
Arkadiusz Fal
9a650b4ac0
Merge pull request #762 from stonerl/allow-username-and-password-in-url
Invidious: propper HTTP basic auth support
2024-08-31 12:46:33 +02:00
Toni Förster
13382270d5
Revert "fixed some potential crashes"
This reverts commit bde9aade1173a8a466e3d54e2adc39bea49efe97.
2024-08-31 02:50:56 +02:00
Toni Förster
24626c2299
apply correct orientation
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-30 23:20:47 +02:00
Toni Förster
18ac577c7f
increase retry delay to 1 second
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-30 23:14:41 +02:00
Toni Förster
617af2cd20
format ordering adapted
- MP4 contains av1 encoded video which is not hardware accelerated.
- MP4 format removed from AVPlayer

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-30 22:04:31 +02:00
Toni Förster
1b778318dc
circular invidious logo
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-30 20:58:42 +02:00
Toni Förster
c9ce574c7a
VideoThumbnails: retry 3 times fetching from URL
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-30 20:01:25 +02:00
Toni Förster
9a1f0d7aaa
Changes to defaults
- Don’t use System Controls with AVPlayer by default
- Changed default order of the formats
- no default AVPlayer profiles except for tvOS
- new constants isIOS, isTvOS, isMacOS
- ArtWork for nowPlaying is .medium on iPhone .maxres on all other platforms
- changes to the player bar defaults

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-30 16:03:35 +02:00
Toni Förster
1cb695848c
make thumbnail fill the view in music mode
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-30 03:43:00 +02:00
Toni Förster
740a2f85ac
updateNowPlayingInfo also with System controls
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-29 15:10:04 +02:00
Toni Förster
1a22ac71be
move AVAudioSession configuration to AppDelegate
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-29 15:09:16 +02:00
Toni Förster
f0d581d512
remove autocorrect from location url TextField
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-28 18:05:40 +02:00
Toni Förster
049a42f2e8
allow basic auth with auth endpoint
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-28 17:40:16 +02:00
Toni Förster
cea2684a29
sanitise user and password in url
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-28 16:21:49 +02:00
Toni Förster
772e5016c4
make sure no log entries are lost
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-28 15:43:15 +02:00
Toni Förster
ed3d9a7d7c
invidious support for basic auth urls
This adds user, password and port to the proxy and thumbnail url, if they exist.

fixes #614 & #731

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-28 15:42:35 +02:00
Toni Förster
bde9aade11
fixed some potential crashes
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-28 15:39:03 +02:00
78 changed files with 3777 additions and 1193 deletions

View File

@ -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

View File

@ -1,10 +1,8 @@
## Build 192
* Fix mpv crashing on macOS by @stonerl in https://github.com/yattee/yattee/pull/754
* Refreshed icons for iOS and macOS by @stonerl in https://github.com/yattee/yattee/pull/752
* Add new MPVKit repo by @stonerl in https://github.com/yattee/yattee/pull/753
* Add Chinese (Simplified) - zh-Hans to LanguageCodes by @stonerl in https://github.com/yattee/yattee/pull/757
* Color changes to VideoActions by @stonerl in https://github.com/yattee/yattee/pull/759
* Hide VideoActions Bar when no buttons is visible by @stonerl in https://github.com/yattee/yattee/pull/760
## Build 199
## What's Changed
* Add support for invidious companion by @lifo9 in https://github.com/yattee/yattee/pull/863
* Translations update from Hosted Weblate by @weblate in https://github.com/yattee/yattee/pull/851
## Previous builds
* Add skip, play/pause, and fullscreen shortcuts to macOS player (by @rickykresslein)
@ -23,6 +21,50 @@
* Add import export of missing settings
* macOS: Fix settings windows layout
* Fix seek OSD layout on tvOS, revert OSD position
* Allow users to disable fullscreen swipe gesture by @stonerl in https://github.com/yattee/yattee/pull/814
* Proper audio interrupt and route change handling by @stonerl in https://github.com/yattee/yattee/pull/815
* Improved subtitle handling by @stonerl in https://github.com/yattee/yattee/pull/817
* Improvements to MPVGLView by @stonerl in https://github.com/yattee/yattee/pull/818
* Add drag gestures to video details by @stonerl in https://github.com/yattee/yattee/pull/820
* Fix uneven playback when using MPV and not syncing refreshrate by @blennster in https://github.com/yattee/yattee/pull/833
* Norwegian Language by @mmaalo in https://github.com/yattee/yattee/pull/834
* Translations update from Hosted Weblate by @weblate in https://github.com/yattee/yattee/pull/836
* Update MPVKit to v0.39.0 by @stonerl in https://github.com/yattee/yattee/pull/824
* Update SwiftUI-Introspect by @stonerl in https://github.com/yattee/yattee/pull/813
* Orientation/Fullscreen fixes and cleanup by @stonerl in https://github.com/yattee/yattee/pull/806
* More robust resolution handling by @stonerl in https://github.com/yattee/yattee/pull/807
* MPV: improved A/V sync by @stonerl in https://github.com/yattee/yattee/pull/805
* Retry loading video before presenting error by @stonerl in https://github.com/yattee/yattee/pull/810
* Refactor Search by @stonerl in https://github.com/yattee/yattee/pull/809
* iOS: Simplified fullscreen and orientation by @stonerl in https://github.com/yattee/yattee/pull/786
* macOS: only apply player shortcuts when window is active by @stonerl in https://github.com/yattee/yattee/pull/802
* player controls: add background opacity selection by @stonerl in https://github.com/yattee/yattee/pull/799
* add missing Shorts resolutions by @stonerl in https://github.com/yattee/yattee/pull/797
* use -O1 on macOS by @stonerl in https://github.com/yattee/yattee/pull/801
* Gestures: swipe up toggles fullscreen by @stonerl in https://github.com/yattee/yattee/pull/778
* dont open video when dismissing context menu by @stonerl in https://github.com/yattee/yattee/pull/780
* mpv: remove video layer when entering background by @stonerl in https://github.com/yattee/yattee/pull/793
* hi-res invidious logos by @stonerl in https://github.com/yattee/yattee/pull/791
* enable -O3 by @stonerl in https://github.com/yattee/yattee/pull/794
* Better audio ducking by @stonerl in https://github.com/yattee/yattee/pull/779
* fix picture in picture by @stonerl in https://github.com/yattee/yattee/pull/789
* Invidious: propper HTTP basic auth support by @stonerl in https://github.com/yattee/yattee/pull/762
* Apply correct orientation by @stonerl in https://github.com/yattee/yattee/pull/770
* Circular Invidious logo by @stonerl in https://github.com/yattee/yattee/pull/769
* Video Thumbnails: retry 3 times fetching from URL by @stonerl in https://github.com/yattee/yattee/pull/768
* Make thumbnail fill the view in music mode by @stonerl in https://github.com/yattee/yattee/pull/766
* Changes to defaults by @stonerl in https://github.com/yattee/yattee/pull/767
* Fixed fullscreen handling for backgrounding by @stonerl in https://github.com/yattee/yattee/pull/772
* Update now playing info when using system controls Partial fix for 503 by @stonerl in https://github.com/yattee/yattee/pull/765
* Stop making videos with unknown length shorts. by @derspyy in https://github.com/yattee/yattee/pull/849
* Add Hungarian to locales list
* Fix crash on HLS live playback by @stonerl in https://github.com/yattee/yattee/pull/775
* Fix mpv crashing on macOS by @stonerl in https://github.com/yattee/yattee/pull/754
* Refreshed icons for iOS and macOS by @stonerl in https://github.com/yattee/yattee/pull/752
* Add new MPVKit repo by @stonerl in https://github.com/yattee/yattee/pull/753
* Add Chinese (Simplified) - zh-Hans to LanguageCodes by @stonerl in https://github.com/yattee/yattee/pull/757
* Color changes to VideoActions by @stonerl in https://github.com/yattee/yattee/pull/759
* Hide VideoActions Bar when no buttons is visible by @stonerl in https://github.com/yattee/yattee/pull/760
* Improved stream resolution handling by @stonerl in https://github.com/yattee/yattee/pull/747
* Fix some potential crashes by @stonerl in https://github.com/yattee/yattee/pull/748
* Fix regression and improve curentChapter handling by @stonerl in https://github.com/yattee/yattee/pull/749

View File

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

View File

@ -10,11 +10,28 @@ struct AccountsBridge: Defaults.Bridge {
return nil
}
// Parse the urlString to check for embedded username and password
var sanitizedUrlString = value.urlString
if var urlComponents = URLComponents(string: value.urlString) {
if let user = urlComponents.user, let password = urlComponents.password {
// Sanitize the embedded username and password
let sanitizedUser = user.addingPercentEncoding(withAllowedCharacters: .urlUserAllowed) ?? user
let sanitizedPassword = password.addingPercentEncoding(withAllowedCharacters: .urlPasswordAllowed) ?? password
// Update the URL components with sanitized credentials
urlComponents.user = sanitizedUser
urlComponents.password = sanitizedPassword
// Reconstruct the sanitized URL
sanitizedUrlString = urlComponents.string ?? value.urlString
}
}
return [
"id": value.id,
"instanceID": value.instanceID ?? "",
"name": value.name,
"apiURL": value.urlString,
"apiURL": sanitizedUrlString,
"username": value.username,
"password": value.password ?? ""
]

View File

@ -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! {

View File

@ -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)
}
}

View File

@ -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 }) {

View File

@ -247,27 +247,27 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
}
func feed(_ page: Int?) -> Resource? {
resource(baseURL: account.url, path: "\(Self.basePath)/auth/feed")
resourceWithAuthCheck(baseURL: account.url, path: "\(Self.basePath)/auth/feed")
.withParam("page", String(page ?? 1))
}
var feed: Resource? {
resource(baseURL: account.url, path: basePathAppending("auth/feed"))
resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/feed"))
}
var subscriptions: Resource? {
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
}
func subscribe(_ channelID: String, onCompletion: @escaping () -> Void = {}) {
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
.child(channelID)
.request(.post)
.onCompletion { _ in onCompletion() }
}
func unsubscribe(_ channelID: String, onCompletion: @escaping () -> Void) {
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
.child(channelID)
.request(.delete)
.onCompletion { _ in onCompletion() }
@ -308,11 +308,11 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
return nil
}
return resource(baseURL: account.url, path: basePathAppending("auth/playlists"))
return resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/playlists"))
}
func playlist(_ id: String) -> Resource? {
resource(baseURL: account.url, path: basePathAppending("auth/playlists/\(id)"))
resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/playlists/\(id)"))
}
func playlistVideos(_ id: String) -> Resource? {
@ -445,6 +445,9 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
urlComponents.scheme = instanceURLComponents.scheme
urlComponents.host = instanceURLComponents.host
urlComponents.user = instanceURLComponents.user
urlComponents.password = instanceURLComponents.password
urlComponents.port = instanceURLComponents.port
guard let url = urlComponents.url else {
return nil
@ -495,7 +498,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
indexID: indexID,
live: json["liveNow"].boolValue,
upcoming: json["isUpcoming"].boolValue,
short: length <= Video.shortLength,
short: length <= Video.shortLength && length != 0.0,
publishedAt: publishedAt,
likes: json["likeCount"].int,
dislikes: json["dislikeCount"].int,
@ -553,6 +556,30 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
)
}
// Determines if the request requires Basic Auth credentials to be removed
private func needsBasicAuthRemoval(for path: String) -> Bool {
return path.hasPrefix("\(Self.basePath)/auth/")
}
// Creates a resource URL with consideration for removing Basic Auth credentials
private func createResourceURL(baseURL: URL, path: String) -> URL {
var resourceURL = baseURL
// Remove Basic Auth credentials if required
if needsBasicAuthRemoval(for: path), var urlComponents = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) {
urlComponents.user = nil
urlComponents.password = nil
resourceURL = urlComponents.url ?? baseURL
}
return resourceURL.appendingPathComponent(path)
}
func resourceWithAuthCheck(baseURL: URL, path: String) -> Resource {
let sanitizedURL = createResourceURL(baseURL: baseURL, path: path)
return super.resource(absoluteURL: sanitizedURL)
}
private func extractThumbnails(from details: JSON) -> [Thumbnail] {
details["videoThumbnails"].arrayValue.compactMap { json in
guard let url = json["url"].url,
@ -563,13 +590,20 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
return nil
}
// some of instances are not configured properly and return thumbnails links
// with incorrect scheme
// Some instances are not configured properly and return thumbnail links
// with an incorrect scheme or a missing port.
components.scheme = accountUrlComponents.scheme
components.port = accountUrlComponents.port
// If basic HTTP authentication is used,
// the username and password need to be prepended to the URL.
components.user = accountUrlComponents.user
components.password = accountUrlComponents.password
guard let thumbnailUrl = components.url else {
return nil
}
print("Final thumbnail URL: \(thumbnailUrl)")
return Thumbnail(url: thumbnailUrl, quality: .init(rawValue: quality)!)
}
@ -621,21 +655,29 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
if json["liveNow"].boolValue {
return hls
}
let videoId = json["videoId"].stringValue
return extractFormatStreams(from: json["formatStreams"].arrayValue) +
extractAdaptiveFormats(from: json["adaptiveFormats"].arrayValue) +
return extractFormatStreams(from: json["formatStreams"].arrayValue, videoId: videoId) +
extractAdaptiveFormats(from: json["adaptiveFormats"].arrayValue, videoId: videoId) +
hls
}
private func extractFormatStreams(from streams: [JSON]) -> [Stream] {
private func extractFormatStreams(from streams: [JSON], videoId: String?) -> [Stream] {
streams.compactMap { stream in
guard let streamURL = stream["url"].url else {
return nil
}
let finalURL: URL
if let videoId, let itag = stream["itag"].string, account.instance.invidiousCompanion {
let companionURLString = "\(account.instance.apiURLString)/latest_version?id=\(videoId)&itag=\(itag)"
finalURL = URL(string: companionURLString) ?? streamURL
} else {
finalURL = streamURL
}
return SingleAssetStream(
instance: account.instance,
avAsset: AVURLAsset(url: streamURL),
avAsset: AVURLAsset(url: finalURL),
resolution: Stream.Resolution.from(resolution: stream["resolution"].string ?? ""),
kind: .stream,
encoding: stream["encoding"].string ?? ""
@ -643,7 +685,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
}
}
private func extractAdaptiveFormats(from streams: [JSON]) -> [Stream] {
private func extractAdaptiveFormats(from streams: [JSON], videoId: String?) -> [Stream] {
let audioStreams = streams
.filter { $0["type"].stringValue.starts(with: "audio/mp4") }
.sorted {
@ -658,15 +700,29 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
return videoStreams.compactMap { videoStream in
guard let audioAssetURL = audioStream["url"].url,
let videoAssetURL = videoStream["url"].url
let videoAssetURL = videoStream["url"].url,
let audioItag = audioStream["itag"].string,
let videoItag = videoStream["itag"].string
else {
return nil
}
let finalAudioURL: URL
let finalVideoURL: URL
if let videoId, account.instance.invidiousCompanion {
let audioCompanionURLString = "\(account.instance.apiURLString)/latest_version?id=\(videoId)&itag=\(audioItag)"
let videoCompanionURLString = "\(account.instance.apiURLString)/latest_version?id=\(videoId)&itag=\(videoItag)"
finalAudioURL = URL(string: audioCompanionURLString) ?? audioAssetURL
finalVideoURL = URL(string: videoCompanionURLString) ?? videoAssetURL
} else {
finalAudioURL = audioAssetURL
finalVideoURL = videoAssetURL
}
return Stream(
instance: account.instance,
audioAsset: AVURLAsset(url: audioAssetURL),
videoAsset: AVURLAsset(url: videoAssetURL),
audioAsset: AVURLAsset(url: finalAudioURL),
videoAsset: AVURLAsset(url: finalVideoURL),
resolution: Stream.Resolution.from(resolution: videoStream["resolution"].stringValue),
kind: .adaptive,
encoding: videoStream["encoding"].string,

View File

@ -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

View File

@ -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]

View File

@ -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],

View File

@ -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],

View File

@ -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

View File

@ -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
}

View File

@ -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: []),

View File

@ -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)
{

View File

@ -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 {

View File

@ -102,7 +102,7 @@ final class AVPlayerBackend: PlayerBackend {
private var frequentTimeObserver: Any?
private var infrequentTimeObserver: Any?
private var playerTimeControlStatusObserver: Any?
private var playerTimeControlStatusObserver: NSKeyValueObservation?
private var statusObservation: NSKeyValueObservation?
@ -119,10 +119,30 @@ final class AVPlayerBackend: PlayerBackend {
#if os(iOS)
controller.player = avPlayer
#endif
logger.info("AVPlayerBackend initialized.")
}
deinit {
// Invalidate any observers to avoid memory leaks
statusObservation?.invalidate()
playerTimeControlStatusObserver?.invalidate()
// Remove any time observers added to AVPlayer
if let frequentObserver = frequentTimeObserver {
avPlayer.removeTimeObserver(frequentObserver)
}
if let infrequentObserver = infrequentTimeObserver {
avPlayer.removeTimeObserver(infrequentObserver)
}
// Remove notification observers
removeItemDidPlayToEndTimeObserver()
logger.info("AVPlayerBackend deinitialized.")
}
func canPlay(_ stream: Stream) -> Bool {
stream.kind == .hls || stream.kind == .stream || (stream.kind == .adaptive && stream.format == .mp4)
stream.kind == .hls || stream.kind == .stream
}
func playStream(
@ -161,7 +181,9 @@ final class AVPlayerBackend: PlayerBackend {
{
seek(to: 0, seekType: .loopRestart)
}
#if !os(macOS)
model.setAudioSessionActive(true)
#endif
avPlayer.play()
// Setting hasStarted to true the first time player started
@ -176,7 +198,9 @@ final class AVPlayerBackend: PlayerBackend {
guard avPlayer.timeControlStatus != .paused else {
return
}
#if !os(macOS)
model.setAudioSessionActive(false)
#endif
avPlayer.pause()
model.objectWillChange.send()
}
@ -190,6 +214,9 @@ final class AVPlayerBackend: PlayerBackend {
}
func stop() {
#if !os(macOS)
model.setAudioSessionActive(false)
#endif
avPlayer.replaceCurrentItem(with: nil)
hasStarted = false
}
@ -344,7 +371,7 @@ final class AVPlayerBackend: PlayerBackend {
let startPlaying = {
#if !os(macOS)
try? AVAudioSession.sharedInstance().setActive(true)
self.model.setAudioSessionActive(true)
#endif
self.setRate(self.model.currentRate)
@ -779,7 +806,7 @@ final class AVPlayerBackend: PlayerBackend {
opened = true
controller.startPictureInPicture()
} else {
print("PiP not possible, waited \(delay) seconds")
self.logger.info("PiP not possible, waited \(delay) seconds")
}
}
}

View File

@ -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
}

View File

@ -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, &params) < 0 {
print("failed to initialize mpv GL context")
logger.critical("failed to initialize mpv GL context")
exit(1)
}
@ -318,6 +345,31 @@ final class MPVClient: ObservableObject {
mpv.isNil ? false : getFlag("eof-reached")
}
var currentContainerFps: Int {
guard !mpv.isNil else { return 30 }
let fps = getDouble("container-fps")
return Int(fps.rounded())
}
func areSubtitlesAdded() async -> Bool {
guard !mpv.isNil else { return false }
let trackCount = await Task(operation: { getInt("track-list/count") }).value
guard trackCount > 0 else { return false }
for index in 0 ..< trackCount {
if let trackType = await Task(operation: { getString("track-list/\(index)/type") }).value, trackType == "sub" {
return true
}
}
return false
}
func logCurrentFps() {
let fps = currentContainerFps
logger.info("Current container FPS: \(fps)")
}
func seek(relative time: CMTime, completionHandler: ((Bool) -> Void)? = nil) {
guard !seeking else {
logger.warning("ignoring seek, another in progress")
@ -361,7 +413,7 @@ final class MPVClient: ObservableObject {
return
}
DispatchQueue.main.async { [weak self] in
DispatchQueue.main.async(qos: .userInteractive) { [weak self] in
guard let self else { return }
let model = self.backend.model
let aspectRatio = self.aspectRatio > 0 && self.aspectRatio < VideoPlayerView.defaultAspectRatio ? self.aspectRatio : VideoPlayerView.defaultAspectRatio
@ -389,10 +441,30 @@ final class MPVClient: ObservableObject {
}
func setNeedsDrawing(_ needsDrawing: Bool) {
// Check if we are currently in a cooldown period
guard !needsDrawingCooldown else {
logger.info("Not drawing, cooldown in progress")
return
}
logger.info("needs drawing: \(needsDrawing)")
// Set the cooldown flag to true and cancel any existing work item
needsDrawingCooldown = true
needsDrawingWorkItem?.cancel()
#if !os(macOS)
glView?.needsDrawing = needsDrawing
#endif
// Create a new DispatchWorkItem to reset the cooldown flag after 0.1 seconds
let workItem = DispatchWorkItem { [weak self] in
self?.needsDrawingCooldown = false
}
needsDrawingWorkItem = workItem
// Schedule the cooldown reset after 0.1 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: workItem)
}
func command(
@ -420,16 +492,59 @@ final class MPVClient: ObservableObject {
}
}
func updateRefreshRate(to refreshRate: Int) {
setString("display-fps-override", "\(String(refreshRate))")
logger.info("Updated refresh rate during playback to: \(refreshRate) Hz")
}
// Retrieve the screen's current refresh rate dynamically.
func getScreenRefreshRate() -> Int {
var refreshRate = 60 // Default to 60 Hz in case of failure
#if os(macOS)
// macOS implementation using NSScreen
if let screen = NSScreen.main,
let displayID = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? CGDirectDisplayID,
let mode = CGDisplayCopyDisplayMode(displayID),
mode.refreshRate > 0
{
refreshRate = Int(mode.refreshRate)
logger.info("Screen refresh rate: \(refreshRate) Hz")
} else {
logger.warning("Failed to get refresh rate from NSScreen.")
}
#else
// iOS implementation using UIScreen with a failover
let mainScreen = UIScreen.main
refreshRate = mainScreen.maximumFramesPerSecond
// Failover: if maximumFramesPerSecond is 0 or an unexpected value
if refreshRate <= 0 {
refreshRate = 60 // Fallback to 60 Hz
logger.warning("Failed to get refresh rate from UIScreen, falling back to 60 Hz.")
} else {
logger.info("Screen refresh rate: \(refreshRate) Hz")
}
#endif
currentRefreshRate = refreshRate
return refreshRate
}
func addVideoTrack(_ url: URL) {
command("video-add", args: [url.absoluteString])
}
func addSubTrack(_ url: URL) {
command("sub-add", args: [url.absoluteString])
func addSubTrack(_ url: URL) async {
await Task {
command("sub-add", args: [url.absoluteString])
}.value
}
func removeSubs() {
command("sub-remove")
func removeSubs() async {
await Task {
command("sub-remove")
}.value
}
func setVideoToAuto() {

View File

@ -153,7 +153,9 @@ extension PlayerBackend {
// Filter out non-HLS streams and streams with resolution more than maxResolution
let nonHLSStreams = streams.filter {
let isHLS = $0.kind == .hls
let isWithinResolution = $0.resolution <= maxResolution.value
// Check if the stream's resolution is within the maximum allowed resolution
let isWithinResolution = $0.resolution.map { $0 <= maxResolution.value } ?? false
logger.info("Stream ID: \($0.id) - Kind: \(String(describing: $0.kind)) - Resolution: \(String(describing: $0.resolution)) - Bitrate: \($0.bitrate ?? 0)")
logger.info("Is HLS: \(isHLS), Is within resolution: \(isWithinResolution)")
return !isHLS && isWithinResolution
@ -187,6 +189,7 @@ extension PlayerBackend {
}
let filteredStreams = adjustedStreams.filter { stream in
// Check if the stream's resolution is within the maximum allowed resolution
let isWithinResolution = stream.resolution <= maxResolution.value
logger.info("Filtered stream ID: \(stream.id) - Is within max resolution: \(isWithinResolution)")
return isWithinResolution

View File

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

View File

@ -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"],

View File

@ -6,12 +6,12 @@ struct QualityProfile: Hashable, Identifiable, Defaults.Serializable {
static var defaultProfile = Self(id: "default", backend: .mpv, resolution: .hd720p60, formats: [.stream], order: Array(Format.allCases.indices))
enum Format: String, CaseIterable, Identifiable, Defaults.Serializable {
case hls
case stream
case avc1
case stream
case webm
case mp4
case av1
case webm
case hls
var id: String {
rawValue
@ -30,18 +30,18 @@ struct QualityProfile: Hashable, Identifiable, Defaults.Serializable {
var streamFormat: Stream.Format? {
switch self {
case .hls:
return nil
case .stream:
return nil
case .avc1:
return .avc1
case .stream:
return nil
case .webm:
return .webm
case .mp4:
return .mp4
case .av1:
return .av1
case .webm:
return .webm
case .hls:
return nil
}
}
}
@ -59,14 +59,16 @@ struct QualityProfile: Hashable, Identifiable, Defaults.Serializable {
}
var formatsDescription: String {
if formats.count == Format.allCases.count {
switch formats.count {
case Format.allCases.count:
return "Any format".localized()
}
if formats.count <= 3 {
case 0:
return "No format selected".localized()
case 1 ... 3:
return formats.map(\.description).joined(separator: ", ")
default:
return String(format: "%@ formats".localized(), String(formats.count))
}
return String(format: "%@ formats".localized(), String(formats.count))
}
func isPreferred(_ stream: Stream) -> Bool {
@ -74,7 +76,8 @@ struct QualityProfile: Hashable, Identifiable, Defaults.Serializable {
return true
}
let resolutionMatch = !stream.resolution.isNil && resolution.value >= stream.resolution
let defaultResolution = Stream.Resolution.custom(height: 720, refreshRate: 30)
let resolutionMatch = resolution.value ?? defaultResolution >= stream.resolution
if resolutionMatch, formats.contains(.stream), stream.kind == .stream {
return true

View File

@ -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
}

View File

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

View File

@ -5,10 +5,27 @@ final class ThumbnailsModel: ObservableObject {
static var shared = ThumbnailsModel()
@Published var unloadable = Set<URL>()
private var retryCounts = [URL: Int]()
private let maxRetries = 3
private let retryDelay: TimeInterval = 1.0
func insertUnloadable(_ url: URL) {
DispatchQueue.main.async {
self.unloadable.insert(url)
let retries = (retryCounts[url] ?? 0) + 1
if retries >= maxRetries {
DispatchQueue.main.async {
self.unloadable.insert(url)
self.retryCounts.removeValue(forKey: url)
}
} else {
DispatchQueue.main.async {
self.retryCounts[url] = retries
}
DispatchQueue.global().asyncAfter(deadline: .now() + retryDelay) {
DispatchQueue.main.async {
self.retryCounts[url] = retries
}
}
}
}

View File

@ -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"
}

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="512pt" height="512pt" version="1.0" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><g><rect x="-.0072516" y=".00056299" width="512.01" height="512.02" fill="#575757" stroke-width=".063019"/><path d="m247.17 455.95c-19.792-0.78921-38.719-4.2564-57.154-10.47-60.968-20.55-108.68-68.579-127-127.86-7.8955-25.538-10.062-53.943-6.2586-82.067 3.7105-27.439 13.603-53.515 29.342-77.344 12.069-18.273 29.138-36.277 47.228-49.816 36.891-27.61 85.944-42.49 132.38-40.157 25.88 1.3001 49.939 6.765 73.106 16.606 8.1948 3.481 20.024 9.6845 27.696 14.525 14.15 8.9272 22.367 15.498 34.482 27.573 13.254 13.211 22.128 24.276 30.398 37.906 7.2081 11.879 14.099 27.15 18.229 40.397 1.5996 5.1305 4.442 16.456 5.6852 22.653 2.3908 11.917 2.6998 15.722 2.7049 33.312 6e-3 18.515-0.46256 24.413-2.9166 36.758-9.3274 46.92-35.58 88.167-74.872 117.64-22.814 17.112-50.027 29.535-78.547 35.858-16.714 3.7059-35.421 5.2453-54.498 4.4846zm-35.1-78.786c-5.3e-4 -0.52647-0.0741-2.0564-0.16311-3.3999l-0.16178-2.4427-4.7018-0.26271c-4.0477-0.22614-4.7968-0.33363-5.3847-0.77253-2.0235-1.5108-1.4679-6.0695 2.2494-18.457 0.8637-2.8781 3.3371-11.321 5.4966-18.762 2.1594-7.4409 5.2002-17.836 6.7573-23.101 1.5571-5.2648 4.1948-14.282 5.8615-20.038 1.6667-5.7562 3.6145-12.4 4.3284-14.764 0.71391-2.3641 3.2583-11.037 5.6542-19.272 4.9475-17.007 8.1626-27.723 8.9438-29.811 0.51852-1.3858 0.54785-1.4139 0.99761-0.95317 0.25486 0.26106 3.8462 7.3667 7.9807 15.79 4.1345 8.4236 13.089 26.573 19.898 40.331 17.188 34.73 37.849 76.578 43.261 87.622l4.5356 9.257 11.359-0.0895c6.2475-0.0492 11.615-0.19623 11.929-0.32672 0.5614-0.23385 0.54167-0.2959-1.3723-4.3176-1.068-2.2442-8.1436-16.601-15.724-31.904-48.687-98.293-61.22-123.86-67.889-138.48-4.7022-10.309-6.9031-14.807-7.7139-15.762-0.82931-0.97742-1.6319-1.0638-2.3704-0.25525-1.1993 1.313-4.1046 10.063-9.3869 28.27-2.0569 7.0899-6.5372 22.425-9.9562 34.077-6.6396 22.629-8.5182 29.037-14.33 48.883-2.0354 6.9495-4.7977 16.369-6.1385 20.931-1.3408 4.5628-4.033 13.81-5.9826 20.549-4.304 14.877-6.136 20.889-7.3886 24.25-2.1371 5.7334-2.5723 6.3292-4.9216 6.7384-0.88855 0.15472-2.4102 0.28196-3.3815 0.28275-2.1993 3e-3 -3.5494 0.36339-4.0558 1.0863-0.42176 0.60215-0.56421 4.8802-0.18251 5.4812 0.20573 0.32388 2.4672 0.37414 23.34 0.51873l8.6151 0.0597-7e-4 -0.95723zm36.751-205.59c4.3282-0.92335 8.4607-4.943 9.4374-9.1796 0.36569-1.5862 0.32543-4.9758-0.077-6.4799-0.85108-3.1813-3.2688-6.291-6.039-7.7675-3.8111-2.0313-9.456-2.0295-13.272 5e-3 -5.9828 3.1888-8.1556 11.089-4.7878 17.408 2.6995 5.0648 8.3611 7.3754 14.738 6.015z" fill="#f0f0f0" stroke-width=".025526"/></g><g transform="matrix(.069892 0 0 -.069892 44.236 474.48)"><path d="m2787 4669c-124-65-123-255 3-319 86-44 196-16 247 62 58 87 26 211-67 258-51 26-132 26-183-1z" fill="#00b6f0" stroke="#00b6f0" stroke-width="4.25"/><path d="m2882 4108c-12-16-63-166-102-303-30-104-101-350-165-565-20-69-58-199-85-290-26-91-64-221-85-290-20-69-58-199-85-290-26-91-64-221-85-290-20-69-57-195-81-280-59-207-93-299-115-310-10-6-35-10-56-10-73 0-84-8-81-54l3-41 228-3 228-2-3 47-3 48-73 3c-66 3-74 5-84 27-13 28 0 104 37 225 13 41 47 156 75 255s66 230 85 290c18 61 56 191 85 290 28 99 66 230 85 290 18 61 56 191 85 290 85 297 123 419 131 429 5 5 17-11 28-35 10-24 192-393 403-819s447-902 523-1058l139-282h168c92 0 168 4 168 8s-75 158-166 342c-588 1183-969 1958-1033 2100-29 63-69 151-89 195-44 95-58 110-80 83z" fill="#575757"/></g></svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

View File

@ -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
}
}

View File

@ -39,6 +39,38 @@ enum Constants {
#endif
}
static var isTvOS: Bool {
#if os(tvOS)
true
#else
false
#endif
}
static var isMacOS: Bool {
#if os(macOS)
true
#else
false
#endif
}
static var isIOS: Bool {
#if os(iOS)
true
#else
false
#endif
}
static var detailsVisibility: Bool {
#if os(iOS)
false
#else
true
#endif
}
static var progressViewScale: Double {
#if os(macOS)
0.4
@ -71,11 +103,11 @@ enum Constants {
#endif
}
static var detailsVisibility: Bool {
#if os(iOS)
false
static var contentViewMinWidth: Double {
#if os(macOS)
835
#else
true
0
#endif
}

View File

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

View File

@ -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()

View File

@ -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

View File

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

View File

@ -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()

View File

@ -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?> {

View File

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

View File

@ -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), &params)
// Call the render function and check for errors
let result = mpv_render_context_render(OpaquePointer(mpvGL), &params)
if result < 0 {
logger.error("mpv_render_context_render() failed with error code: \(result)")
} else {
logger.info("mpv_render_context_render() called successfully.")
}
}
}
}
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")
}

View File

@ -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)

View File

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

View File

@ -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)

View File

@ -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)

View File

@ -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 {}
}

View File

@ -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

View File

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

View File

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

View File

@ -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)
}

View File

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

View File

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

View File

@ -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()

View File

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

View File

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

View File

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

View File

@ -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)

View File

@ -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
}

View File

@ -24,14 +24,42 @@ struct VideoContextMenuView: View {
private var backgroundContext = PersistenceController.shared.container.newBackgroundContext()
@State private var isOverlayVisible = false
init(video: Video) {
self.video = video
_watchRequest = video.watchFetchRequest
}
var body: some View {
if video.videoID != Video.fixtureID {
contextMenu
ZStack {
// Conditional overlay to block taps on underlying views
if isOverlayVisible {
Color.clear
.contentShape(Rectangle())
#if !os(tvOS)
// This is not available on tvOS < 16 so we leave out.
// TODO: remove #if when setting the minimum deployment target to >= 16
.onTapGesture {
// Dismiss overlay without triggering other interactions
isOverlayVisible = false
}
#endif
.ignoresSafeArea() // Ensure overlay covers the entire screen
.accessibilityLabel("Dismiss context menu")
.accessibilityHint("Tap to close the context")
.accessibilityAddTraits(.isButton)
}
if video.videoID != Video.fixtureID {
contextMenu
.onAppear {
isOverlayVisible = true
}
.onDisappear {
isOverlayVisible = false
}
}
}
}

View File

@ -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

View File

@ -54,7 +54,7 @@
"Connected successfully (%@)" = "تم الاتصال بنجاح (%@)";
"Country" = "البلد";
"Country Name or Code" = "اسم الدولة أو الرمز";
"Copy %@ link" = "نسخ رابط %@";
"Copy %@ link" = "نسخ رابط%@";
"Contributing" = "المساهمة";
"Contact" = "التواصل";
"Continue from %@" = "الإستمرار من %@";

View File

@ -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";

View File

@ -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";

View 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";

View 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";

View File

@ -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" = "다음";

View File

@ -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 …";

View File

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

View 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" = "இறக்குமதி கோப்பில் கடவுச்சொல் சேமிக்கப்பட்டது";

View 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 FPSi";
"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" = "ı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ı";
"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" = "ı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 %@";

View File

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

View File

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

View File

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

View File

@ -1,22 +1,38 @@
import AVFoundation
import Defaults
import Foundation
import Logging
import UIKit
final class AppDelegate: UIResponder, UIApplicationDelegate {
var orientationLock = UIInterfaceOrientationMask.all
private var logger = Logger(label: "stream.yattee.app.delegate")
private(set) static var instance: AppDelegate!
func application(_: UIApplication, supportedInterfaceOrientationsFor _: UIWindow?) -> UIInterfaceOrientationMask {
orientationLock
return orientationLock
}
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { // swiftlint:disable:this discouraged_optional_collection
Self.instance = self
#if os(iOS)
UIViewController.swizzleHomeIndicatorProperty()
#if !os(macOS)
UIViewController.swizzleHomeIndicatorProperty()
OrientationTracker.shared.startDeviceOrientationTracking()
OrientationModel.shared.startOrientationUpdates()
// Configure the audio session for playback
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback)
} catch {
logger.error("Failed to set audio session category: \(error)")
}
// Begin receiving remote control events
UIApplication.shared.beginReceivingRemoteControlEvents()
#endif
return true
}

View File

@ -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)")

View File

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

View File

@ -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 {