Compare commits

..

122 Commits

Author SHA1 Message Date
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 bde9aade11.
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
Arkadiusz Fal
a194738bb6 Bump build number to 192 2024-08-28 13:40:12 +02:00
Arkadiusz Fal
45567254f2 Update CHANGELOG 2024-08-28 13:40:00 +02:00
Arkadiusz Fal
a3139ad059 Fix swiftformat offense 2024-08-28 13:34:36 +02:00
Arkadiusz Fal
598f17479f Merge pull request #760 from stonerl/hide-video-actions
Hide VideoActions Bar when no buttons is visible
2024-08-28 13:33:43 +02:00
Arkadiusz Fal
e888abfba9 Merge pull request #761 from stonerl/revert-new-appicons
Revert new AppIcons
2024-08-28 13:33:32 +02:00
Toni Förster
e1e53b2d36 Revert new AppIcons
This reverts commit 59da0e71b6.

Revert "Merge pull request #758 from stonerl/new-app-icons-second-try"

This reverts commit 7b26fdf400, reversing
changes made to 67b41e36d5.

Revert "Merge pull request #756 from stonerl/new-app-icons-second-try"

This reverts commit b51eadc7a9, reversing
changes made to 0c1fb02d50.
2024-08-28 12:42:57 +02:00
Toni Förster
9510d91d61 Hide VideoActions Bar when no buttons is visible
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-28 12:32:59 +02:00
Arkadiusz Fal
59da0e71b6 Fix bundle identifier 2024-08-27 22:46:07 +02:00
Arkadiusz Fal
6bdfb7368c Merge pull request #759 from stonerl/videoactions-color
Color changes to VideoActions
2024-08-27 22:26:56 +02:00
Arkadiusz Fal
7b26fdf400 Merge pull request #758 from stonerl/new-app-icons-second-try
change AppStore icon
2024-08-27 22:26:31 +02:00
Arkadiusz Fal
67b41e36d5 Merge pull request #757 from stonerl/zh-hans
Add Chinese (Simplified) - zh-Hans to LanguageCodes
2024-08-27 22:26:17 +02:00
Toni Förster
c9c60349df Color changes to VideoActions
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-27 22:02:00 +02:00
Toni Förster
6a70663f06 add more iOS icons
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-27 17:59:59 +02:00
Toni Förster
3d556d836f change the App Store Icon
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-27 16:26:56 +02:00
Toni Förster
8feeb33a55 distinguish between iOS and macOS AppIcons
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-27 15:45:54 +02:00
Toni Förster
497c3bfc12 Add Chinese (Simplified) - zh-Hans to LanguageCodes
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-27 15:33:06 +02:00
Arkadiusz Fal
b51eadc7a9 Merge pull request #756 from stonerl/new-app-icons-second-try
refreshed icons for iOS and macOS
2024-08-27 14:11:20 +02:00
Toni Förster
7d0c1180c4 refreshed icons for iOS and macOS
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-27 13:31:13 +02:00
Arkadiusz Fal
0c1fb02d50 Merge pull request #755 from yattee/revert-752-new-app-icons
Revert "refreshed icons for iOS and macOS"
2024-08-27 08:21:46 +02:00
Arkadiusz Fal
6f358fab56 Revert "refreshed icons for iOS and macOS" 2024-08-27 08:19:45 +02:00
Arkadiusz Fal
7631e2a8ed Bump build number to 191 2024-08-27 08:00:24 +02:00
Arkadiusz Fal
3a5f3fdfde Update CHANGELOG 2024-08-27 08:00:17 +02:00
Arkadiusz Fal
e3633bdaf7 Merge pull request #752 from stonerl/new-app-icons
refreshed icons for iOS and macOS
2024-08-27 07:56:31 +02:00
Arkadiusz Fal
e912d910bc Merge pull request #754 from stonerl/fix-mpv-crash-on-macos
fix mpv crashing on macOS
2024-08-27 07:56:03 +02:00
Arkadiusz Fal
5ccb0f90d5 Merge pull request #753 from stonerl/use-new-mpvkit-repo
add new MPVKit repo
2024-08-27 07:55:55 +02:00
Toni Förster
278bc343c2 fix mpv crashing on macOS
fixes #712

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-27 04:04:23 +02:00
Toni Förster
5dc197664d add new MPVKit repo
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-26 22:59:24 +02:00
Toni Förster
192550ba7a refreshed icons for iOS and macOS
- added support for adaptive/tinted Icons on iOS.
- closes #700
- new macOS icon set, courtesy of @Kongolabongo (Discourse)

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-26 15:55:36 +02:00
Arkadiusz Fal
3369e23e74 Bump build number to 190 2024-08-26 08:20:57 +02:00
Arkadiusz Fal
4381511c91 Update CHANGELOG 2024-08-26 08:20:51 +02:00
Arkadiusz Fal
af99df9b8a Merge pull request #750 from stonerl/refined-font-sclaing-for-captions
refined chapter font scaling
2024-08-26 08:16:55 +02:00
Arkadiusz Fal
21f21cc944 Merge pull request #749 from stonerl/fix-chapter-regression
fix regression and improve curentChapter handling
2024-08-26 08:16:45 +02:00
Arkadiusz Fal
e1d8bb8125 Merge pull request #748 from stonerl/fix-potential-crashes
fix some potential crashes
2024-08-26 08:16:28 +02:00
Arkadiusz Fal
d948ea6887 Merge pull request #747 from stonerl/fix-endless-loading-of-streams
Improved stream resolution handling
2024-08-26 08:16:19 +02:00
Toni Förster
66eb8051bf refined chapter font scaling
adapted the scaling of chapter fonts after some user feedback

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-26 01:27:09 +02:00
Toni Förster
95d3170d31 fix regression and improve curentChapter handling
with #745 I left som testing changes in the PR that resulted in currentChapter index not being updated. This is fixed now.

Also, the ScrollViewReader waiter 0.5s before jumping to the current Chapter. So it is always drawn correctly.

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-26 00:46:35 +02:00
Toni Förster
74b6adb247 fix some potential crashes
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-25 23:41:06 +02:00
Toni Förster
a45522f710 Improved stream resolution handling
Invidious now reports the actual resolution and doesn’t hardcode them anymore.

See: https://github.com/iv-org/invidious/pull/4586

- Extended the list of possible resolutions in the StreamModel
- trigger videoLoadFailureHandler if no streams are available
- more logging for backend.bestPlayable

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-25 17:23:04 +02:00
Arkadiusz Fal
0b01adf6eb Bump build number to 189 2024-08-24 17:55:30 +02:00
Arkadiusz Fal
444f6bcc03 Update CHANGELOG 2024-08-24 17:55:22 +02:00
Arkadiusz Fal
3f871bce2c Fix possible crashes 2024-08-24 17:52:35 +02:00
Arkadiusz Fal
dc3492fd96 Fix tvOS comments 2024-08-24 14:30:51 +02:00
Arkadiusz Fal
c7365a7dc1 Fix build issues 2024-08-24 14:04:02 +02:00
Arkadiusz Fal
fc9fc194f2 Bump build number to 188 2024-08-24 13:58:59 +02:00
Arkadiusz Fal
317ac63a3f Update CHANGELOG 2024-08-24 13:58:46 +02:00
Arkadiusz Fal
dc7cee7388 Revert "Stay fullscreen when opening notification center"
This reverts commit 08c922f57c.
2024-08-24 13:56:25 +02:00
Arkadiusz Fal
1c5d909201 Revert mpvkit package location upgrade 2024-08-24 13:27:56 +02:00
Arkadiusz Fal
146da6b9cc Merge pull request #746 from stonerl/SwiftyJSON-fixes
add missing SwiftyJSON arguments
2024-08-24 13:20:57 +02:00
Toni Förster
6be13451e0 add missing SwiftyJSON arguments
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-24 13:17:26 +02:00
Arkadiusz Fal
0c41ab6aa2 Update packages 2024-08-24 12:59:38 +02:00
Arkadiusz Fal
1b1e95711a Fix typo 2024-08-24 12:59:38 +02:00
Arkadiusz Fal
13d4a592bc Merge pull request #570 from stonerl/close-fullscreen-on-end
add option to exit fullscreen on end
2024-08-24 12:16:26 +02:00
Arkadiusz Fal
ab493614ba Merge pull request #744 from stonerl/hide-comments
Allow hiding comments
2024-08-24 12:13:44 +02:00
Arkadiusz Fal
0b57187435 Merge pull request #738 from stonerl/chore-linter-warnings
chore: address linter warnings
2024-08-24 12:12:25 +02:00
Arkadiusz Fal
e7928d1016 Merge branch 'main' into chore-linter-warnings 2024-08-24 12:12:12 +02:00
Arkadiusz Fal
b3d73aae92 Merge pull request #735 from stonerl/update-dependencies
update dependencies
2024-08-24 12:11:16 +02:00
Arkadiusz Fal
bafcacb9a1 Merge pull request #724 from weblate/weblate-yattee-localizable-strings
Translations update from Hosted Weblate
2024-08-24 12:10:54 +02:00
maboroshin
931d373d41 Translated using Weblate (Japanese)
Currently translated at 98.5% (554 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ja/
2024-08-24 10:09:45 +00:00
Limonov
5d1620b0a0 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-08-24 10:09:44 +00:00
Limonov
44cfe8e6bc Translated using Weblate (Ukrainian)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/uk/
2024-08-24 10:09:44 +00:00
Limonov
8220bbc3fe Translated using Weblate (Russian)
Currently translated at 98.9% (556 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ru/
2024-08-24 10:09:43 +00:00
Limonov
f01836010c Translated using Weblate (Russian)
Currently translated at 98.5% (554 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ru/
2024-08-24 10:09:43 +00:00
Limonov
37ccb27c8e Translated using Weblate (Russian)
Currently translated at 99.2% (558 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ru/
2024-08-24 10:09:42 +00:00
Kaambiz
93eb2eb258 Translated using Weblate (Persian)
Currently translated at 75.4% (424 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/fa/
2024-08-24 10:09:41 +00:00
Saukki
ce9db8cd12 Translated using Weblate (Finnish)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/fi/
2024-08-24 10:09:41 +00:00
Saukki
d361690d13 Added translation using Weblate (Finnish) 2024-08-24 10:09:40 +00:00
Limonov
ded9699a3a Translated using Weblate (Russian)
Currently translated at 98.9% (556 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ru/
2024-08-24 10:09:40 +00:00
Limonov
101ddf79d5 Translated using Weblate (Russian)
Currently translated at 99.6% (560 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ru/
2024-08-24 10:09:39 +00:00
Limonov
d11c5a8c42 Translated using Weblate (Ukrainian)
Currently translated at 88.7% (499 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/uk/
2024-08-24 10:09:39 +00:00
Limonov
b2a84ef01b 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-08-24 10:09:38 +00:00
Limonov
6c4eb0c840 Translated using Weblate (Russian)
Currently translated at 97.6% (549 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ru/
2024-08-24 10:09:38 +00:00
Arkadiusz Fal
2b41c73d56 Merge pull request #743 from stonerl/controllcenter-swipe
Stay fullscreen when opening notification center
2024-08-24 12:09:28 +02:00
Arkadiusz Fal
516e305cd3 Merge pull request #742 from stonerl/subscription-button
Improvements to opening channels from Videos
2024-08-24 12:09:07 +02:00
Arkadiusz Fal
8921a38504 Merge pull request #741 from stonerl/timecodes-in-comments
iOS: make timestamps in comments touchable
2024-08-24 12:08:27 +02:00
Arkadiusz Fal
61d589a9b5 Merge pull request #740 from stonerl/thumbnails
Improved thumbnail handling
2024-08-24 12:07:56 +02:00
Arkadiusz Fal
55cbd1fe80 Merge pull request #737 from stonerl/xcode16-recommended-settings
Xcode 16 - update recommended settings
2024-08-24 12:06:35 +02:00
Arkadiusz Fal
d501cb938c Merge pull request #745 from stonerl/updateWatch-status
only updateWatch status while video is playing
2024-08-24 12:02:42 +02:00
Toni Förster
8e97d3f42f set playingFullscreen to proper value
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-20 22:56:55 +02:00
Toni Förster
64a18678ce add option to exit fullscreen on end 2024-08-20 22:19:19 +02:00
Toni Förster
522aecfbc1 only updateWatch status while video is playing
This should circumvent edge cases where videos are marked as watch when they failed to play back.

Fixes #660

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-20 22:10:35 +02:00
Toni Förster
f5e509c091 Allow hiding comments 2024-08-20 20:39:21 +02:00
Toni Förster
08c922f57c Stay fullscreen when opening notification center
The upper 5% of the screen ignore swipe down gestures when in fullscreen, to avoid leaving fullscreen when opening the notification center.

fixes #702

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-20 16:14:57 +02:00
Toni Förster
0e8436ab40 VideoDetails: click on channel name opens channel
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-20 14:13:46 +02:00
Toni Förster
f3c876acf6 VideoDetails: open channel when touching the logo
The touch was consumed by the double touch action and the channel did not open.

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-20 14:00:43 +02:00
Toni Förster
70d821fe5d use correct systemImage for the Subscribe button
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-20 13:18:32 +02:00
Toni Förster
9a450c9503 minor tweak to comment replies
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-20 01:23:54 +02:00
Toni Förster
7e346bf49c iOS: make timestamps in comments touchable
Timestamps in comments can now be touched and jump to the corresponding part in the video.

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-20 00:52:04 +02:00
Toni Förster
35534bcbb1 Improved thumbnail handling
- ThumbnailsModel optionally returns the quality
- Have constants for 4:3 and 16:9 aspect ratio
- Add high quality options for thumbnails
- Rename Highest quality to Best quality
- make 4:3 thumbnails fill the VideoCell
- use .maxes instead of .maxresdefault (the latter sometimes returns white images)

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-19 16:35:29 +02:00
Toni Förster
94577332a1 chore: address linter warnings
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-18 14:46:51 +02:00
Toni Förster
56b17b0aa1 Xcode 16 - update recommended settings
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-18 10:49:50 +02:00
Toni Förster
5512337984 update dependencies
MPVKit has a new home.

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-15 02:59:39 +02:00
78 changed files with 2214 additions and 992 deletions

View File

@@ -1,10 +1,13 @@
## Build 187
* Allow import of accounts to manually added (not imported) instances
* Add import export of missing settings
* tvOS: Allow account picker by long pressing channels button in subscriptions view by @patelhiren in https://github.com/yattee/yattee/pull/704
* macOS: Fix settings windows layout
* Fix seek OSD layout on tvOS, revert OSD position
* Translations update from Hosted Weblate by @weblate in https://github.com/yattee/yattee/pull/694
## Build 193
* Invidious: propper HTTP basic auth support by @stonerl in https://github.com/yattee/yattee/pull/762
* Apply correct orientation by @stonerl in https://github.com/yattee/yattee/pull/770
* Circular Invidious logo by @stonerl in https://github.com/yattee/yattee/pull/769
* Video Thumbnails: retry 3 times fetching from URL by @stonerl in https://github.com/yattee/yattee/pull/768
* Make thumbnail fill the view in music mode by @stonerl in https://github.com/yattee/yattee/pull/766
* Changes to defaults by @stonerl in https://github.com/yattee/yattee/pull/767
* Fixed fullscreen handling for backgrounding by @stonerl in https://github.com/yattee/yattee/pull/772
* Update now playing info when using system controls Partial fix for 503 by @stonerl in https://github.com/yattee/yattee/pull/765
* Fix crash on HLS live playback by @stonerl in https://github.com/yattee/yattee/pull/775
## Previous builds
* Add skip, play/pause, and fullscreen shortcuts to macOS player (by @rickykresslein)
@@ -19,6 +22,29 @@
* History Setting: hide the recent activity in the sidebar or limit the number of items shown (by @rickykresslein)
* Fix issues with empty comments (by @stonerl)
* Improved Invidious comments (by @stonerl)
* Allow import of accounts to manually added (not imported) instances
* Add import export of missing settings
* macOS: Fix settings windows layout
* Fix seek OSD layout on tvOS, revert OSD position
* Fix mpv crashing on macOS by @stonerl in https://github.com/yattee/yattee/pull/754
* Refreshed icons for iOS and macOS by @stonerl in https://github.com/yattee/yattee/pull/752
* Add new MPVKit repo by @stonerl in https://github.com/yattee/yattee/pull/753
* Add Chinese (Simplified) - zh-Hans to LanguageCodes by @stonerl in https://github.com/yattee/yattee/pull/757
* Color changes to VideoActions by @stonerl in https://github.com/yattee/yattee/pull/759
* Hide VideoActions Bar when no buttons is visible by @stonerl in https://github.com/yattee/yattee/pull/760
* Improved stream resolution handling by @stonerl in https://github.com/yattee/yattee/pull/747
* Fix some potential crashes by @stonerl in https://github.com/yattee/yattee/pull/748
* Fix regression and improve curentChapter handling by @stonerl in https://github.com/yattee/yattee/pull/749
* Refined chapter font scaling by @stonerl in https://github.com/yattee/yattee/pull/750
* Improved thumbnail handling by @stonerl in https://github.com/yattee/yattee/pull/740
* iOS: make timestamps in comments touchable by @stonerl in https://github.com/yattee/yattee/pull/741
* Improvements to opening channels from Videos by @stonerl in https://github.com/yattee/yattee/pull/742
* Allow hiding comments by @stonerl in https://github.com/yattee/yattee/pull/744
* Add option to exit fullscreen on end by @stonerl in https://github.com/yattee/yattee/pull/570
* Only updateWatch status while video is playing by @stonerl in https://github.com/yattee/yattee/pull/745
* Xcode 16 - update recommended settings by @stonerl in https://github.com/yattee/yattee/pull/737
* Translations update from Hosted Weblate by @weblate in https://github.com/yattee/yattee/pull/724
* tvOS: Allow account picker by long pressing channels button in subscriptions view by @patelhiren in https://github.com/yattee/yattee/pull/704
* tvOS: Refined Subscriptions View by @patelhiren in https://github.com/yattee/yattee/pull/697
* More responsive UI when Favorites are used. by @stonerl in https://github.com/yattee/yattee/pull/695
* Improved conditional proxying by @stonerl in https://github.com/yattee/yattee/pull/696

View File

@@ -6,8 +6,10 @@ extension UIViewController {
}
public class func swizzleHomeIndicatorProperty() {
swizzle(origSelector: #selector(getter: UIViewController.prefersHomeIndicatorAutoHidden),
withSelector: #selector(getter: UIViewController.swizzle_prefersHomeIndicatorAutoHidden),
forClass: UIViewController.self)
swizzle(
origSelector: #selector(getter: UIViewController.prefersHomeIndicatorAutoHidden),
withSelector: #selector(getter: UIViewController.swizzle_prefersHomeIndicatorAutoHidden),
forClass: UIViewController.self
)
}
}

View File

@@ -10,20 +10,20 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.952.0)
aws-sdk-core (3.201.1)
aws-partitions (1.970.0)
aws-sdk-core (3.202.2)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.88.0)
aws-sdk-core (~> 3, >= 3.201.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.156.0)
aws-sdk-s3 (1.159.0)
aws-sdk-core (~> 3, >= 3.201.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.8.0)
aws-sigv4 (1.9.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
@@ -38,7 +38,7 @@ GEM
domain_name (0.6.20240107)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.110.0)
excon (0.111.0)
faraday (1.10.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
@@ -60,7 +60,7 @@ GEM
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
@@ -68,7 +68,7 @@ GEM
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.3.1)
fastlane (2.221.1)
fastlane (2.222.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -126,7 +126,7 @@ 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.0)
google-cloud-core (1.7.1)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
@@ -147,14 +147,14 @@ GEM
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.6)
http-cookie (1.0.7)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.2)
json (2.7.2)
jwt (2.8.2)
base64
mini_magick (4.13.1)
mini_magick (4.13.2)
mini_mime (1.1.5)
multi_json (1.15.0)
multipart-post (2.4.1)
@@ -164,14 +164,14 @@ GEM
optparse (0.5.0)
os (1.1.4)
plist (3.7.1)
public_suffix (6.0.0)
public_suffix (6.0.1)
rake (13.2.1)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.9)
rexml (3.3.6)
strscan
rouge (2.0.7)
ruby2_keywords (0.0.5)
@@ -197,13 +197,13 @@ GEM
uber (0.1.0)
unicode-display_width (2.5.0)
word_wrap (1.0.0)
xcodeproj (1.24.0)
xcodeproj (1.25.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.2.4)
rexml (>= 3.3.2, < 4.0)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)

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

@@ -81,7 +81,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
configureTransformer(pathPattern("search/suggestions"), requestMethods: [.get]) { (content: Entity<JSON>) -> [String] in
if let suggestions = content.json.dictionaryValue["suggestions"] {
return suggestions.arrayValue.map { $0.stringValue.replacingHTMLEntities }
return suggestions.arrayValue.map(\.stringValue).map(\.replacingHTMLEntities)
}
return []
@@ -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
@@ -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)!)
}

View File

@@ -13,6 +13,7 @@ struct ChannelPlaylistsCacheModel: CacheModel {
var storage = try? Storage<String, JSON>(
diskConfig: Self.diskConfig,
memoryConfig: Self.memoryConfig,
fileManager: FileManager.default,
transformer: BaseCacheModel.jsonTransformer
)

View File

@@ -13,6 +13,7 @@ struct ChannelsCacheModel: CacheModel {
let storage = try? Storage<String, JSON>(
diskConfig: Self.diskConfig,
memoryConfig: Self.memoryConfig,
fileManager: FileManager.default,
transformer: BaseCacheModel.jsonTransformer
)

View File

@@ -14,6 +14,7 @@ struct FeedCacheModel: CacheModel {
let storage = try? Storage<String, JSON>(
diskConfig: Self.diskConfig,
memoryConfig: Self.memoryConfig,
fileManager: FileManager.default,
transformer: BaseCacheModel.jsonTransformer
)
@@ -22,7 +23,7 @@ struct FeedCacheModel: CacheModel {
let date = iso8601DateFormatter.string(from: Date())
logger.info("caching feed \(account.feedCacheKey) -- \(date)")
let feedTimeObject: JSON = ["date": date]
let videosObject: JSON = ["videos": videos.prefix(cacheLimit).map { $0.json.object }]
let videosObject: JSON = ["videos": videos.prefix(cacheLimit).map(\.json.object)]
try? storage?.setObject(feedTimeObject, forKey: feedTimeCacheKey(account.feedCacheKey))
try? storage?.setObject(videosObject, forKey: account.feedCacheKey)
}

View File

@@ -14,6 +14,7 @@ struct PlaylistsCacheModel: CacheModel {
let storage = try? Storage<String, JSON>(
diskConfig: Self.diskConfig,
memoryConfig: Self.memoryConfig,
fileManager: FileManager.default,
transformer: BaseCacheModel.jsonTransformer
)
@@ -21,7 +22,7 @@ struct PlaylistsCacheModel: CacheModel {
let date = iso8601DateFormatter.string(from: Date())
logger.info("caching \(playlistCacheKey(account)) -- \(date)")
let feedTimeObject: JSON = ["date": date]
let playlistsObject: JSON = ["playlists": playlists.map { $0.json.object }]
let playlistsObject: JSON = ["playlists": playlists.map(\.json.object)]
try? storage?.setObject(feedTimeObject, forKey: playlistTimeCacheKey(account))
try? storage?.setObject(playlistsObject, forKey: playlistCacheKey(account))
}

View File

@@ -15,6 +15,7 @@ final class SubscribedChannelsModel: ObservableObject, CacheModel {
let storage = try? Storage<String, JSON>(
diskConfig: SubscribedChannelsModel.diskConfig,
memoryConfig: SubscribedChannelsModel.memoryConfig,
fileManager: FileManager.default,
transformer: BaseCacheModel.jsonTransformer
)

View File

@@ -13,6 +13,7 @@ struct VideosCacheModel: CacheModel {
let storage = try? Storage<String, JSON>(
diskConfig: Self.diskConfig,
memoryConfig: Self.memoryConfig,
fileManager: FileManager.default,
transformer: BaseCacheModel.jsonTransformer
)

View File

@@ -152,7 +152,7 @@ struct Channel: Identifiable, Hashable {
"subscriptionsText": subscriptionsText as Any,
"totalViews": totalViews as Any,
"verified": verified as Any,
"videos": videos.map { $0.json.object }
"videos": videos.map(\.json.object)
]
}

View File

@@ -19,7 +19,7 @@ struct ChannelPlaylist: Identifiable {
"title": title,
"thumbnailURL": thumbnailURL?.absoluteString ?? "",
"channel": channel?.json.object ?? "",
"videos": videos.map { $0.json.object },
"videos": videos.map(\.json.object),
"videosCount": String(videosCount ?? 0)
]
}

View File

@@ -42,7 +42,7 @@ final class CommentsModel: ObservableObject {
.comments(video.videoID, page: page)?
.load()
.onSuccess { [weak self] response in
guard let self = self else { return }
guard let self else { return }
if let commentsPage: CommentsPage = response.typedContent() {
self.all += commentsPage.comments
self.nextPage = commentsPage.nextPage

View File

@@ -274,7 +274,7 @@ extension Country {
private static func filteredCountries(_ predicate: (String) -> Bool) -> [Country] {
Country.allCases
.map { $0.name }
.map(\.name)
.filter(predicate)
.compactMap { string in Country.allCases.first { $0.name == string } }
}

View File

@@ -121,7 +121,7 @@ final class FeedModel: ObservableObject, CacheModel {
backgroundContext.perform { [weak self] in
guard let self else { return }
let watched = self.watchFetchRequestResult(feed, context: self.backgroundContext).filter { $0.finished }
let watched = self.watchFetchRequestResult(feed, context: self.backgroundContext).filter(\.finished)
let unwatched = feed.filter { video in !watched.contains { $0.videoID == video.videoID } }
let unwatchedCount = max(0, feed.count - watched.count)

View File

@@ -47,7 +47,7 @@ extension PlayerModel {
}
func updateWatch(finished: Bool = false, time: CMTime? = nil) {
guard let currentVideo, saveHistory else { return }
guard let currentVideo, saveHistory, isPlaying else { return }
let id = currentVideo.videoID
let time = time ?? backend.currentTime

View File

@@ -7,6 +7,7 @@ final class PlayerSettingsGroupExporter: SettingsGroupExporter {
"playerInstanceID": Defaults[.playerInstanceID] ?? "",
"pauseOnHidingPlayer": Defaults[.pauseOnHidingPlayer],
"closeVideoOnEOF": Defaults[.closeVideoOnEOF],
"exitFullscreenOnEOF": Defaults[.exitFullscreenOnEOF],
"expandVideoDescription": Defaults[.expandVideoDescription],
"collapsedLinesDescription": Defaults[.collapsedLinesDescription],
"showChapters": Defaults[.showChapters],
@@ -36,6 +37,8 @@ final class PlayerSettingsGroupExporter: SettingsGroupExporter {
export["pauseOnEnteringBackground"].bool = Defaults[.pauseOnEnteringBackground]
#endif
export["showComments"].bool = Defaults[.showComments]
#if !os(tvOS)
export["showScrollToTopInComments"].bool = Defaults[.showScrollToTopInComments]
#endif

View File

@@ -17,6 +17,10 @@ struct PlayerSettingsGroupImporter {
Defaults[.closeVideoOnEOF] = closeVideoOnEOF
}
if let exitFullscreenOnEOF = json["exitFullscreenOnEOF"].bool {
Defaults[.exitFullscreenOnEOF] = exitFullscreenOnEOF
}
if let expandVideoDescription = json["expandVideoDescription"].bool {
Defaults[.expandVideoDescription] = expandVideoDescription
}
@@ -83,6 +87,9 @@ struct PlayerSettingsGroupImporter {
}
#endif
if let showComments = json["showComments"].bool {
Defaults[.showComments] = showComments
}
#if !os(tvOS)
if let showScrollToTopInComments = json["showScrollToTopInComments"].bool {
Defaults[.showScrollToTopInComments] = showScrollToTopInComments

View File

@@ -3,7 +3,7 @@ import Foundation
final class MenuModel: ObservableObject {
static let shared = MenuModel()
private var cancellables = [AnyCancellable]()
private var cancellables = Set<AnyCancellable>()
init() {
registerChildModel(AccountsModel.shared)
@@ -12,10 +12,16 @@ final class MenuModel: ObservableObject {
}
func registerChildModel<T: ObservableObject>(_ model: T?) {
guard !model.isNil else {
guard let model else {
return
}
cancellables.append(model!.objectWillChange.sink { [weak self] _ in self?.objectWillChange.send() })
model.objectWillChange
.receive(on: DispatchQueue.main) // Ensure the update occurs on the main thread
.debounce(for: .milliseconds(10), scheduler: DispatchQueue.main) // Debounce to avoid immediate feedback loops
.sink { [weak self] _ in
self?.objectWillChange.send()
}
.store(in: &cancellables)
}
}

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(
@@ -779,7 +799,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

@@ -2,9 +2,9 @@ import AVFAudio
import CoreMedia
import Defaults
import Foundation
import Libmpv
import Logging
import MediaPlayer
import MPVKit
import Repeat
import SwiftUI
@@ -360,8 +360,8 @@ final class MPVBackend: PlayerBackend {
setRate(model.currentRate)
// After the video has ended, hitting play restarts the video from the beginning.
if currentTime?.seconds.formattedAsPlaybackTime() == model.playerTime.duration.seconds.formattedAsPlaybackTime() &&
currentTime!.seconds > 0 && model.playerTime.duration.seconds > 0
if let currentTime, currentTime.seconds.formattedAsPlaybackTime() == model.playerTime.duration.seconds.formattedAsPlaybackTime() &&
currentTime.seconds > 0 && model.playerTime.duration.seconds > 0
{
seek(to: 0, seekType: .loopRestart)
}

View File

@@ -1,8 +1,8 @@
import CoreMedia
import Defaults
import Foundation
import Libmpv
import Logging
import MPVKit
#if !os(macOS)
import Siesta
import UIKit
@@ -99,6 +99,11 @@ final class MPVClient: ObservableObject {
checkError(mpv_set_option_string(mpv, "demuxer-lavf-analyzeduration", "1"))
checkError(mpv_set_option_string(mpv, "demuxer-lavf-probe-info", Defaults[.mpvDemuxerLavfProbeInfo]))
// Disable ytdl, since it causes crashes on macOS.
#if os(macOS)
checkError(mpv_set_option_string(mpv, "ytdl", "no"))
#endif
checkError(mpv_initialize(mpv))
let api = UnsafeMutableRawPointer(mutating: (MPV_RENDER_API_TYPE_OPENGL as NSString).utf8String)

View File

@@ -1,6 +1,7 @@
import CoreMedia
import Defaults
import Foundation
import Logging
#if !os(macOS)
import UIKit
#endif
@@ -75,6 +76,10 @@ protocol PlayerBackend {
}
extension PlayerBackend {
var logger: Logger {
return Logger(label: "stream.yattee.player.backend")
}
func seek(to time: CMTime, seekType: SeekType, completionHandler: ((Bool) -> Void)? = nil) {
model.seek.registerSeek(at: time, type: seekType, restore: currentTime)
seek(to: time, seekType: seekType, completionHandler: completionHandler)
@@ -111,15 +116,22 @@ extension PlayerBackend {
model.prepareCurrentItemForHistory(finished: true)
if model.queue.isEmpty {
if Defaults[.closeVideoOnEOF] {
#if os(tvOS)
#if os(tvOS)
if Defaults[.closeVideoOnEOF] {
if model.activeBackend == .appleAVPlayer {
model.avPlayerBackend.controller?.dismiss(animated: false)
}
#endif
model.resetQueue()
model.hide()
}
model.resetQueue()
model.hide()
}
#else
if Defaults[.closeVideoOnEOF] {
model.resetQueue()
model.hide()
} else if Defaults[.exitFullscreenOnEOF], model.playingFullScreen {
model.exitFullScreen()
}
#endif
} else {
model.advanceToNextItem()
}
@@ -133,55 +145,89 @@ extension PlayerBackend {
}
func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting, formatOrder: [QualityProfile.Format]) -> Stream? {
// filter out non-HLS streams and streams with resolution more than maxResolution
let nonHLSStreams = streams.filter {
$0.kind != .hls && $0.resolution <= maxResolution.value
}
logger.info("Starting bestPlayable function")
logger.info("Total streams received: \(streams.count)")
logger.info("Max resolution allowed: \(String(describing: maxResolution.value))")
logger.info("Format order: \(formatOrder)")
// find max resolution and bitrate from non-HLS streams
// Filter out non-HLS streams and streams with resolution more than maxResolution
let nonHLSStreams = streams.filter {
let isHLS = $0.kind == .hls
// Safely unwrap resolution and maxResolution.value to avoid crashes
let isWithinResolution = ($0.resolution != nil && maxResolution.value != nil) ? $0.resolution! <= maxResolution.value! : false
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
}
logger.info("Non-HLS streams after filtering: \(nonHLSStreams.count)")
// Find max resolution and bitrate from non-HLS streams
let bestResolutionStream = nonHLSStreams.max { $0.resolution < $1.resolution }
let bestBitrateStream = nonHLSStreams.max { $0.bitrate ?? 0 < $1.bitrate ?? 0 }
logger.info("Best resolution stream: \(String(describing: bestResolutionStream?.id)) with resolution: \(String(describing: bestResolutionStream?.resolution))")
logger.info("Best bitrate stream: \(String(describing: bestBitrateStream?.id)) with bitrate: \(String(describing: bestBitrateStream?.bitrate))")
let bestResolution = bestResolutionStream?.resolution ?? maxResolution.value
let bestBitrate = bestBitrateStream?.bitrate ?? bestResolutionStream?.resolution.bitrate ?? maxResolution.value.bitrate
return streams.map { stream in
logger.info("Final best resolution selected: \(String(describing: bestResolution))")
logger.info("Final best bitrate selected: \(bestBitrate)")
let adjustedStreams = streams.map { stream in
if stream.kind == .hls {
logger.info("Adjusting HLS stream ID: \(stream.id)")
stream.resolution = bestResolution
stream.bitrate = bestBitrate
stream.format = .hls
} else if stream.kind == .stream {
logger.info("Adjusting non-HLS stream ID: \(stream.id)")
stream.format = .stream
}
return stream
}
.filter { stream in
stream.resolution <= maxResolution.value
let filteredStreams = adjustedStreams.filter { stream in
// Safely unwrap resolution and maxResolution.value to avoid crashes
let isWithinResolution = (stream.resolution != nil && maxResolution.value != nil) ? stream.resolution! <= maxResolution.value! : false
logger.info("Filtered stream ID: \(stream.id) - Is within max resolution: \(isWithinResolution)")
return isWithinResolution
}
.max { lhs, rhs in
logger.info("Filtered streams count after adjustments: \(filteredStreams.count)")
let bestStream = filteredStreams.max { lhs, rhs in
if lhs.resolution == rhs.resolution {
guard let lhsFormat = QualityProfile.Format(rawValue: lhs.format.rawValue),
let rhsFormat = QualityProfile.Format(rawValue: rhs.format.rawValue)
else {
print("Failed to extract lhsFormat or rhsFormat")
logger.info("Failed to extract lhsFormat or rhsFormat for streams \(lhs.id) and \(rhs.id)")
return false
}
let lhsFormatIndex = formatOrder.firstIndex(of: lhsFormat) ?? Int.max
let rhsFormatIndex = formatOrder.firstIndex(of: rhsFormat) ?? Int.max
logger.info("Comparing formats for streams \(lhs.id) and \(rhs.id) - LHS Format Index: \(lhsFormatIndex), RHS Format Index: \(rhsFormatIndex)")
return lhsFormatIndex > rhsFormatIndex
}
logger.info("Comparing resolutions for streams \(lhs.id) and \(rhs.id) - LHS Resolution: \(String(describing: lhs.resolution)), RHS Resolution: \(String(describing: rhs.resolution))")
return lhs.resolution < rhs.resolution
}
logger.info("Best stream selected: \(String(describing: bestStream?.id)) with resolution: \(String(describing: bestStream?.resolution)) and format: \(String(describing: bestStream?.format))")
return bestStream
}
func updateControls(completionHandler: (() -> Void)? = nil) {
print("updating controls")
logger.info("updating controls")
guard model.presentingPlayer, !model.controls.presentingOverlays else {
print("ignored controls update")
logger.info("ignored controls update")
completionHandler?()
return
}
@@ -189,7 +235,7 @@ extension PlayerBackend {
DispatchQueue.main.async(qos: .userInteractive) {
#if !os(macOS)
guard UIApplication.shared.applicationState != .background else {
print("not performing controls updates in background")
logger.info("not performing controls updates in background")
completionHandler?()
return
}

View File

@@ -56,6 +56,7 @@ final class PlayerModel: ObservableObject {
@Published var presentingPlayer = false { didSet { handlePresentationChange() } }
@Published var activeBackend = PlayerBackendType.mpv
@Published var forceBackendOnPlay: PlayerBackendType?
@Published var wasFullscreen = false
var avPlayerBackend = AVPlayerBackend()
var mpvBackend = MPVBackend()
@@ -90,7 +91,7 @@ final class PlayerModel: ObservableObject {
}}
@Published var aspectRatio = VideoPlayerView.defaultAspectRatio
@Published var stream: Stream?
@Published var currentRate: Double = 1.0 { didSet { handleCurrentRateChange() } }
@Published var currentRate = 1.0 { didSet { handleCurrentRateChange() } }
@Published var qualityProfileSelection: QualityProfile? { didSet { handleQualityProfileChange() } }
@@ -684,7 +685,7 @@ final class PlayerModel: ObservableObject {
}
// First, we need to create an array with supported formats.
let formatOrderPiP: [QualityProfile.Format] = [.hls, .stream, .mp4]
let formatOrderPiP: [QualityProfile.Format] = [.stream, .hls]
guard let video = currentVideo else { return }
guard let stream = avPlayerBackend.bestPlayable(availableStreams, maxResolution: .hd720p30, formatOrder: formatOrderPiP) else { return }
@@ -880,26 +881,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 +927,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)
@@ -979,6 +983,17 @@ final class PlayerModel: ObservableObject {
avPlayerBackend.bindPlayerToLayer()
}
#if os(iOS)
if wasFullscreen {
wasFullscreen = false
DispatchQueue.main.async { [weak self] in
Delay.by(0.3) {
self?.enterFullScreen()
}
}
}
#endif
guard closePiPAndOpenPlayerOnEnteringForeground, playingInPictureInPicture else {
return
}
@@ -993,6 +1008,15 @@ final class PlayerModel: ObservableObject {
} else if !playingInPictureInPicture {
avPlayerBackend.removePlayerFromLayer()
}
#if os(iOS)
guard playingFullScreen else { return }
wasFullscreen = playingFullScreen
DispatchQueue.main.async { [weak self] in
Delay.by(0.3) {
self?.exitFullScreen(showControls: false)
}
}
#endif
}
#endif
@@ -1001,6 +1025,7 @@ final class PlayerModel: ObservableObject {
logger.info("entering fullscreen")
toggleFullscreen(false, showControls: showControls)
self.playingFullScreen = true
}
func exitFullScreen(showControls: Bool = true) {
@@ -1008,6 +1033,7 @@ final class PlayerModel: ObservableObject {
logger.info("exiting fullscreen")
toggleFullscreen(true, showControls: showControls)
self.playingFullScreen = false
}
func updateNowPlayingInfo() {
@@ -1015,18 +1041,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,
@@ -1034,7 +1064,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 {
@@ -1055,7 +1085,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
}
@@ -1238,7 +1268,7 @@ final class PlayerModel: ObservableObject {
}
private func destroyKeyPressMonitor() {
if let keyPressMonitor = keyPressMonitor {
if let keyPressMonitor {
NSEvent.removeMonitor(keyPressMonitor)
}
}

View File

@@ -74,7 +74,7 @@ extension PlayerModel {
preservedTime = currentItem.playbackTime
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
guard let self else { return }
guard let video = item.video else {
return
}
@@ -127,6 +127,7 @@ extension PlayerModel {
var streamByQualityProfile: Stream? {
let profile = qualityProfile ?? .defaultProfile
// First attempt: Filter by both `canPlay` and `isPreferred`
if let streamPreferredForProfile = backend.bestPlayable(
availableStreams.filter { backend.canPlay($0) && profile.isPreferred($0) },
maxResolution: profile.resolution, formatOrder: profile.formats
@@ -134,7 +135,24 @@ extension PlayerModel {
return streamPreferredForProfile
}
return backend.bestPlayable(availableStreams.filter { backend.canPlay($0) }, maxResolution: profile.resolution, formatOrder: profile.formats)
// Fallback: Filter by `canPlay` only
let fallbackStream = backend.bestPlayable(
availableStreams.filter { backend.canPlay($0) },
maxResolution: profile.resolution, formatOrder: profile.formats
)
// If no stream is found, trigger the error handler
guard let finalStream = fallbackStream else {
let error = RequestError(
userMessage: "No supported streams available.",
cause: NSError(domain: "stream.yatte.app", code: -1, userInfo: [NSLocalizedDescriptionKey: "No supported streams available"])
)
videoLoadFailureHandler(error, video: currentVideo)
return nil
}
// Return the found stream
return finalStream
}
func advanceToNextItem() {

View File

@@ -16,10 +16,12 @@ struct ScreenSaverManager {
return false
}
noSleepReturn = IOPMAssertionCreateWithName(kIOPMAssertionTypeNoDisplaySleep as CFString,
IOPMAssertionLevel(kIOPMAssertionLevelOn),
reason as CFString,
&noSleepAssertion)
noSleepReturn = IOPMAssertionCreateWithName(
kIOPMAssertionTypeNoDisplaySleep as CFString,
IOPMAssertionLevel(kIOPMAssertionLevelOn),
reason as CFString,
&noSleepAssertion
)
return noSleepReturn == kIOReturnSuccess
}

View File

@@ -69,7 +69,10 @@ final class PlaylistsModel: ObservableObject {
.onSuccess { resource in
self.error = nil
if let playlists: [Playlist] = resource.typedContent() {
self.playlists = playlists
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.playlists = playlists
}
PlaylistsCacheModel.shared.storePlaylist(account: account, playlists: playlists)
onSuccess()
}

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 {
@@ -100,7 +102,7 @@ struct QualityProfileBridge: Defaults.Bridge {
"name": value.name ?? "",
"backend": value.backend.rawValue,
"resolution": value.resolution.rawValue,
"formats": value.formats.map { $0.rawValue }.joined(separator: Self.formatsSeparator),
"formats": value.formats.map(\.rawValue).joined(separator: Self.formatsSeparator),
"order": value.order.map { String($0) }.joined(separator: Self.formatsSeparator) // New line
]
}

View File

@@ -5,26 +5,153 @@ 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
// 8K UHD (16:9) Resolutions
case hd4320p60
case hd4320p50
case hd4320p48
case hd4320p30
case hd4320p25
case hd4320p24
// 5K (16:9) Resolutions
case hd2560p60
case hd2560p50
case hd2560p48
case hd2560p30
case hd2560p25
case hd2560p24
// 2:1 Aspect Ratio (Univisium) Resolutions
case hd2880p60
case hd2880p50
case hd2880p48
case hd2880p30
case hd2880p25
case hd2880p24
// 16:10 Resolutions
case hd2400p60
case hd2400p50
case hd2400p48
case hd2400p30
case hd2400p25
case hd2400p24
// 16:9 Resolutions
case hd2160p60
case hd2160p50
case hd2160p48
case hd2160p30
case hd2160p25
case hd2160p24
// 16:10 Resolutions
case hd1600p60
case hd1600p50
case hd1600p48
case hd1600p30
case hd1600p25
case hd1600p24
// 16:9 Resolutions
case hd1440p60
case hd1440p50
case hd1440p48
case hd1440p30
case hd1440p25
case hd1440p24
// 16:10 Resolutions
case hd1280p60
case hd1280p50
case hd1280p48
case hd1280p30
case hd1280p25
case hd1280p24
// 16:10 Resolutions
case hd1200p60
case hd1200p50
case hd1200p48
case hd1200p30
case hd1200p25
case hd1200p24
// 16:9 Resolutions
case hd1080p60
case hd1080p50
case hd1080p48
case hd1080p30
case hd1080p25
case hd1080p24
// 16:10 Resolutions
case hd1050p60
case hd1050p50
case hd1050p48
case hd1050p30
case hd1050p25
case hd1050p24
// 16:9 Resolutions
case hd960p60
case hd960p50
case hd960p48
case hd960p30
case hd960p25
case hd960p24
// 16:10 Resolutions
case hd900p60
case hd900p50
case hd900p48
case hd900p30
case hd900p25
case hd900p24
// 16:10 Resolutions
case hd800p60
case hd800p50
case hd800p48
case hd800p30
case hd800p25
case hd800p24
// 16:9 Resolutions
case hd720p60
case hd720p50
case hd720p48
case hd720p30
case hd720p25
case hd720p24
// Standard Definition (SD) Resolutions
case sd854p30
case sd854p25
case sd768p30
case sd768p25
case sd640p30
case sd640p25
case sd480p30
case sd480p25
case sd428p30
case sd428p25
case sd360p30
case sd360p25
case sd320p30
case sd320p25
case sd240p30
case sd240p25
case sd214p30
case sd214p25
case sd144p30
case sd144p25
case sd128p30
case sd128p25
case unknown
var name: String {
@@ -59,22 +186,94 @@ class Stream: Equatable, Hashable, Identifiable {
var bitrate: Int {
switch self {
case .hd2160p60, .hd2160p50, .hd2160p48, .hd2160p30:
// 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
case .hd1440p60, .hd1440p50, .hd1440p48, .hd1440p30:
// 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
case .hd1080p60, .hd1080p50, .hd1080p48, .hd1080p30:
// 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
case .hd720p60, .hd720p50, .hd720p48, .hd720p30:
// 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
case .sd480p30:
// Standard Definition (SD) Resolutions
case .sd854p30, .sd854p25, .sd768p30, .sd768p25, .sd640p30, .sd640p25:
return 4_000_000 // 4 Mbit/s
case .sd360p30:
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 .sd240p30:
case .sd320p30, .sd320p25:
return 1_200_000 // 1.2 Mbit/s
case .sd240p30, .sd240p25:
return 1_000_000 // 1 Mbit/s
case .sd144p30:
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
}

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
}
}
}
}
@@ -20,21 +37,23 @@ final class ThumbnailsModel: ObservableObject {
return unloadable.contains(url)
}
func best(_ video: Video) -> URL? {
func best(_ video: Video) -> (url: URL?, quality: Thumbnail.Quality?) {
for quality in availableQualitites {
let url = video.thumbnailURL(quality: quality)
if !isUnloadable(url) {
return url
return (url, quality)
}
}
return nil
return (nil, nil)
}
private var availableQualitites: [Thumbnail.Quality] {
switch Defaults[.thumbnailsQuality] {
case .highest:
return [.maxresdefault, .medium, .default]
return [.maxres, .high, .medium, .default]
case .high:
return [.high, .medium, .default]
case .medium:
return [.medium, .default]
case .low:

View File

@@ -1,2 +1,12 @@
<?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>
<!-- Generated by Pixelmator Pro 3.6.7 -->
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<g id="Group">
<path id="Path" fill="#f0f0f0" stroke="none" d="M 244.186371 511.752167 C 219.045975 510.71109 195.004303 506.137482 171.587616 497.941071 C 94.144188 470.833344 33.538929 407.477814 10.268302 329.279663 C 0.239193 295.592224 -2.512759 258.122925 2.318441 221.024231 C 7.031626 184.829193 19.597385 150.432068 39.58955 118.998993 C 54.919968 94.894897 76.601517 71.145599 99.579987 53.286163 C 146.440094 16.865601 208.748688 -2.762817 267.733124 0.314728 C 300.60672 2.029694 331.167175 9.238464 360.594604 22.219849 C 371.003937 26.811676 386.029724 34.994751 395.774933 41.379883 C 413.748718 53.155853 424.186218 61.823517 439.575043 77.75174 C 456.410675 95.178497 467.682678 109.774475 478.1875 127.753906 C 487.343475 143.423645 496.096527 163.56778 501.34256 181.042023 C 503.374359 187.809723 506.984924 202.749298 508.564056 210.923828 C 511.600952 226.643677 511.993439 231.662842 511.999939 254.866028 C 512.007507 279.289337 511.412323 287.069458 508.295135 303.353882 C 496.447205 365.24649 463.100311 419.655823 413.19043 458.533966 C 384.211426 481.106567 349.644592 497.493866 313.417664 505.834595 C 292.186981 510.723083 268.424774 512.753723 244.192581 511.750305 Z M 199.601273 407.824738 C 199.600616 407.13028 199.507141 405.112122 199.394073 403.339905 L 199.188583 400.117706 L 193.216202 399.771149 C 188.074692 399.472839 187.123169 399.331085 186.376404 398.752106 C 183.806091 396.759216 184.51181 390.745789 189.233658 374.405304 C 190.33078 370.608765 193.472549 359.471619 196.215607 349.656189 C 198.958557 339.840759 202.82106 326.12854 204.798935 319.183411 C 206.776825 312.238525 210.127289 300.343872 212.2444 292.751038 C 214.361496 285.15802 216.835648 276.394104 217.742447 273.275696 C 218.649307 270.157227 221.881256 258.716736 224.924591 247.853851 C 231.209076 225.419739 235.292999 211.284149 236.285294 208.529846 C 236.943924 206.701843 236.981201 206.664764 237.55249 207.272522 C 237.876221 207.616882 242.438049 216.990021 247.689819 228.101257 C 252.941574 239.212921 264.315857 263.153992 272.964874 281.302307 C 294.797607 327.11499 321.04184 382.317078 327.916321 396.885345 L 333.677551 409.096344 L 348.10614 408.978271 C 356.041901 408.913391 362.859833 408.719421 363.258698 408.547302 C 363.971802 408.238831 363.946777 408.156982 361.515564 402.851898 C 360.158997 399.891571 351.171295 380.953369 341.54248 360.767029 C 279.69873 231.107727 263.778931 197.38205 255.30777 178.09668 C 249.3349 164.497955 246.53923 158.564606 245.509338 157.30484 C 244.455933 156.015533 243.436447 155.901581 242.498398 156.96814 C 240.974991 158.700165 237.284607 170.24234 230.574875 194.259399 C 227.962112 203.611725 222.271103 223.840454 217.928177 239.210693 C 209.49437 269.060883 207.108093 277.513733 199.725769 303.692749 C 197.14035 312.859924 193.631577 325.285278 191.928467 331.303101 C 190.225357 337.321899 186.805634 349.519958 184.329178 358.409424 C 178.862122 378.033875 176.535034 385.964355 174.94397 390.397858 C 172.229355 397.960846 171.676529 398.746796 168.692398 399.28656 C 167.563736 399.490662 165.63089 399.658478 164.39711 399.659515 C 161.603485 399.663513 159.888535 400.138885 159.245316 401.092468 C 158.709564 401.88678 158.528641 407.530029 159.013474 408.322784 C 159.274811 408.750031 162.147385 408.816345 188.66066 409.00708 L 199.603806 409.085815 L 199.602936 407.82312 Z M 246.283508 136.628906 C 251.781326 135.410889 257.030548 130.108551 258.271179 124.519989 C 258.735718 122.427612 258.68457 117.95636 258.17337 115.97229 C 257.092316 111.775818 254.02124 107.673767 250.502441 105.726105 C 245.661484 103.0466 238.49118 103.04895 233.643967 105.732697 C 226.044434 109.939087 223.284454 120.360321 227.562363 128.69577 C 230.991348 135.376801 238.182877 138.424713 246.28302 136.630219 Z"/>
<path id="Circle" fill="#575757" stroke="none" d="M 256 0 C 114.61525 0 0 114.615257 0 256 C 0 397.384735 114.61525 512 256 512 C 397.384735 512 512 397.384735 512 256 C 512 114.615257 397.384735 0 256 0 Z M 256 4 C 395.175446 4 508 116.824524 508 256 C 508 395.175446 395.175446 508 256 508 C 116.824524 508 4 395.175446 4 256 C 4 116.824524 116.824524 4 256 4 Z"/>
</g>
<g id="g1">
<path id="path1" fill="#00b6f0" stroke="#00b6f0" stroke-width="0.297331" d="M 234.067764 106.178009 C 223.288239 112.003052 223.375183 129.030151 234.328568 134.765594 C 241.804688 138.70871 251.367157 136.199432 255.800674 129.209381 C 260.842682 121.41275 258.060883 110.300354 249.976257 106.088379 C 245.54274 103.758362 238.501282 103.758362 234.067764 106.178009 Z"/>
<path id="path2" fill="#575757" stroke="none" d="M 242.34436 157.257843 C 241.282883 158.735199 236.77153 172.585571 233.321655 185.235535 C 230.667953 194.83847 224.387421 217.55304 218.72612 237.405212 C 216.956955 243.776398 213.595551 255.779999 211.207184 264.182556 C 208.907288 272.585114 205.545883 284.588745 203.688263 290.9599 C 201.919098 297.331055 198.557724 309.334686 196.169357 317.737244 C 193.869431 326.139801 190.508026 338.143433 188.650406 344.514587 C 186.881271 350.885742 183.608307 362.52005 181.485321 370.368591 C 176.266296 389.482056 173.258743 397.976929 171.312653 398.992645 C 170.428085 399.546631 168.216629 399.915985 166.359024 399.915985 C 159.901581 399.915985 158.928543 400.654663 159.193924 404.9021 L 159.459305 408.687897 L 179.627701 408.964874 L 199.796112 409.149567 L 199.530731 404.809784 L 199.265381 400.377686 L 192.807953 400.100647 C 186.969711 399.823669 186.262039 399.638977 185.377472 397.607605 C 184.227524 395.022217 185.377472 388.0047 188.650406 376.832092 C 189.800354 373.046326 192.807953 362.427704 195.28476 353.286499 C 197.761581 344.145264 201.122986 332.049255 202.803696 326.509125 C 204.395935 320.876648 207.757339 308.872986 210.322601 299.731781 C 212.799438 290.590576 216.160843 278.494598 217.841522 272.954437 C 219.433777 267.32196 222.795181 255.318329 225.360458 246.177094 C 232.879379 218.753387 236.240784 207.488464 236.948441 206.565094 C 237.390732 206.103394 238.45224 207.58078 239.425278 209.796844 C 240.309845 212.012909 256.40918 246.084747 275.073822 285.419769 C 293.738434 324.754761 314.614532 368.706543 321.337311 383.110901 L 333.632965 409.149567 L 348.493896 409.149567 C 356.632019 409.149567 363.354828 408.780212 363.354828 408.410889 C 363.354828 408.041534 356.72049 393.821838 348.670807 376.832092 C 296.657532 267.598999 262.955078 196.038818 257.293793 182.927185 C 254.728485 177.110016 251.19017 168.984467 249.421021 164.921692 C 245.52887 156.149841 244.290451 154.764771 242.34436 157.257843 Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -380,7 +380,7 @@ struct ChannelVideosView: View {
navigation.sidebarSectionChanged.toggle()
}
} label: {
Label("Subscribe", systemImage: "circle")
Label("Subscribe", systemImage: "star.circle")
.help("Subscribe")
#if os(iOS)
.labelStyle(.automatic)

View File

@@ -4,6 +4,8 @@ import SwiftUI
enum Constants {
static let overlayAnimation = Animation.linear(duration: 0.2)
static let aspectRatio16x9 = 16.0 / 9.0
static let aspectRatio4x3 = 4.0 / 3.0
static var isAppleTV: Bool {
#if os(iOS)
@@ -37,6 +39,30 @@ 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 progressViewScale: Double {
#if os(macOS)
0.4

View File

@@ -20,14 +20,14 @@ extension Defaults.Keys {
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 +41,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 +64,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)
@@ -75,16 +75,18 @@ extension Defaults.Keys {
static let expandVideoDescription = Key<Bool>("expandVideoDescription", default: expandVideoDescriptionDefault)
static let collapsedLinesDescription = Key<Int>("collapsedLinesDescription", default: 5)
static let exitFullscreenOnEOF = Key<Bool>("exitFullscreenOnEOF", default: true)
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)
static let playerSidebar = Key<PlayerSidebarSetting>("playerSidebar", default: .defaultValue)
static let showKeywords = Key<Bool>("showKeywords", default: false)
static let showComments = Key<Bool>("showComments", default: true)
#if !os(tvOS)
static let showScrollToTopInComments = Key<Bool>("showScrollToTopInComments", default: true)
#endif
@@ -92,10 +94,10 @@ extension Defaults.Keys {
#if os(iOS)
static let honorSystemOrientationLock = Key<Bool>("honorSystemOrientationLock", default: true)
static let enterFullscreenInLandscape = Key<Bool>("enterFullscreenInLandscape", default: UIDevice.current.userInterfaceIdiom == .phone)
static let enterFullscreenInLandscape = Key<Bool>("enterFullscreenInLandscape", default: Constants.isIPhone)
static let rotateToLandscapeOnEnterFullScreen = Key<FullScreenRotationSetting>(
"rotateToLandscapeOnEnterFullScreen",
default: UIDevice.current.userInterfaceIdiom == .phone ? .landscapeRight : .disabled
default: Constants.isIPhone ? .landscapeRight : .disabled
)
#endif
@@ -114,14 +116,14 @@ 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 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
@@ -173,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
@@ -462,12 +555,14 @@ enum ButtonLabelStyle: String, CaseIterable, Defaults.Serializable {
}
enum ThumbnailsQuality: String, CaseIterable, Defaults.Serializable {
case highest, medium, low
case highest, high, medium, low
var description: String {
switch self {
case .highest:
return "Highest quality".localized()
return "Best quality".localized()
case .high:
return "High quality".localized()
case .medium:
return "Medium quality".localized()
case .low:
@@ -607,7 +702,7 @@ enum SponsorBlockColors: String {
case music_offtopic = "#FF9900" // Orange
// Define all cases, can be used to iterate over the colors
static let allCases: [SponsorBlockColors] = [.sponsor, .selfpromo, .interaction, .intro, .outro, .preview, .filler, .music_offtopic]
static let allCases: [SponsorBlockColors] = [Self.sponsor, Self.selfpromo, Self.interaction, Self.intro, Self.outro, Self.preview, Self.filler, Self.music_offtopic]
// Create a dictionary with the category names as keys and colors as values
static let dictionary: [String: String] = {

View File

@@ -47,63 +47,115 @@ enum LanguageCodes: String, CaseIterable {
case Vietnamese = "vi"
case Xhosa = "xh"
case Chinese = "zh"
case Chinese_Hans = "zh-Hans"
case Zulu = "zu"
var description: String {
switch self {
case .Afrikaans: return "Afrikaans"
case .Arabic: return "Arabic"
case .Azerbaijani: return "Azerbaijani"
case .Bengali: return "Bengali"
case .Catalan: return "Catalan"
case .Czech: return "Czech"
case .Welsh: return "Welsh"
case .Danish: return "Danish"
case .German: return "German"
case .Greek: return "Greek"
case .English: return "English"
case .English_GB: return "English (United Kingdom)"
case .Spanish: return "Spanish"
case .Persian: return "Persian"
case .Finnish: return "Finnish"
case .Filipino: return "Filipino"
case .French: return "French"
case .Irish: return "Irish"
case .Hebrew: return "Hebrew"
case .Hindi: return "Hindi"
case .Hungarian: return "Hungarian"
case .Indonesian: return "Indonesian"
case .Italian: return "Italian"
case .Japanese: return "Japanese"
case .Javanese: return "Javanese"
case .Korean: return "Korean"
case .Lithuanian: return "Lithuanian"
case .Malay: return "Malay"
case .Maltese: return "Maltese"
case .Dutch: return "Dutch"
case .Norwegian: return "Norwegian"
case .Polish: return "Polish"
case .Portuguese: return "Portuguese"
case .Romanian: return "Romanian"
case .Russian: return "Russian"
case .Slovak: return "Slovak"
case .Slovene: return "Slovene"
case .Swedish: return "Swedish"
case .Swahili: return "Swahili"
case .Thai: return "Thai"
case .Tagalog: return "Tagalog"
case .Turkish: return "Turkish"
case .Ukrainian: return "Ukrainian"
case .Urdu: return "Urdu"
case .Uzbek: return "Uzbek"
case .Vietnamese: return "Vietnamese"
case .Xhosa: return "Xhosa"
case .Chinese: return "Chinese"
case .Zulu: return "Zulu"
case .Afrikaans:
return "Afrikaans"
case .Arabic:
return "Arabic"
case .Azerbaijani:
return "Azerbaijani"
case .Bengali:
return "Bengali"
case .Catalan:
return "Catalan"
case .Czech:
return "Czech"
case .Welsh:
return "Welsh"
case .Danish:
return "Danish"
case .German:
return "German"
case .Greek:
return "Greek"
case .English:
return "English"
case .English_GB:
return "English (United Kingdom)"
case .Spanish:
return "Spanish"
case .Persian:
return "Persian"
case .Finnish:
return "Finnish"
case .Filipino:
return "Filipino"
case .French:
return "French"
case .Irish:
return "Irish"
case .Hebrew:
return "Hebrew"
case .Hindi:
return "Hindi"
case .Hungarian:
return "Hungarian"
case .Indonesian:
return "Indonesian"
case .Italian:
return "Italian"
case .Japanese:
return "Japanese"
case .Javanese:
return "Javanese"
case .Korean:
return "Korean"
case .Lithuanian:
return "Lithuanian"
case .Malay:
return "Malay"
case .Maltese:
return "Maltese"
case .Dutch:
return "Dutch"
case .Norwegian:
return "Norwegian"
case .Polish:
return "Polish"
case .Portuguese:
return "Portuguese"
case .Romanian:
return "Romanian"
case .Russian:
return "Russian"
case .Slovak:
return "Slovak"
case .Slovene:
return "Slovene"
case .Swedish:
return "Swedish"
case .Swahili:
return "Swahili"
case .Thai:
return "Thai"
case .Tagalog:
return "Tagalog"
case .Turkish:
return "Turkish"
case .Ukrainian:
return "Ukrainian"
case .Urdu:
return "Urdu"
case .Uzbek:
return "Uzbek"
case .Vietnamese:
return "Vietnamese"
case .Xhosa:
return "Xhosa"
case .Chinese:
return "Chinese"
case .Chinese_Hans:
return "Chinese (Simplified)"
case .Zulu:
return "Zulu"
}
}
static func languageName(for code: String) -> String {
return LanguageCodes(rawValue: code)?.description ?? "Unknown"
return Self(rawValue: code)?.description ?? "Unknown"
}
}

View File

@@ -209,7 +209,7 @@ struct OpenURLHandler {
return accounts.api.channelByName(name)
}
if let instance = InstancesModel.shared.all.first(where: { $0.app.supportsOpeningChannelsByName }) {
if let instance = InstancesModel.shared.all.first(where: \.app.supportsOpeningChannelsByName) {
return instance.anonymous.channelByName(name)
}
@@ -223,7 +223,7 @@ struct OpenURLHandler {
return accounts.api.channelByUsername(username)
}
if let instance = InstancesModel.shared.all.first(where: { $0.app.supportsOpeningChannelsByName }) {
if let instance = InstancesModel.shared.all.first(where: \.app.supportsOpeningChannelsByName) {
return instance.anonymous.channelByUsername(username)
}

View File

@@ -61,7 +61,8 @@ final class AppleAVPlayerViewController: UIViewController {
_ sections: [NowPlayingView.ViewSection],
title: String
) -> UIHostingController<AnyView> {
let controller = UIHostingController(rootView:
let controller = UIHostingController(
rootView:
AnyView(
NowPlayingView(sections: sections, inInfoViewController: true)
.frame(maxHeight: 600)

View File

@@ -248,31 +248,33 @@ 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
}
}
}
}
var controlsBackgroundURL: URL? {
if let video = player.videoForDisplay,
let url = thumbnails.best(video)
{
return url
}
return nil
}
var timeline: some View {
TimelineView(context: .player).foregroundColor(.primary)
}

View File

@@ -33,7 +33,7 @@ struct TimelineView: View {
@State private var dragOffset: Double = 0
@State private var draggedFrom: Double = 0
private var start: Double = 0.0
private var start = 0.0
private var height = 8.0
var cornerRadius: Double

View File

@@ -1,6 +1,6 @@
import GLKit
import Libmpv
import Logging
import MPVKit
import OpenGLES
final class MPVOGLView: GLKView {

View File

@@ -1,210 +0,0 @@
import AVKit
import Defaults
import SwiftUI
final class PlayerViewController: UIViewController {
var playerLoaded = false
var commentsModel: CommentsModel!
var navigationModel: NavigationModel!
var playerModel: PlayerModel!
var subscriptionsModel: SubscriptionsModel!
var playerView = AVPlayerViewController()
let persistenceController = PersistenceController.shared
#if !os(tvOS)
var aspectRatio: Double? {
let ratio = Double(playerView.videoBounds.width) / Double(playerView.videoBounds.height)
guard ratio.isFinite else {
return VideoPlayerView.defaultAspectRatio // swiftlint:disable:this implicit_return
}
return [ratio, 1.0].max()!
}
#endif
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadPlayer()
#if os(tvOS)
if !playerView.isBeingPresented, !playerView.isBeingDismissed {
present(playerView, animated: false)
}
#endif
}
#if os(tvOS)
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if !playerModel.presentingPlayer, !Defaults[.pauseOnHidingPlayer], !playerModel.isPlaying {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
self?.playerModel.play()
}
}
}
#endif
func loadPlayer() {
guard !playerLoaded else {
return
}
playerModel.controller = self
playerView.player = playerModel.player
playerView.allowsPictureInPicturePlayback = true
#if os(iOS)
if #available(iOS 14.2, *) {
playerView.canStartPictureInPictureAutomaticallyFromInline = true
}
#endif
playerView.delegate = self
#if os(tvOS)
var infoViewControllers = [UIHostingController<AnyView>]()
if CommentsModel.enabled {
infoViewControllers.append(infoViewController([.comments], title: "Comments"))
}
var queueSections = [NowPlayingView.ViewSection.playingNext]
if Defaults[.showHistoryInPlayer] {
queueSections.append(.playedPreviously)
}
infoViewControllers.append(contentsOf: [
infoViewController([.related], title: "Related"),
infoViewController(queueSections, title: "Queue")
])
playerView.customInfoViewControllers = infoViewControllers
#else
embedViewController()
#endif
}
#if os(tvOS)
func infoViewController(
_ sections: [NowPlayingView.ViewSection],
title: String
) -> UIHostingController<AnyView> {
let controller = UIHostingController(rootView:
AnyView(
NowPlayingView(sections: sections, inInfoViewController: true)
.frame(maxHeight: 600)
.environmentObject(commentsModel)
.environmentObject(playerModel)
.environmentObject(subscriptionsModel)
.environment(\.managedObjectContext, persistenceController.container.viewContext)
)
)
controller.title = title
return controller
}
#else
func embedViewController() {
playerView.view.frame = view.bounds
addChild(playerView)
view.addSubview(playerView.view)
playerView.didMove(toParent: self)
}
#endif
}
extension PlayerViewController: AVPlayerViewControllerDelegate {
func playerViewControllerShouldDismiss(_: AVPlayerViewController) -> Bool {
true
}
func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_: AVPlayerViewController) -> Bool {
true
}
func playerViewControllerWillBeginDismissalTransition(_: AVPlayerViewController) {
if Defaults[.pauseOnHidingPlayer] {
playerModel.pause()
}
dismiss(animated: false)
}
func playerViewControllerDidEndDismissalTransition(_: AVPlayerViewController) {}
func playerViewController(
_: AVPlayerViewController,
willBeginFullScreenPresentationWithAnimationCoordinator context: UIViewControllerTransitionCoordinator
) {
playerModel.playingFullscreen = true
#if os(iOS)
if !context.isCancelled, Defaults[.lockLandscapeWhenEnteringFullscreen] {
Orientation.lockOrientation(.landscape, andRotateTo: UIDevice.current.orientation.isLandscape ? nil : .landscapeRight)
}
#endif
}
func playerViewController(
_: AVPlayerViewController,
willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator
) {
let wasPlaying = playerModel.isPlaying
coordinator.animate(alongsideTransition: nil) { context in
#if os(iOS)
if wasPlaying {
self.playerModel.play()
}
#endif
if !context.isCancelled {
#if os(iOS)
self.playerModel.lockedOrientation = nil
if Defaults[.enterFullscreenInLandscape] {
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
}
self.playerModel.playingFullscreen = false
if wasPlaying {
self.playerModel.play()
}
#endif
}
}
}
func playerViewController(
_: AVPlayerViewController,
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if self.navigationModel.presentingChannel {
self.playerModel.playerNavigationLinkActive = true
} else {
self.playerModel.show()
}
#if os(tvOS)
if self.playerModel.playingInPictureInPicture {
self.present(self.playerView, animated: false) {
completionHandler(true)
}
}
#else
completionHandler(true)
#endif
}
}
func playerViewControllerWillStartPictureInPicture(_: AVPlayerViewController) {
playerModel.playingInPictureInPicture = true
playerModel.playerNavigationLinkActive = false
}
func playerViewControllerWillStopPictureInPicture(_: AVPlayerViewController) {
playerModel.playingInPictureInPicture = false
}
}

View File

@@ -65,7 +65,7 @@ import SwiftUI
}
static var thumbnailHeight: Double {
thumbnailWidth / (16 / 9)
thumbnailWidth / Constants.aspectRatio16x9
}
}
@@ -119,7 +119,7 @@ import SwiftUI
}
static var thumbnailHeight: Double {
thumbnailWidth / 1.7777
thumbnailWidth / Constants.aspectRatio16x9
}
}
#endif

View File

@@ -29,18 +29,14 @@ struct ChaptersView: View {
ScrollView(.horizontal) {
ScrollViewReader { scrollViewProxy in
LazyHStack(spacing: 20) {
chapterViews(for: chapters[...], scrollViewProxy: scrollViewProxy)
chapterViews(for: chapters[...])
}
.padding(.horizontal, 15)
.onAppear {
if let currentChapterIndex = player.currentChapterIndex {
scrollViewProxy.scrollTo(currentChapterIndex, anchor: .center)
}
scrollToCurrentChapter(scrollViewProxy)
}
.onChange(of: player.currentChapterIndex) { currentChapterIndex in
if let index = currentChapterIndex {
scrollViewProxy.scrollTo(index, anchor: .center)
}
.onChange(of: player.currentChapterIndex) { _ in
scrollToCurrentChapter(scrollViewProxy)
}
}
}
@@ -53,7 +49,8 @@ struct ChaptersView: View {
}
}
#else
Section { chapterViews(for: chapters[...]) }.padding(.horizontal)
Section { chapterViews(for: chapters[...]) }
.padding(.horizontal)
#endif
} else {
#if os(iOS)
@@ -80,7 +77,7 @@ struct ChaptersView: View {
}
#if !os(tvOS)
private func chapterViews(for chaptersToShow: ArraySlice<Chapter>, opacity: Double = 1.0, clickable: Bool = true, scrollViewProxy _: ScrollViewProxy? = nil) -> some View {
private func chapterViews(for chaptersToShow: ArraySlice<Chapter>, opacity: Double = 1.0, clickable: Bool = true) -> some View {
ForEach(Array(chaptersToShow.indices), id: \.self) { index in
let chapter = chaptersToShow[index]
ChapterView(chapter: chapter, chapterIndex: index, showThumbnail: showThumbnails)
@@ -89,6 +86,14 @@ struct ChaptersView: View {
.allowsHitTesting(clickable)
}
}
private func scrollToCurrentChapter(_ scrollViewProxy: ScrollViewProxy) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { // Slight delay to ensure the view is fully rendered
if let currentChapterIndex = player.currentChapterIndex {
scrollViewProxy.scrollTo(currentChapterIndex, anchor: .center)
}
}
}
#endif
}

View File

@@ -1,9 +1,13 @@
#if os(iOS)
import ActiveLabel
#endif
import SDWebImageSwiftUI
import SwiftUI
struct CommentView: View {
let comment: Comment
@Binding var repliesID: Comment.ID?
var availableWidth: CGFloat
@State private var subscribed = false
@@ -127,7 +131,9 @@ struct CommentView: View {
Text(comment.time)
.font(.caption2)
#if !os(tvOS)
.foregroundColor(.secondary)
#endif
}
.lineLimit(1)
}
@@ -155,11 +161,11 @@ struct CommentView: View {
Text("\(comment.likeCount.formattedAsAbbreviation())")
}
#if !os(tvOS)
.foregroundColor(.secondary)
.font(.system(size: 12))
#endif
}
}
.foregroundColor(.secondary)
}
private var repliesButton: some View {
@@ -177,7 +183,8 @@ struct CommentView: View {
Text("Replies")
}
#if os(tvOS)
.padding(10)
.font(.system(size: 26))
.padding(.vertical, 3)
#endif
}
.buttonStyle(.plain)
@@ -204,7 +211,7 @@ struct CommentView: View {
Group {
let last = comments.replies.last
ForEach(comments.replies) { comment in
Self(comment: comment, repliesID: $repliesID)
Self(comment: comment, repliesID: $repliesID, availableWidth: availableWidth - 22)
#if os(tvOS)
.focusable()
#endif
@@ -220,22 +227,27 @@ struct CommentView: View {
private var commentText: some View {
Group {
let text = Text(comment.text)
#if os(macOS)
.font(.system(size: 14))
#elseif os(iOS)
.font(.system(size: 15))
#endif
.lineSpacing(3)
.fixedSize(horizontal: false, vertical: true)
let rawText = comment.text
if #available(iOS 15.0, macOS 12.0, *) {
text
#if !os(tvOS)
.textSelection(.enabled)
#if os(iOS)
ActiveLabelCommentRepresentable(
text: rawText,
availableWidth: availableWidth
)
#elseif os(macOS)
Text(rawText)
.font(.system(size: 14))
.lineSpacing(3)
.fixedSize(horizontal: false, vertical: true)
.textSelection(.enabled)
#else
Text(comment.text)
#endif
} else {
text
Text(rawText)
.font(.system(size: 15))
.lineSpacing(3)
.fixedSize(horizontal: false, vertical: true)
}
}
}
@@ -248,13 +260,78 @@ struct CommentView: View {
}
}
#if os(iOS)
struct ActiveLabelCommentRepresentable: UIViewRepresentable {
var text: String
var availableWidth: CGFloat
@State private var label = ActiveLabel()
@Environment(\.openURL) private var openURL
var player = PlayerModel.shared
func makeUIView(context _: Context) -> some UIView {
customizeLabel()
return label
}
func updateUIView(_: UIViewType, context _: Context) {
label.preferredMaxLayoutWidth = availableWidth
}
func customizeLabel() {
label.customize { label in
label.enabledTypes = [.url, .timestamp]
label.text = text
label.font = .systemFont(ofSize: 15)
label.lineSpacing = 3
label.preferredMaxLayoutWidth = availableWidth
label.URLColor = UIColor(Color.accentColor)
label.timestampColor = UIColor(Color.accentColor)
label.handleURLTap(urlTapHandler(_:))
label.handleTimestampTap(timestampTapHandler(_:))
label.numberOfLines = 0
}
}
private func urlTapHandler(_ url: URL) {
var urlToOpen = url
if var components = URLComponents(url: url, resolvingAgainstBaseURL: false) {
components.scheme = "yattee"
if let yatteeURL = components.url {
let parser = URLParser(url: urlToOpen, allowFileURLs: false)
let destination = parser.destination
if destination == .video,
parser.videoID == player.currentVideo?.videoID,
let time = parser.time
{
player.backend.seek(to: Double(time), seekType: .userInteracted)
return
}
if destination != nil {
urlToOpen = yatteeURL
}
}
}
openURL(urlToOpen)
}
private func timestampTapHandler(_ timestamp: Timestamp) {
player.backend.seek(to: timestamp.timeInterval, seekType: .userInteracted)
}
}
#endif
struct CommentView_Previews: PreviewProvider {
static var fixture: Comment {
Comment.fixture
}
static var previews: some View {
CommentView(comment: fixture, repliesID: .constant(fixture.id))
CommentView(comment: fixture, repliesID: .constant(fixture.id), availableWidth: 375)
.padding(5)
}
}

View File

@@ -2,6 +2,7 @@ import SwiftUI
struct CommentsView: View {
@State private var repliesID: Comment.ID?
@State private var availableWidth = 0.0
@ObservedObject private var comments = CommentsModel.shared
@@ -14,16 +15,21 @@ struct CommentsView: View {
} else if !comments.loaded {
PlaceholderProgressView()
} else {
let last = comments.all.last
LazyVStack {
ForEach(comments.all) { comment in
CommentView(comment: comment, repliesID: $repliesID)
CommentView(comment: comment, repliesID: $repliesID, availableWidth: availableWidth)
.onAppear {
comments.loadNextPageIfNeeded(current: comment)
}
.borderBottom(height: comment != last ? 0.5 : 0, color: Color("ControlsBorderColor"))
.borderBottom(height: comment != comments.all.last ? 0.5 : 0, color: Color("ControlsBorderColor"))
}
}
.background(GeometryReader { geometry in
Color.clear
.onAppear {
self.availableWidth = Double(geometry.size.width)
}
})
}
}
.padding(.horizontal)

View File

@@ -86,6 +86,10 @@ struct VideoActions: View {
}
}
func isAnyActionVisible() -> Bool {
return Action.allCases.contains { isVisible($0) }
}
func isActionable(_ action: Action) -> Bool {
switch action {
case .share:
@@ -198,10 +202,10 @@ struct VideoActions: View {
VStack(spacing: 3) {
Image(systemName: systemImage)
.frame(width: 20, height: 20)
.foregroundColor(active ? Color("AppRedColor") : .accentColor)
.foregroundColor(active ? Color("AppRedColor") : .primary)
if playerActionsButtonLabelStyle.text {
Text(name.localized())
.foregroundColor(active ? Color("AppRedColor") : .secondary)
.foregroundColor(active ? Color("AppRedColor") : .primary)
.font(.caption2)
.allowsTightening(true)
.lineLimit(1)

View File

@@ -47,6 +47,12 @@ struct VideoDetails: View {
.frame(width: 40, height: 40)
.buttonStyle(.plain)
.padding(.trailing, 5)
// TODO: when setting tvOS minimum to 16, the platform modifier can be removed
#if !os(tvOS)
.simultaneousGesture(
TapGesture() // Ensures the button tap is recognized
)
#endif
VStack(alignment: .leading, spacing: 2) {
HStack {
@@ -55,6 +61,14 @@ struct VideoDetails: View {
.font(.subheadline)
.fontWeight(.semibold)
.lineLimit(1)
// TODO: when setting tvOS minimum to 16, the platform modifier can be removed
#if !os(tvOS)
.onTapGesture {
guard let channel = video?.channel else { return }
NavigationModel.shared.openChannel(channel, navigationStyle: .sidebar)
}
.accessibilityAddTraits(.isButton)
#endif
} else if model.videoBeingOpened != nil {
Text("Yattee")
.font(.subheadline)
@@ -189,6 +203,7 @@ struct VideoDetails: View {
@Default(.showChapterThumbnails) private var showChapterThumbnails
@Default(.showChapterThumbnailsOnlyWhenDifferent) private var showChapterThumbnailsOnlyWhenDifferent
@Default(.showRelated) private var showRelated
@Default(.showComments) private var showComments
#if !os(tvOS)
@Default(.showScrollToTopInComments) private var showScrollToTopInComments
#endif
@@ -208,25 +223,34 @@ 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)
.tapRecognizer(
tapSensitivity: 0.2,
doubleTapAction: {
.simultaneousGesture( // Simultaneous gesture to prioritize button tap
TapGesture(count: 2).onEnded {
withAnimation(.default) {
fullScreen.toggle()
}
}
)
#endif
VideoActions(video: player.videoForDisplay)
.padding(.vertical, 5)
.frame(maxHeight: 50)
.frame(maxWidth: .infinity)
.borderTop(height: 0.5, color: Color("ControlsBorderColor"))
.borderBottom(height: 0.5, color: Color("ControlsBorderColor"))
.animation(nil, value: player.currentItem)
.frame(minWidth: 0, maxWidth: .infinity)
// swiftlint:enable trailing_closure
if VideoActions().isAnyActionVisible() {
VideoActions(video: player.videoForDisplay)
.padding(.vertical, 5)
.frame(maxHeight: 50)
.frame(maxWidth: .infinity)
.borderTop(height: 0.5, color: Color("ControlsBorderColor"))
.borderBottom(height: 0.5, color: Color("ControlsBorderColor"))
.animation(nil, value: player.currentItem)
.frame(minWidth: 0, maxWidth: .infinity)
} else {
Rectangle()
.fill(Color.clear)
.frame(height: 0.5)
.frame(maxWidth: .infinity)
.background(Color("ControlsBorderColor"))
}
ScrollViewReader { proxy in
pageView
@@ -284,6 +308,8 @@ struct VideoDetails: View {
switch page {
case .queue:
return !sidebarQueue && player.isAdvanceToNextItemAvailable
case .comments:
return showComments
default:
return !video.isLocal
}
@@ -362,10 +388,12 @@ struct VideoDetails: View {
PlayerQueueView(sidebarQueue: false)
.padding(.horizontal)
case .comments:
CommentsView()
.onAppear {
comments.loadIfNeeded()
}
if showComments {
CommentsView()
.onAppear {
comments.loadIfNeeded()
}
}
}
}
.padding(.bottom, 60)

View File

@@ -19,7 +19,7 @@ struct VideoPlayerView: View {
static let hiddenOffset = 0.0
#endif
static let defaultAspectRatio = 16 / 9.0
static let defaultAspectRatio = Constants.aspectRatio16x9
static var defaultMinimumHeightLeft: Double {
#if os(macOS)
335
@@ -281,7 +281,7 @@ struct VideoPlayerView: View {
}
.gesture(player.controls.presentingOverlays ? nil : playerDragGesture)
#if os(macOS)
.onAppear(perform: {
.onAppear {
NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) {
hoverThrottle.execute {
if !player.currentItem.isNil, hoveringPlayer {
@@ -291,7 +291,7 @@ struct VideoPlayerView: View {
return $0
}
})
}
#endif
.background(Color.black)

View File

@@ -15,7 +15,7 @@ struct SearchTextField: View {
#if os(macOS)
Image(systemName: "magnifyingglass")
.resizable()
.aspectRatio(contentMode: .fill)
.scaledToFill()
.frame(width: 12, height: 12)
.padding(.horizontal, 8)
.opacity(0.8)

View File

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

View File

@@ -8,9 +8,11 @@ struct PlayerSettings: View {
@Default(.playerSidebar) private var playerSidebar
@Default(.showKeywords) private var showKeywords
@Default(.showComments) private var showComments
#if !os(tvOS)
@Default(.showScrollToTopInComments) private var showScrollToTopInComments
@Default(.collapsedLinesDescription) private var collapsedLinesDescription
@Default(.exitFullscreenOnEOF) private var exitFullscreenOnEOF
#endif
@Default(.expandVideoDescription) private var expandVideoDescription
@Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer
@@ -85,6 +87,9 @@ struct PlayerSettings: View {
}
pauseOnHidingPlayerToggle
closeVideoOnEOFToggle
#if !os(tvOS)
exitFullscreenOnEOFToggle
#endif
#if !os(macOS)
pauseOnEnteringBackgroundToogle
#endif
@@ -175,6 +180,7 @@ struct PlayerSettings: View {
if !accounts.isEmpty {
keywordsToggle
commentsToggle
#if !os(tvOS)
showScrollToTopInCommentsToggle
#endif
@@ -250,9 +256,13 @@ struct PlayerSettings: View {
.modifier(SettingsPickerModifier())
}
private var commentsToggle: some View {
Toggle("Show comments", isOn: $showComments)
}
#if !os(tvOS)
private var showScrollToTopInCommentsToggle: some View {
Toggle("Show scroll to top button in comments", isOn: $showScrollToTopInComments)
Toggle("Show scroll to top button in comments", isOn: $showScrollToTopInComments).disabled(!showComments)
}
#endif
@@ -294,6 +304,13 @@ struct PlayerSettings: View {
Toggle("Close video and player on end", isOn: $closeVideoOnEOF)
}
#if !os(tvOS)
private var exitFullscreenOnEOFToggle: some View {
Toggle("Exit fullscreen on end", isOn: $exitFullscreenOnEOF)
.disabled(closeVideoOnEOF)
}
#endif
#if !os(macOS)
private var pauseOnEnteringBackgroundToogle: some View {
Toggle("Pause when entering background", isOn: $pauseOnEnteringBackground)
@@ -312,8 +329,8 @@ struct PlayerSettings: View {
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("Landscape left").tag(FullScreenRotationSetting.landscapeLeft)
Text("Landscape right").tag(FullScreenRotationSetting.landscapeRight)
Text("No rotation").tag(FullScreenRotationSetting.disabled)
}
.modifier(SettingsPickerModifier())
@@ -344,9 +361,9 @@ struct PlayerSettings: View {
private var captionsFontScaleSizePicker: some View {
Picker("Size", selection: $captionsFontScaleSize) {
Text("Small").tag(String("0.5"))
Text("Small").tag(String("0.725"))
Text("Medium").tag(String("1.0"))
Text("Large").tag(String("2.0"))
Text("Large").tag(String("1.5"))
}
.onChange(of: captionsFontScaleSize) { _ in
PlayerModel.shared.mpvBackend.client.setSubFontSize(scaleSize: captionsFontScaleSize)

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)
}
@@ -380,7 +380,7 @@ struct QualityProfileForm: View {
func submitForm() {
guard valid else { return }
let activeFormats = orderedFormats.filter { $0.isActive }.map { $0.format }
let activeFormats = orderedFormats.filter(\.isActive).map(\.format)
let formProfile = QualityProfile(
id: qualityProfile?.id ?? UUID().uuidString,

View File

@@ -15,7 +15,7 @@ struct FeedView: View {
#endif
var videos: [ContentItem] {
guard let selectedChannel = selectedChannel else {
guard let selectedChannel else {
return ContentItem.array(of: feed.videos)
}
return ContentItem.array(of: feed.videos.filter {
@@ -24,9 +24,7 @@ struct FeedView: View {
}
var channels: [Channel] {
feed.videos.map {
$0.channel
}.unique()
feed.videos.map(\.channel).unique()
}
@State private var selectedChannel: Channel?
@@ -272,7 +270,7 @@ struct FeedView: View {
}
var channelHeaderView: some View {
guard let selectedChannel = selectedChannel else {
guard let selectedChannel else {
return AnyView(
Text("All Channels")
.font(.caption)

View File

@@ -6,7 +6,7 @@ struct TrendingCountry: View {
@StateObject private var store = Store(Country.allCases)
@State private var query: String = ""
@State private var query = ""
@State private var selection: Country?
@Environment(\.colorScheme) private var colorScheme

View File

@@ -26,8 +26,7 @@ struct URLParser {
urlString.contains("youtube.com") ||
urlString.contains("youtu.be") ||
urlString.contains("youtube-nocookie.com"),
let url = URL(string: "https://\(urlString)"
)
let url = URL(string: "https://\(urlString)")
{
self.url = url
}
@@ -176,10 +175,8 @@ struct URLParser {
private func removePrefixes(_ value: String, _ prefixes: [String]) -> String {
var value = value
for prefix in prefixes {
if value.hasPrefix(prefix) {
value.removeFirst(prefix.count)
}
for prefix in prefixes where value.hasPrefix(prefix) {
value.removeFirst(prefix.count)
}
return value

View File

@@ -219,22 +219,18 @@ struct VideoBanner: View {
return watch!.finished ? 0.5 : 1
}
private var thumbnailWidth: Double {
#if os(tvOS)
356
#else
120
#endif
}
private var thumbnailHeight: Double {
#if os(tvOS)
200
#else
72
75
#endif
}
private var thumbnailWidth: Double {
thumbnailHeight * Constants.aspectRatio16x9
}
private var videoDurationLabel: String? {
guard videoDuration != 0 else { return nil }
return (videoDuration ?? video?.length)?.formattedAsPlaybackTime()

View File

@@ -440,7 +440,7 @@ struct VideoCell: View {
#endif
}
.mask(RoundedRectangle(cornerRadius: thumbnailRoundingCornerRadius))
.modifier(AspectRatioModifier())
.aspectRatio(Constants.aspectRatio16x9, contentMode: .fill)
}
private var time: String? {
@@ -471,24 +471,6 @@ struct VideoCell: View {
.lineLimit(lineLimit)
.truncationMode(.middle)
}
struct AspectRatioModifier: ViewModifier {
@Environment(\.horizontalCells) private var horizontalCells
func body(content: Content) -> some View {
Group {
if horizontalCells {
content
} else {
content
.aspectRatio(
VideoPlayerView.defaultAspectRatio,
contentMode: .fill
)
}
}
}
}
}
struct VideoCellThumbnail: View {
@@ -496,7 +478,15 @@ struct VideoCellThumbnail: View {
@ObservedObject private var thumbnails = ThumbnailsModel.shared
var body: some View {
ThumbnailView(url: thumbnails.best(video))
GeometryReader { geometry in
let (url, quality) = thumbnails.best(video)
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)
.clipped()
}
}
}

View File

@@ -100,7 +100,7 @@ struct YatteeApp: App {
.commands {
SidebarCommands()
CommandGroup(replacing: .newItem, addition: {})
CommandGroup(replacing: .newItem) {}
MenuCommands(model: Binding<MenuModel>(get: { MenuModel.shared }, set: { _ in }))
}
@@ -191,7 +191,7 @@ struct YatteeApp: App {
NavigationModel.shared.tabSelection = section ?? .search
DispatchQueue.global(qos: .userInitiated).async {
DispatchQueue.main.async {
playlists.load()
}

View File

@@ -487,3 +487,5 @@
"Bugs and great feature ideas can be sent to the GitHub issues tracker. " = "ایرادها و پیشنهادی خوب برای امکانات را می‌توانید به GitHub issues tracker بفرستید. ";
"Copy %@ link" = "پیوند %@ را کپی کنید";
"Copy %@ link with time" = "پیوند %@ با مهرزمان کپی کنید";
"Decreased opacity" = "کاهش تاری";
"Decrease rate" = "کاهش نرخ";

View File

@@ -0,0 +1,564 @@
" subscribers" = " tilaajaa";
"%@ Channel" = "%@ Kanava";
"%@ subscribers" = "%@ tilaajaa";
"%lld videos" = "%lld videota";
"Accounts" = "Tilit";
"Add Account" = "Lisää tili";
"Add Location" = "Lisää Sijainti";
"Add profile..." = "Lisää profiili...";
"Add to %@" = "Lisää kohteeseen %@";
"Add to Favorites" = "Lisää Suosikeihin";
"Advanced" = "Edistynyt";
"Always use AVPlayer for live videos" = "Käytä aina AVPlayeriä live videoille";
"Anonymous" = "Anonyymi";
"Badge" = "Merkki";
"Backend" = "Backend";
"Badge color" = "Merkin väri";
"Based on system color scheme" = "Järjestelmän väriteeman mukainen";
"Battery" = "Akku";
"Blue" = "Sininen";
"Buffering stream..." = "Puskuroidaan...";
"Bugs and great feature ideas can be sent to the GitHub issues tracker. " = "Virheilmoitukset ja hyvät kehitysideat voi lähettää GitHubin ongelmien seurantaan. ";
"Cancel" = "Peruta";
"Captions" = "Tekstitykset";
"Categories to Skip" = "Luokat jotka ohitetaan";
"Category" = "Luokka";
"Charging" = "Ladattaessa";
"Clear" = "Tyhjennä";
"Clear All" = "Tyhjennä kaikki";
"Clear Search History" = "Tyhjennä hakuhistoria";
"Clear Search History..." = "Tyhjennä hakuhistoria...";
"Close PiP when player is opened" = "Sulje Kuva kuvassa, kun soitin avataan";
"Close PiP when starting playing other video" = "Sulje Kuva kuvassa, kun aloitetaan toisen videon toisto";
"Close player when closing video" = "Sulje soitin, kun video suljetaan";
"Close player when starting PiP" = "Sulje soitin, kun Kuva kuvassa aktivoidaan";
"Close Video" = "Sulje video";
"Close video after playing last in the queue" = "Sulje video, kun jonon viimeinen toistettu";
"Comments" = "Kommentit";
"Connected successfully (%@)" = "Yhdistetty onnistuneesti (%@)";
"Connection failed" = "Yhteys epäonnistui";
"Contact" = "Yhteystiedot";
"Continue" = "Jatka";
"Continue from %@" = "Jatka kohdasta %@";
"Contributing" = "Osallistuminen";
"Controls" = "Säätimet";
"Copy %@ link" = "Kopioi %@ linkki";
"Copy %@ link with time" = "Kopioi %@ linkki ja aika";
"Could not load locations manifest" = "SIjaintiluetteloa ei voitu ladata";
"Country" = "Maa";
"Country Name or Code" = "Maan nimi tai koodi";
"Create Playlist" = "Luo soittolista";
"Current: %@\n%@" = "Nykyinen: %@\n%@";
"Custom" = "Mukautettu";
"Custom Locations" = "Mukautetut sijainnit";
"Date" = "Päivämäärä";
"Decrease rate" = "Vähennä nopeutta";
"Decreased opacity" = "Vähennetty opasiteetti";
"Delete" = "Poista";
"Disabled" = "Pois käytöstä";
"Discord Server" = "Discord-serveri";
"Discussions take place in Discord and Matrix. It's a good spot for general questions." = "Keskusteluja käydään Matrixissa ja Discordissa. Se on hyvä paikka yleisiin kysymyksiin.";
"Don't use public locations" = "Älä käytä julkisia sijainteja";
"Donations" = "Lahjoitukset";
"Done" = "Tehty";
"Duration" = "Kestoaika";
"Edit" = "Muokkaa";
"Edit Playlist" = "Muokkaa soittolistaa";
"Edit Quality Profile" = "Muokkaa laatuprofiilia";
"Edit..." = "Muokkaa...";
"Enable logging" = "Kytke loki käyttöön";
"Enable Return YouTube Dislike" = "Palauta YouTube Dislike";
"Enter fullscreen in landscape" = "Siirry koko näytön tilaan vaakasuunnassa";
"Error" = "Virhe";
"Error when accessing playlist" = "Virhe soittolistaa avatessa";
"Favorites" = "Suosikit";
"Find Other" = "Etsi muu";
"Finding something to play..." = "Etsitään jotain toistettavaksi...";
"Frontend URL" = "Etusivun URL";
"Fullscreen size" = "Koko täydellä näytöllä";
"High" = "Korkea";
"Highest" = "Korkein";
"Gaming" = "Pelaaminen";
"Help" = "Apua";
"History" = "Historia";
"Honor orientation lock" = "Kunnioita näytön suunnan lukitusta";
"Hour" = "Tunti";
"I am lost" = "Olen eksyksissä";
"I found a bug /" = "Löysin bugin";
"I have a feature request" = "Minulla on ominaisuuspyyntö";
"I like this app!" = "Tykkään tästä sovelluksesta!";
"If you are reporting a bug, include all relevant details (especially: app version, used device and system version, steps to reproduce)." = "Jos raportoit virheestä, sisällytä kaikki asiaankuuluvat tiedot (erityisesti: sovellusversio, käytetty laite- ja järjestelmäversio, kuinka virheen voi toistaa).";
"Increase rate" = "Lisää nopeutta";
"Info" = "Info";
"Instance of current account" = "Nykyisen tilin instanssi";
"Interaction" = "Vuorovaikutus";
"Interface" = "Käyttöliittymä";
"Issues Tracker" = "Ongelmien seuranta";
"Large" = "Suuri";
"Large layout is not suitable for all devices and using it may cause controls not to fit on the screen." = "Suuri asettelu ei sovellu kaikille laitteille. Käyttämällä sitä, kaikki säätimet eivät välttämättä sovi näytölle.";
"Loading..." = "Ladataan...";
"Locations" = "Sijainnit";
"Lock portrait mode" = "Lukitse muotokuvatila";
"Long" = "Pitkä";
"Low" = "Matala";
"Low quality" = "Matala laatu";
"Lowest" = "Matalin";
"Mark video as watched after playing" = "Merkitse video katsotuksi toiston jälkeen";
"Mark watched videos with" = "Merkitse katsotut videot";
"Matrix Channel" = "Matrix-kanava";
"Medium" = "Keskikoko";
"Milestones" = "Virstanpylväitä";
"Month" = "Kuukausi";
"Music" = "Musiikki";
"New Playlist" = "Uusi soittolista";
"No description" = "Ei kuvausta";
"No Playlists" = "Ei soittolistoja";
"Normal" = "Normaali";
"Not Playing" = "Ei toisteta";
"Nothing" = "Ei mitään";
"Only when signed in" = "Vain sisäänkirjautuneena";
"Open Settings" = "Avaa asetukset";
"Opening audio stream…" = "Avataan äänistreamia…";
"Orientation" = "Suunta";
"Outro" = "Outro";
"Pause" = "Tauko";
"Pause when entering background" = "Tauko, kun siirrytään taustalle";
"Play All" = "Toista kaikki";
"Play in PiP" = "Toista Kuva kuvassa";
"Play Now" = "Toista nyt";
"Playlist" = "Soittolista";
"Playlists" = "Soittolistat";
"Popular" = "Suositut";
"Preferred Formats" = "Suositut formaatit";
"Proxy videos" = "Videot välityspalvelimen kautta";
"Public Locations" = "Julkiset sijainnit";
"Quality" = "Laatu";
"Quality Profile" = "Laatuprofiili";
"Queue" = "Jono";
"Queue is empty" = "Jono on tyhjä";
"Rating" = "Arvosana";
"Recents" = "Viimeaikaiset";
"Red" = "Punainen";
"Refresh" = "Päivitä";
"Regular size" = "Tavallinen koko";
"Regular Size" = "Tavallinen Koko";
"Related" = "Liittyviä";
"Relevance" = "Merkitys";
"Remove from Favorites" = "Poista suosikeista";
"Remove from history" = "Poista historiasta";
"Remove from Playlist" = "Poista soittolistasta";
"Replies" = "Vastaukset";
"Reset" = "Nollaa";
"Reset search filters" = "Nollaa hakusuodattimet";
"Reset watched status when playing again" = "Nollaa katsottu-tila kun toistetaan uudelleen";
"Resolution" = "Resoluutio";
"Restart" = "Käynnistä uudelleen";
"Restart the app to apply the settings above." = "Käynnistä sovellus uudelleen yllä olevien asetusten käyttämiseksi.";
"Restart/Play next" = "Käynnistä uudelleen/Toista seuraava";
"Restore default profiles..." = "Palauta oletusprofiilit...";
"Rotate to portrait when exiting fullscreen" = "Käännä pystyasentoon kun poistutaan koko näytön tilasta";
"Round corners" = "Pyöreät kulmat";
"Save" = "Tallenna";
"Save history of played videos" = "Tallenna toistettujen videoiden historia";
"Save history of searches, channels and playlists" = "Tallenna hakujen, kanavien ja soittolistojen historia";
"Search" = "Haku";
"Search..." = "Haku...";
"Sections" = "Osat";
"Seek gesture sensitivity" = "Kelaus eleen herkkyys";
"Seek gesture speed" = "Kelaus eleen nopeus";
"Seek with horizontal swipe on video" = "Kelaa vaakasuuntaisella pyyhkäisyllä videossa";
"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." = "Osiot jotka yleensä löytyvät videon alusta ja jotka sisältävät animaatioita, still-kuvia tai klippejä, joita on myös saman tekijän muissa videoissa.";
"Select location closest to you:" = "Valitse sinua lähin sijainti:";
"Settings" = "Asetukset";
"Share %@ link" = "Jaa %@ linkki";
"Share %@ link with time" = "Jaa %@ linkki ja aika";
"Share..." = "Jaa...";
"Short" = "Lyhyt";
"Show account username" = "Näytä tilin käyttäjätunnus";
"Show anonymous accounts" = "Näytä anonyymit tilit";
"Show channel name" = "Näytä kanavan nimi";
"Show history" = "Näytä historia";
"Show keywords" = "Näytä avainsanat";
"Show playback statistics" = "Näytä toiston statistiikka";
"Show progress of watching on thumbnails" = "Näytä katselun edistyminen pikkukuvassa";
"Show sidebar when space permits" = "Näytä sivupalkki, kun tila sallii";
"Show video length" = "Näytä videon pituus";
"Shuffle" = "Sekoita";
"Shuffle All" = "Sekoita kaikki";
"Sidebar" = "Sivupalkki";
"Sign In Required" = "Kirjautuminen vaaditaan";
"Small" = "Pieni";
"Sort" = "Järjestä";
"Sort: %@" = "Järjestä: %@";
"Source" = "Lähde";
"SponsorBlock API Instance" = "SponsorBlock API Instanssi";
"Switch to other public location" = "Vaihda toiseen julkiseen sijaintiin";
"System controls buttons" = "Järjestelmän ohjauspainikkeet";
"System controls show buttons for %@" = "Järjestelmän säätimet näyttää painikkeet %@:lle";
"This cannot be reverted" = "Tätä ei voi perua";
"This cannot be reverted. You might need to switch between views or restart the app to see changes." = "Tätä ei voi perua. Joudut ehkä vaihtamaan näkymää tai uudelleen käynnistämään sovelluksen, jotta muutokset näkyvät.";
"This will remove all your custom profiles and return their default values. This cannot be reverted." = "Tämä poistaa kaikki mukautetut profiilit ja palauttaa niiden oletusasetukset. Tätä ei voi perua.";
"Thumbnails" = "Pikkukuvat";
"Today" = "Tänään";
"Trending" = "Trendaavat";
"TV" = "TV";
"Typically near or at the end of the video when the credits pop up and/or endcards are shown." = "Tyypillisesti lähellä videon loppua, jossa on lopputekstit ja/tai loppukortit.";
"unknown" = "tuntematon";
"Unsubscribe" = "Lopeta tilaus";
"Upload date" = "Latauspäivä";
"URL" = "URL";
"Used to create links from videos, channels and playlists" = "Käytetään linkkien luomiseen videoista, kanavista ja soittolistoista";
"Username" = "Käyttäjänimi";
"Very Large" = "Erittäin suuri";
"Views" = "Katselukerrat";
"Watched" = "Katsottu";
"Watched %@" = "Katsottu %@";
"Watching now" = "Katsotaan juuri nyt";
"Week" = "Viikko";
"Welcome" = "Tervetuloa";
"Wi-Fi" = "Wi-Fi";
"Yattee %@ (build %@)" = "Yattee %@ (koontiversio %@)";
"Year" = "Vuosi";
"You can find information about using Yattee in the Wiki pages." = "Löydät tietoa Yatteen käytöstä Wiki-sivuilta.";
"You have no Playlists" = "Sinulla ei ole soittolistoja";
"You have no playlists\n\nTap on \"New Playlist\" to create one" = "Sinulla ei ole soittolistoja\n\nNapauta \"Uusi soittolista\" luodaksesi uuden";
"You need to select an account\nto access %@ section" = "Sinun täytyy valita tili\npäästäksesi %@ osioon";
"Public" = "Julkinen";
"Unlisted" = "Listaamaton";
"Now Playing" = "Toistossa nyt";
"Current Location" = "Nykyinen sijainti";
"Private" = "Yksityinen";
"Playback queue is empty" = "Toistojono on tyhjä";
"Add Channels, Playlists and Searches to Favorites using" = "Lisää kanavia, soittolistoja ja hakuja suosikkeihin käyttäen";
"Make default" = "Tee oletus";
"Visibility" = "Näkyvyys";
"Current Playlist" = "Nykyinen soittolista";
"Stream & Player" = "Striimi ja soitin";
"Statistics" = "Statistiikka";
"Hardware decoder" = "Laitteisto dekoodaaja";
"Stream FPS" = "Striimin FPS";
"Cached time" = "Puskuroitu aika";
"Rate & Captions" = "Nopeus & tekstitykset";
"Dropped frames" = "Jätetyt kehykset";
"Any format" = "Mikä tahansa formaatti";
"%@ formats" = "%@ formaattia";
"Keep last played video in the queue after restart" = "Pidä viimeksi toistettu video jonossa uudelleen käynnistyksen jälkeen";
"It can be changed later in settings. You can use your own locations too." = "Sitä voidaan muuttaa myöhemmin asetuksista. Voit käyttää myös omia sijainteja.";
"Press and hold remote button to open captions and quality menus" = "Paina ja pidä kaukosäätimen painiketta avataksesi tekstitysten ja laadun valikot";
"No comments" = "Ei kommentteja";
"No chapters information available" = "Ei kappale tietoja saatavilla";
"Open logs in Finder" = "Avaa lokit Finderissä";
"Could not refresh Subscriptions" = "Tilauksia ei voitu päivittää";
"Could not load streams" = "Striimejä ei voitu ladata";
"Could not open video" = "Videota ei voitu avata";
"Channel could not be found" = "Kanavaa ei löytynyt";
"Could not extract channel information" = "Kanavan tietoja ei voitu hakea";
"Could not extract SID from received cookies: %@" = "SID:tä ei voitu kaivaa vastaanotetuista evästeistä: %@";
"Could not update your token." = "Valtuutustasi ei voitu päivittää.";
"Could not refresh Trending" = "Trendaavia ei voitu päivittää";
"This URL could not be opened" = "Tätä URL:ia ei voitu avata";
"Could not open channel" = "Kanavaa ei voitu avata";
"Could not refresh Popular" = "Suosittuja ei voitu päivittää";
"Could not create share link" = "Jakolinkkiä ei voitu luoda";
"Could not extract video ID" = "Video ID:tä ei voitu hakea";
"This video could not be opened" = "Tätä videota ei voi avata";
"Could not extract playlist ID" = "Soittolistan ID:tä ei voitu hakea";
"Could not load video" = "Videota ei voitu ladata";
"No locations available at the moment" = "Sijainteja ei ole saatavilla tällä hetkellä";
"Could not refresh Playlists" = "Soittolistaa ei voitu päivittää";
"If you want this app to be available in your language, join translation project." = "Jos haluat tämän sovelluksen olevan saatavilla kielelläsi, liity käännösprojektiin.";
"Translations" = "Käännökset";
"No documents" = "Ei dokumentteja";
"Recent Documents" = "Viimeaikaiset dokumentit";
"Home" = "Koti";
"Share files from Finder on a Mac\nor iTunes on Windows" = "Jaa tiedostoja Macin Finderistä\ntai iTunesista Windowsissa";
"Show Home" = "Näytä koti";
"Show Open Videos quick actions" = "Näytä Avaa Videoita -pikatoiminnot";
"Recent History" = "Viimeaikainen historia";
"Show Favorites" = "Näytä suosikit";
"Inspector visibility" = "Inspektorin näkyvyys";
"Edit Favorites…" = "Muokkaa suosikkeja…";
"Show Open Videos toolbar button" = "Näytä Avaa Videoita -painike työkalupalkissa";
"Buttons labels" = "Painikkeiden nimikkeet";
"Files" = "Tiedostot";
"Show Documents" = "Näytä dokumentit";
"Pages toolbar position" = "SIvujen työkalurivin positio";
"Video Details" = "Videon tiedot";
"Show Inspector" = "Näytä inspektori";
"Open" = "Avaa";
"Video actions buttons" = "Videotoimintojen painikkeet";
"Pages buttons" = "Sivujen painikkeet";
"URL to Open" = "Avattava URL";
"Enter link to open" = "Syötä avattava linkki";
"Could not open Files" = "Tiedostoja ei voitu avata";
"Paste" = "Liitä";
"Open Videos" = "Avaa videot";
"Enter links to open, one per line" = "Syötä avattavat linkit, yksi per rivi";
"Playback Mode" = "Toistotila";
"Add" = "Lisää";
"Hide" = "Piilota";
"Always" = "Aina";
"Only for local files and URLs" = "Vain paikallisille tiedostoille ja URL:lle";
"Right" = "Oikea";
"Channels" = "Kanavat";
"Open Files" = "Avaa tiedostot";
"Share" = "Jaa";
"Show icons and text when space permits" = "Näytä kuvakkeet ja teksti, kun tila sallii";
"Left" = "Vasen";
"Format" = "Formaatti";
"Driver" = "Ajuri";
"Show only icons" = "Näytä vain kuvakkeet";
"Center" = "Keskellä";
"Documents" = "Dokumentit";
"Audio" = "Audio";
"File" = "Tiedosto";
"Video" = "Video";
"Codec" = "Kodekki";
"Size" = "Koko";
"FPS" = "FPS";
"Sample Rate" = "Näytetaajuus";
"Could not find any links to open in your clipboard" = "Leikepöydältä ei löytynyt yhtään avattavaa linkkiä";
"Address" = "Osoite";
"Remove…" = "Poista…";
"Actions buttons" = "Toiminto-painikkeet";
"Show sidebar" = "Näytä sivupalkki";
"Locations Manifest" = "Sijaintien luettelo";
"Remove Location" = "Poista sijainti";
"Open Video" = "Avaa video";
"Default Profile" = "Oletusprofiili";
"Playback history is empty" = "Toistohistoria on tyhjä";
"Copy%@link" = "Kopioi %@ linkki";
"Share%@link" = "Jaa %@ linkki";
"Are you sure you want to remove this document?" = "Oletko varma, että haluat poistaa tämän dokumentin?";
"\"%@\" will be irreversibly removed from this device." = "\"%@\" poistetaan peruuttamattomasti tästä laitteesta.";
"Could not delete document" = "Dokumenttia ei voitu poistaa";
"Are you sure you want to remove %@ location?" = "Haluatko varmasti poistaa %@ sijainnin?";
"Shorts" = "Shorts";
"Verified" = "Tarkistettu";
"Channel" = "Kanava";
"Open expanded" = "Avaa laajennettuna";
"Mark channel feed as unwatched" = "Merkitse kanavasyöte katsomattomaksi";
"Mark channel feed as watched" = "Merkitse kanavasyöte katsotuksi";
"Player Bar" = "Toistopalkki";
"Short videos: visible" = "Shorts-videot: näkyvillä";
"Short videos: hidden" = "Shorts-videot: piilotettu";
"Play all unwatched" = "Toista kaikki katsomattomat";
"Double tap gesture" = "Kaksoisnapautus ele";
"Tap and hold channel thumbnail to open context menu with more actions" = "Napauta ja pidä kanavan pikkukuvaa avataksesi valikon, jossa lisää toimintoja";
"Always show controls buttons" = "Näytä aina ohjauspainikkeet";
"Single tap gesture" = "Yhden napautuksen ele";
"Maximum width expanded" = "Suurin leveys laajennettuna";
"Clear all" = "Tyhjennä kaikki";
"Right click channel thumbnail to open context menu with more actions" = "Klikkaa kanavan pikkukuvaa hiiren oikealla avataksesi valikon, jossa lisää toimintoja";
"Show unwatched feed badges" = "Näytä katsomattoman syötteen merkit";
"Seeking" = "Kelaaminen";
"Gesture: fowards" = "Ele: eteenpäin";
"Controls Buttons" = "Ohjauspainikkeet";
"System controls" = "Järjestelmän säätimet";
"Controls button: backwards" = "Ohjauspainike: taaksepäin";
"Controls button: forwards" = "Ohjauspainike: eteenpäin";
"Gesture: backwards" = "Ele: taaksepäin";
"Gesture settings control skipping interval for double tap gesture on left/right side of the player. Changing system controls settings requires restart." = "Ele asetukset ohjaavat skippaus väliä kaksoisnapautus eleelle soittimen vasemmalla/oikealla puolella. Järjestelmäsäätimien asetuksian muuttaminen vaatii uudelleenkäynnistyksen.";
"Hide player" = "Piilota soitin";
"Actions Buttons" = "Toiminto-painikkeet";
"Play next item" = "Toista seuraava kohde";
"Lock orientation" = "Lukitse näytön suunta";
"Music Mode" = "Musiikki-tila";
"Total size: %@" = "Koko: %@";
"Open channels with description expanded" = "Avaa kanavat kuvaus on laajennettuna";
"Cache" = "Välimuisti";
"Subscribe/Unsubscribe" = "Tilaa/Lopeta tilaus";
"Show cache status" = "Näytä välimuistin tila";
"Maximum feed items" = "Syötteen kohteiden enimmäismäärä";
"Are you sure you want to clear cache?" = "Haluatko varmasti tyhjentää välimuistin?";
"Show Next in Queue" = "Näytä seuraava jonossa";
"Show toggle watch status button" = "Näytä katsottu-tilan valitsin painike";
"Next in Queue" = "Seuraavana jonossa";
"List" = "Lista";
"Cells" = "Solut";
"Toggle size" = "Vaihda koko";
"Toggle player" = "Valitse soitin";
"Do nothing" = "Älä tee mitään";
"Feed" = "Syöte";
"Open channel" = "Avaa kanava";
"Inspector" = "Inspektori";
"Open video description expanded" = "Avaa videon kuvaus laajennettuna";
"Mark all as unwatched" = "Merkitse kaikki katsomattomaksi";
"Mark all as watched" = "Merkitse kaikki katsotuksi";
"Queue - shuffled" = "Jono - sekoitettu";
"Playback Settings" = "Toiston asetukset";
"Replay" = "Katso uudelleen";
"Fullscreen" = "Koko näyttö";
"Lock" = "Lukitse";
"Description" = "Kuvaus";
"Loop one" = "Yksi video silmukkana";
"Autoplay next" = "Toista seuraava automaattisesti";
"Stream" = "Striimi";
"Show scroll to top button in comments" = "Näytä vieritä ylös-painike kommenteissa";
"Enter account credentials to connect..." = "Syötä tilin tunnistetiedot yhdistääksesi…";
"Enter location address to connect..." = "Syötä sijainnin osoite yhdistääksesi…";
"Seek" = "Kelaa";
"Opened File" = "Avattu tiedosto";
"File Extension" = "Tiedostopääte";
"Opening file…" = "Tiedostoa avataan…";
"Public account" = "Julkinen tili";
"Close video and player on end" = "Sulje video ja soitin lopuksi";
"Use system controls with AVPlayer" = "Käytä järjestelmän ohjaimia AVPlayerille";
"Rotate when entering fullscreen on landscape video" = "Käännä kun siirrytään koko näytölle vaakasuuntaisessa videossa";
"Landscape left" = "Vaakasuunta vasen";
"Landscape right" = "Vaakasuunta oikea";
"No rotation" = "Ei kääntöä";
"Available" = "Saatavilla";
"Startup section" = "Aloitusosio";
"Home Settings" = "Koti asetukset";
"Watched: hidden" = "Katsottu: piilotettu";
"Watched: visible" = "Katsottu: näkyvillä";
"No videos to show" = "Ei videoita näytettäväksi";
"(watched hidden)" = "(katsotut piilotettu)";
"(shorts hidden)" = "(shorts-videot piilotettu)";
"Play Now in MPV" = "Toista nyt MPV:llä";
"Disable filters" = "Poista suodattimet käytöstä";
"Limit" = "Rajoita";
"Are you sure you want to remove %@ from Favorites?" = "Haluatko poistaa %@:n suosikeista?";
"Show video context menu options to force selected backend" = "Näytä videon kontekstivalikon valinnat pakottaaksesi valitun backendin";
"Podcasts" = "Podcastit";
"Releases" = "Julkaisut";
"Add %@" = "Lisää %@";
"Description preview" = "Kuvauksen esikatselu";
"No preview" = "Ei esikatselua";
"Open vertical chapters expanded" = "Avaa pystysuuntaiset kappaleet laajennettuna";
"Chapters (if available)" = "Kappaleet (jos saatavilla)";
"Import Settings..." = "Tuo asetukset…";
"Export Settings" = "Vie asetukset";
"Accounts passwords (unencrypted)" = "Tilien salasanat (salaamaton)";
"Other" = "Muu";
"Other data" = "Muu data";
"Export..." = "Vie…";
"Other data include last used playback preferences and listing options" = "Muu data sisältää viimeksi käytetyt toistoasetukset ja listaus vaihtoehdot";
"Are you sure you want to export unencrypted passwords?" = "Haluatko varmasti viedä salaamattomat salasanat?";
"Icon only" = "Vain kuvake";
"Export" = "Vie";
"File information" = "Tiedoston tiedot";
"Build" = "Koontiversio";
"Import" = "Tuo";
"Platform" = "Alusta";
"Action button labels" = "Toiminto -painikkeiden nimikkeet";
"Icon and text" = "Kuvake ja teksti";
"Password required to import" = "Tuonti vaatii salasanan";
"Custom Location already exists" = "Mukautettu sijainti on jo olemassa";
"Custom Location not selected for import" = "Mukautettua sijaintia ei ole valittu tuontia varten";
"Account already exists" = "Tili on jo olemassa";
"Password saved in import file" = "Salasana tallennettu tuontitiedostoon";
"Export in progress..." = "Vienti meneillään…";
"In progress..." = "Meneillään…";
"10 seconds forwards/backwards" = "10 sekuntia eteen/taakse";
"%@ Playlist" = "%@ Soittolista";
"Add Location..." = "Lisää Sijainti..";
"Accounts are not supported for the application of this instance" = "Tilejä ei tueta tämän instanssin käyttöön";
"Add Account..." = "Lisää tili...";
"Add Quality Profile" = "Lisää Laatuprofiili";
"Add to Playlist..." = "Lisää Soittolistaan...";
"Add to Playlist" = "Lisää Soittolistaan";
"Clear History" = "Tyhjennä historia";
"All" = "Kaikki";
"Any" = "Mikä tahansa";
"Apply to all" = "Käytä kaikkiin";
"Are you sure you want to clear history of watched videos?" = "Haluatko varmasti tyhjentää katsottujen videoiden historian?";
"Are you sure you want to clear search history?" = "Haluatko varmasti tyhjentää hakuhistorian?";
"Are you sure you want to delete playlist?" = "Haluatko varmasti poistaa soittolistan?";
"Are you sure you want to restore default quality profiles?" = "Haluatko varmasti palauttaa oletusarvoiset laatuprofiilit?";
"Are you sure you want to unsubscribe from %@?" = "Haluatko varmasti poistaa tilauksen %@?";
"Automatic" = "Automaattinen";
"Autoplaying Next" = "Toistetaan automaattisesti seuraava";
"Badge & Decreased opacity" = "Merkki & Vähentynyt opasiteetti";
"Browsing" = "Selaaminen";
"Button" = "Painike";
"Cellular" = "Mobiiliverkko";
"Chapters" = "Kappaleet";
"Clear All Recents" = "Tyhjennä kaikki viimeaikaiset";
"Clear the queue" = "Tyhjennä jono";
"Close" = "Sulje";
"Close PiP and open player when application enters foreground" = "Sulje kuva kuvassa ja avaa soitin, kun sovellus tulee etualalle";
"LIVE" = "LIVE";
"Loading streams…" = "Ladataan…";
"Mark as watched" = "Merkitse katsotuksi";
"Matrix Chat" = "Matrix Chat";
"Medium quality" = "Keskimääräinen laatu";
"Movies" = "Elokuvat";
"MPV Documentation" = "MPV-dokumentaatio";
"More info can be found in:" = "Lisätietoja löytyy:";
"Name" = "Nimi";
"Next" = "Seuraava";
"No results" = "Ei tuloksia";
"Not available" = "Ei saatavilla";
"Password" = "Salasana";
"Open \"Playlists\" tab to create new one" = "Avaa \"Soittolistat\" -välilehti luodaksesi uuden";
"Opening %@ stream…" = "Avataan %@ streamia…";
"Picture in Picture" = "Kuva kuvassa";
"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." = "Osa videosta, joka edistää tuotetta tai palvelua, joka ei liity suoraan tekijään. Tekijä saa maksun tai korvauksen rahan tai ilmaisten tuotteiden muodossa.";
"Pause when player is closed" = "Tauko, kun soitin suljetaan";
"Play" = "Toista";
"Play Last" = "Toista viimeinen";
"Play Next" = "Toista seuraava";
"Play Music" = "Toista musiikki";
"Playback" = "Toisto";
"Player" = "Soitin";
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "Soittolista \"%@\" poistetaan\nToimintoa ei voi perua.";
"Profiles" = "Profiilit";
"Promoting a product or service that is directly related to the creator themselves. This usually includes merchandise or promotion of monetized platforms." = "Mainostaa tuotetta tai palvelua, joka liittyy suoraan tekijään. Tämä sisältää yleensä kaupankäyntialustojen tuotteita tai mainostamista.";
"Highest quality" = "Korkein laatu";
"I want to ask a question" = "Haluan esittää kysymyksen";
"If you are interested what's coming in future updates, you can track project Milestones." = "Jos olet kiinnostunut siitä, mitä tulevissa päivityksissä tapahtuu, voit seurata projektin virstanpylväitä.";
"Intro" = "Intro";
"Just watched" = "Juuri katsottu";
"Hide sidebar" = "Kätke sivupalkki";
"Public Manifest" = "Julkinen luettelo";
"Offtopic in Music Videos" = "Offtopic musiikkivideoissa";
"Rate" = "Nopeus";
"Remove" = "Poista";
"Remove from the queue" = "Poista jonosta";
"Search history is empty" = "Hakuhistoria on tyhjä";
"Self-promotion" = "Itsensä mainostaminen";
"Smaller" = "Pienempi";
"Sponsor" = "Sponsori";
"SponsorBlock" = "SponsoriBlock";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Nimenomainen muistutus tykkätä, tilata tai olla vuorovaikutuksessa heidän kanssaan millä tahansa maksullisella tai ilmaisella alustalla (esim. napsauttamalla videota).";
"Filter" = "Suodatin";
"Filter: active" = "Suodatin: käytössä";
"For videos which feature music as the primary content." = "Videoille, joissa musiikki on ensisijainen sisältö.";
"Formats will be selected in order as listed.\nHLS is an adaptive format (resolution setting does not apply)." = "Formaatit valitaan listan mukaisessa järjestyksessä.\nHLS on adaptiivinen formaatti (resoluutioasetus ei sovellu).";
"Subscriptions" = "Tilaukset";
"Switch to public locations" = "Vaihda julkiseen sijaintiin";
"Subscribe" = "Tilaa";
"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." = "Se on kiva kuulla. On hauskaa tarjota sovelluksia, joita muut haluavat käyttää. Voit harkita lahjoittamista projektiin tai apua edistämällä uusia ominaisuuksia.";
"Yattee" = "Yattee";
"This information will be processed only on your device and used to connect you to the server in the specified country." = "Tämä tieto käsitellään ainoastaan laitteessasi ja sitä käytetään yhdistämään palvelimeen määrittelemässäsi maassa.";
"Videos" = "Videot";
"When partially watched video is played" = "Kun osittain katsottu video toistetaan";
"Wiki" = "Wiki";
"Playing Next" = "Toistetaan seuraavaksi";
"You can switch between profiles in playback settings controls." = "Voit vaihtaa profiilien välillä toistoasetuksissa.";
"Comments are disabled" = "Kommentit ovat poissa käytöstä";
"For custom locations you can configure Frontend URL in Locations settings" = "Voit konfiguroida mukautetun sijainnin etusivun URL:n Sijaintien asetuksissa";
"Share Logs..." = "Jaa lokit…";
"Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "Soittolista on tyhjä\n\nNapauta ja pidä videota ja sitten\n”Lisää soittolistaan”";
"Could not open playlist" = "Soittolistaa ei voitu avata";
"Reload manifest" = "Uudelleen lataa luettelo";
"Clear Queue before opening" = "Tyhjennä jono ennen avaamista";
"Gesture settings control skipping interval for double click on left/right side of the player. Changing system controls settings requires restart." = "Ele asetukset ohjaavat skippaus väliä kaksoisklikkaukselle soittimen vasemmalla/oikealla puolella. Järjestelmäsäätimien asetuksian muuttaminen vaatii uudelleenkäynnistyksen.";
"Gesture settings control skipping interval for remote arrow buttons (for 2nd generation Siri Remote or newer). Changing system controls settings requires restart." = "Ele asetukset ohjaavat skippaus väliä kaukosäätimen nuolinäppäimille (2. sukupolven Siri Remote, tai uudempi). Järjestelmäsäätimien asetuksian muuttaminen vaatii uudelleenkäynnistyksen.";
"Close video" = "Sulje video";
"Live Streams" = "Live striimit";
"You need to create an instance and accounts\nto access %@ section" = "Sinun täytyy luoda instanssi ja tilejä\npäästäksesi %@ osioon";
"You can use automatic profile selection based on current device status or switch it in video playback settings controls." = "Voit käyttää automaattista profiilivalintaa nykyisen laitteen tilan perusteella tai vaihtaa sitä videon toistoasetuksissa.";
"Play Now in AVPlayer" = "Toista nyt AVPlayerillä";
"Show channel avatars in videos lists" = "Näytä kanavien kuvakkeet video listoissa";
"Show channel avatars in channels lists" = "Näytä kanavien kuvakkeet kanavalistoissa";
"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" = "Älä jaa tätä tiedostoa kenenkään kanssa tai voit menettää pääsyn tileillesi. Jos et valitse salasanojen vientiä, sinua pyydetään antamaan ne tuonnin aikana";
"Custom Location selected for import" = "Mukautettu sijainti valittu tuontia varten";
"Your Accounts" = "Sinun tilisi";
"Browse without account" = "Selaa ilman tiliä";
"(watched and shorts hidden)" = "(katsotut ja shorts-videot piilotettu)";
"Keep channels with unwatched videos on top of subscriptions list" = "Pidä kanavat, joilla katsomattomia videoita, tilauslistan yläosassa";

View File

@@ -4,7 +4,7 @@
"Save history of searches, channels and playlists" = "検索、チャンネル、再生リストの履歴を保存";
"Statistics" = "統計";
"Apply to all" = "これをすべてに適用";
"Reload manifest" = "マニフェストを再読込";
"Reload manifest" = "定義ファイルを再読込";
"Left" = "左";
"Codec" = "コーデック";
"Add profile..." = "プロファイルの追加...";
@@ -15,8 +15,8 @@
"%lld videos" = "%lld本の動画";
"%@ Channel" = "%@ チャンネル";
"%@ Playlist" = "%@ 再生リスト";
"Add Location" = "場所を追加";
"Add Location..." = "場所を追加...";
"Add Location" = "インスタンスを追加";
"Add Location..." = "インスタンスを追加...";
"Advanced" = "高度";
"Add to Favorites" = "お気に入りに追加";
"Add to %@" = "%@に追加";
@@ -43,13 +43,13 @@
"Continue from %@" = "%@から続ける";
"Comments" = "コメント";
"Create Playlist" = "再生リストの作成";
"Could not load locations manifest" = "場所のマニフェストが読み込めません";
"Could not load locations manifest" = "インスタンスの定義ファイルを読み込めません";
"Copy %@ link" = "%@ リンクをコピー";
"Copy %@ link with time" = "%@ リンクを時間指定でコピー";
"Current: %@\n%@" = "現在: %@\n%@";
"Decreased opacity" = "透明化";
"Delete" = "削除";
"Custom Locations" = "場所の指定";
"Custom Locations" = "インスタンスの指定";
"Decrease rate" = "速度を下げる";
"Done" = "完了";
"Discord Server" = "Discord のサーバー";
@@ -57,7 +57,7 @@
"Edit" = "編集";
"Edit Playlist" = "再生リストを編集";
"Donations" = "寄付";
"Don't use public locations" = "公開された場所を使用しない";
"Don't use public locations" = "公開インスタンスを使用しない";
"Enable logging" = "ログ記録を有効化";
"Enable Return YouTube Dislike" = "低評価を表示(Return YouTube Dislike)";
"Error" = "エラー";
@@ -90,7 +90,7 @@
"Loading streams…" = "ストリーム読込中…";
"Lock portrait mode" = "縦モードをロック";
"LIVE" = "ライブ";
"Locations" = "場所";
"Locations" = "インスタンス";
/* Video duration filter in search */
"Long" = "長い";
@@ -121,7 +121,7 @@
"Play Music" = "音楽を再生";
"Profiles" = "プロファイル";
"Popular" = "人気";
"Public Manifest" = "公開のマニフェスト";
"Public Manifest" = "公開の定義ファイル";
"Queue" = "再生キュー";
"Red" = "赤";
"Recents" = "最近";
@@ -144,7 +144,7 @@
"Seek with horizontal swipe on video" = "動画で水平スワイプでシーク";
"Search" = "検索";
"Search..." = "検索...";
"Select location closest to you:" = "あなたの場所に近い場所を選択:";
"Select location closest to you:" = "あなたに近い場所を選択します:";
"Share %@ link" = "%@ リンクを共有";
"Share %@ link with time" = "%@ リンク 時間指定で共有";
"Show playback statistics" = "再生データの統計を表示";
@@ -162,7 +162,7 @@
/* Subscriptions title */
"Subscriptions" = "登録チャンネル";
"Switch to other public location" = "ほかの公開された場所に切り替え";
"Switch to other public location" = "ほかの公開インスタンスに変更";
"This cannot be reverted" = "元に戻せません";
"This cannot be reverted. You might need to switch between views or restart the app to see changes." = "元に戻せません。変更の確認には、表示を切り替えたり、アプリの再起動が必要なこともあります。";
"Trending" = "急上昇";
@@ -184,13 +184,13 @@
"You have no playlists\n\nTap on \"New Playlist\" to create one" = "再生リストなし\n\n作成には「新規再生リスト」をタップ";
"Private" = "非公開";
"Unlisted" = "限定公開";
"Current Location" = "現在の場所";
"Current Location" = "現在のインスタンス";
"You can switch between profiles in playback settings controls." = "再生設定の制御からプロファイルを切り替えできます。";
"Add Channels, Playlists and Searches to Favorites using" = "チャンネル、再生リスト、検索をお気に入りにするには";
"Stream FPS" = "ストリーム FPS";
"Dropped frames" = "フレーム落ち";
"Keep last played video in the queue after restart" = "再起動しても再生待ちの最後の動画を維持";
"It can be changed later in settings. You can use your own locations too." = "設定は後で変更できます。場所の指定も可能です。";
"It can be changed later in settings. You can use your own locations too." = "設定は後で変更できます。インスタンスの指定も可能です。";
"Comments are disabled" = "コメント無効です";
"Could not refresh Subscriptions" = "登録チャンネルを更新できません";
"Could not load streams" = "ストリームを開けません";
@@ -201,7 +201,7 @@
"Could not load video" = "動画を読み込めません";
"Translations" = "翻訳";
"Could not refresh Playlists" = "再生リストを更新できません";
"No locations available at the moment" = "現時点で利用可能な場所がありません";
"No locations available at the moment" = "現時点で利用可能なインスタンスがありません";
"Show Open Videos quick actions" = "動画を開くクイック操作を表示";
"Show Documents" = "文書を表示";
"Pages toolbar position" = "ページのツールバー位置";
@@ -221,12 +221,12 @@
"Audio" = "音声";
"Actions buttons" = "操作ボタン";
"Default Profile" = "標準のプロファイル";
"Remove Location" = "場所を削除";
"Locations Manifest" = "場所のマニフェスト";
"Remove Location" = "インスタンスを削除";
"Locations Manifest" = "インスタンスの定義ファイル";
"Copy%@link" = "%@リンクをコピー";
"Share%@link" = "%@リンクを共有";
"\"%@\" will be irreversibly removed from this device." = "「%@」はこの端末から完全に削除されます。";
"Are you sure you want to remove %@ location?" = "場所 %@ を削除しますか?";
"Are you sure you want to remove %@ location?" = "インスタンス %@ を削除しますか?";
"Live Streams" = "ライブ配信";
"Mark channel feed as watched" = "チャンネルフィードを視聴済みにする";
"Short videos: visible" = "ショート動画: 表示";
@@ -526,10 +526,10 @@
"Gesture settings control skipping interval for double tap gesture on left/right side of the player. Changing system controls settings requires restart." = "ジェスチャーの設定で、プレイヤーの左右をダブルタップした時のスキップ間隔を変更します。システム制御の設定を変更するには、再起動が必要です。";
"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." = "作成者と直接関係のない製品やサービスを宣伝する部分。作成者は金銭や商品提供によって報酬を受け取ります。";
"Press and hold remote button to open captions and quality menus" = "リモコンのボタンを長押しすると、字幕と品質のメニューが表示されます";
"For custom locations you can configure Frontend URL in Locations settings" = "場所を指定するには、場所の設定からフロントエンドのURLを設定します";
"Public Locations" = "公開された場所";
"Switch to public locations" = "公開された場所に切り替え";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "有料/無料のプラットフォームかを問わず、いいね、登録などを明示的に操作を促す(例: 動画をクリック)。\n";
"For custom locations you can configure Frontend URL in Locations settings" = "インスタンスを指定するには、設定からフロントエンドのURLを設定します";
"Public Locations" = "公開インスタンス";
"Switch to public locations" = "公開インタンスに切り替え";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "有料/無料のプラットフォームかを問わず、いいね、登録などを明示的に操作を促す(例: 動画をクリック)。";
"Proxy videos" = "動画閲覧にプロキシ使用";
"Sections" = "表示するボタン";
"System controls show buttons for %@" = "システム制御「%@」用のボタンを表示";
@@ -610,14 +610,14 @@
"File information" = "ファイル情報";
"Platform" = "プラットフォーム";
"Icon and text" = "アイコンと文字";
"Custom Location not selected for import" = "指定の場所は取り込み用に選択されていません";
"Custom Location not selected for import" = "指定したインスタンスは取り込み用に選択されていません";
"Import Settings..." = "設定の取り込み...";
"Export Settings" = "設定を出力";
"Accounts passwords (unencrypted)" = "アカウントのパスワード (暗号化なし)";
"Other" = "ほか";
"Other data" = "ほかのデータ";
"Are you sure you want to export unencrypted passwords?" = "暗号化のないパスワードを本当に出力しますか?";
"Custom Location selected for import" = "指定の場所は取り込み用に選択済み";
"Custom Location selected for import" = "指定したインスタンスは取り込み用に選択済み";
"Export" = "出力";
"Build" = "ビルド";
"Import" = "取り込み";
@@ -628,4 +628,4 @@
"Password saved in import file" = "取り込みファイルにパスワードを保存しました";
"Account already exists" = "アカウントは既に存在します";
"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" = "このファイルを他の人と共有しないでください。パスワードを出力していなければ、取り込み時にパスワードが求められます";
"Custom Location already exists" = "指定の場所は既に存在します";
"Custom Location already exists" = "指定したインスタンスは既に存在します";

View File

@@ -10,33 +10,33 @@
"Add Location" = "Добавить локацию";
"Add profile..." = "Добавить профиль...";
"Add to %@" = "Добавить к %@";
"Add to Favorites" = "Добавить к избранному";
"Add to Playlist" = "Добавить к плейлисту";
"Add to Favorites" = "Добавить в избранное";
"Add to Playlist" = "Добавить в плейлист";
"Anonymous" = "Анонимный";
/* Video date filter in search
Video duration filter in search */
"Any" = "Любой";
"Any" = "Любая";
"Apply to all" = "Применить ко всем";
"Are you sure you want to delete playlist?" = "Вы точно хотите удалить плейлист?";
"Are you sure you want to unsubscribe from %@?" = "Вы точно хотите описаться от %@?";
"Autoplaying Next" = "Автоматически проигрывать след. видео";
"Backend" = "Бэкэнд";
"Are you sure you want to unsubscribe from %@?" = "Вы точно хотите отписаться от %@?";
"Autoplaying Next" = "Автоматически воспроизводится следующее";
"Backend" = "Backend";
"Blue" = "Синий";
"Button" = "Кнопка";
"Cancel" = "Отмена";
"Captions" = "Субтитры";
"Categories to Skip" = "Категории для пропуска";
"Cellular" = "Через мобильный интернет";
"Cellular" = "Сотовая связь";
"Charging" = "Зарядка";
"Clear All Recents" = "Очистить все последние записи";
"Clear All Recents" = "Очистить всё последнее";
"Close" = "Закрыть";
"Close PiP when player is opened" = "Закрыть К-в-К, когда плеер открыт";
/* Video sort order in search */
"Date" = "Дата";
"Decrease rate" = "Снижение уровня";
"Decreased opacity" = "Уменьшение непрозрачности";
"Date" = "По дате";
"Decrease rate" = "Снизить скорость";
"Decreased opacity" = "Прозрачностью";
"Delete" = "Удалить";
"Donations" = "Донаты";
"Done" = "Применить";
@@ -44,14 +44,14 @@
"Edit Playlist" = "Редактировать плейлист";
"Edit Quality Profile" = "Редактировать профиль качества";
"Edit..." = "Редактировать...";
"Enable Return YouTube Dislike" = "Включить дизлайки";
"Enable Return YouTube Dislike" = "Включить Return YouTube Dislike";
"Error" = "Ошибка";
"Error when accessing playlist" = "Ошибка при доступе к плейлисту";
"Favorites" = "Избранное";
"For videos which feature music as the primary content." = "Для видео, в которых музыка используется в качестве основного контента.";
"Highest quality" = "Высшее качество";
"For videos which feature music as the primary content." = "Для видео, основным содержанием которых является музыка.";
"Highest quality" = "Наивысшее качество";
"History" = "История";
"Honor orientation lock" = "Фиксация ориентации Honor";
"Honor orientation lock" = "Учитывать блокировку ориентации";
"Locations" = "Локации";
"More info can be found in:" = "Более подробную информацию можно найти в:";
"Movies" = "Фильмы";
@@ -70,20 +70,20 @@
"Play Next" = "Играть следующее";
"Preferred Formats" = "Предпочитаемые форматы";
"Profiles" = "Профили";
"Proxy videos" = "Проксировать видео";
"Related" = "Связанный";
"Proxy videos" = "Использовать прокси для видео";
"Related" = "Похожие";
"Share..." = "Поделиться...";
/* Video duration filter in search */
"Short" = "Короткое";
"Show account username" = "Показать никнэйм аккаунта";
"Short" = "Короткая";
"Show account username" = "Показать имя пользователя аккаунта";
"Show anonymous accounts" = "Показать анонимные аккаунты";
"Show channel name" = "Показать имя канала";
"Show keywords" = "Показать ключевые слова";
"Show playback statistics" = "Показать статистику просмотра";
"Show sidebar when space permits" = "Показать боковую панель, когда позволяет пространство";
"Show video length" = "Показать длину видео";
"Shuffle" = "Переместить";
"Shuffle" = "Перемешать";
"Sign In Required" = "Требуется войти";
/* Player controls layout size */
@@ -93,11 +93,11 @@
/* Selected video was played on given date */
"Watched %@" = "Просмотренные %@";
"Wi-Fi" = "Wi-Fi";
"You have no playlists\n\nTap on \"New Playlist\" to create one" = "У вас нет списков воспроизведения\n\nНажмите на \"Новый плейлист\", чтобы создать его";
"You have no playlists\n\nTap on \"New Playlist\" to create one" = "У вас нет плейлистов\n\nНажмите на \"Новый плейлист\", чтобы создать один";
"Could not refresh Subscriptions" = "Не удалось обновить подписки";
"This video could not be opened" = "Это видео не удалось открыть";
"Translations" = "Переводы";
"Show Open Videos quick actions" = "Показать быстродействие открытия видео";
"Show Open Videos quick actions" = "Показать быстрые действия при открытии видео";
"Recent History" = "Недавняя история";
"Files" = "Файлы";
"Show Documents" = "Показать документы";
@@ -115,14 +115,14 @@
"Open channel" = "Открыть канал";
"Inspector" = "Инспектор";
"Open video description expanded" = "Открыть расширенное описание видео";
" subscribers" = " подписчики";
" subscribers" = " подписчиков";
"%@ Channel" = "%@ Канал";
"%@ Playlist" = "%@ Плейлист";
"10 seconds forwards/backwards" = "10 секунд вперёд/назад";
"Accounts are not supported for the application of this instance" = "Аккаунты не поддерживаются для приложения на этом инстансе";
"Add Location..." = "Добавить локацию...";
"Accounts are not supported for the application of this instance" = "Аккаунты не поддерживаются приложением для этого инстанса";
"Add Location..." = "Добавить локацию..";
"Add Quality Profile" = "Добавить профиль качества";
"Add to Playlist..." = "Добавить к плейлисту...";
"Add to Playlist..." = "Добавить в плейлист...";
"Advanced" = "Расширенные";
/* Trending category, section containing all kinds of videos */
@@ -130,37 +130,37 @@
"Always use AVPlayer for live videos" = "Всегда использовать AVPlayer для прямых трансляций";
"Are you sure you want to clear search history?" = "Вы точно хотите очистить историю поиска?";
"Are you sure you want to clear history of watched videos?" = "Вы точно хотите очистить историю просмотренных видео?";
"Are you sure you want to restore default quality profiles?" = "Вы точно хотите восстановить настройки по умолчанию для профиля качества?";
"Automatic" = "Автоматически";
"Badge" = "Оценка";
"Badge & Decreased opacity" = "Оценка и уменьшенная непрозрачность";
"Badge color" = "Цвет оценки";
"Are you sure you want to restore default quality profiles?" = "Вы точно хотите восстановить изначальные профили качества?";
"Automatic" = "Автоматический";
"Badge" = "Значком";
"Badge & Decreased opacity" = "Значком и прозрачностью";
"Badge color" = "Цвет значка";
"Based on system color scheme" = "Как в системе";
"Battery" = "Батарея";
"Browsing" = "В браузере";
"Buffering stream..." = "Буферизация прямой трансляции...";
"Bugs and great feature ideas can be sent to the GitHub issues tracker. " = "Баги и новые идеи можно отправить на трекер проблем на GitHub. ";
"Browsing" = "Навигация";
"Buffering stream..." = "Буферизация потока...";
"Bugs and great feature ideas can be sent to the GitHub issues tracker. " = "Об ошибках и предложениях сообщайте на странице проекта в GitHub во вкладке Issues. ";
"Category" = "Категория";
"Chapters" = "Главы";
"Clear" = "Очистить";
"Clear All" = "Очистить всё";
"Clear History" = "Очистить историю";
"Close PiP and open player when application enters foreground" = "Закрыть К-в-К и открыть плеер, когда приложение не в спящем режиме";
"Close PiP and open player when application enters foreground" = "Закрыть К-в-К и открыть плеер, когда приложение на переднем плане";
"Clear Search History" = "Очистить историю поиска";
"Show Open Videos toolbar button" = "Кнопка \"Показать открытые видео\" на панели инструментов";
"Buttons labels" = "Ярлыки кнопок";
"Show Open Videos toolbar button" = "Показать кнопку \"Открыть видео\" на панели инструментов";
"Buttons labels" = "Надписи на кнопках";
"Rotate to portrait when exiting fullscreen" = "Поворачивать в портретное положение при выходе из полноэкранного режима";
"Round corners" = "Закругленные углы";
"Save" = "Сохранить";
"Save history of played videos" = "Сохранить историю проигранных видео";
"Save history of searches, channels and playlists" = "Сохранение истории поиска, каналов и плейлистов";
"Save history of searches, channels and playlists" = "Сохранить истории поиска, каналов и плейлистов";
"Search" = "Поиск";
"Search history is empty" = "История поиска пуста";
"Search..." = "Поиск...";
"Sections" = "Разделы";
"Seek gesture sensitivity" = "Поиск чувствительности к жестам";
"Seek gesture speed" = "Поиск скорости жеста";
"Seek with horizontal swipe on video" = "Поиск с помощью горизонтального свайпа по видео";
"Seek gesture sensitivity" = "Чувствительность горизонтального свайпа";
"Seek gesture speed" = "Скорость горизонтального свайпа";
"Seek with horizontal swipe on video" = "Прокручивать видео горизонтальным свайпом";
"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:" = "Выберите ближайшее к вам местоположение:";
@@ -171,7 +171,7 @@
"Share %@ link" = "Поделиться %@ ссылкой";
"Show history" = "Показать историю";
"Show progress of watching on thumbnails" = "Показать прогресс просмотра на миниатюрах";
"Shuffle All" = "Переместить всё";
"Shuffle All" = "Перемешать всё";
"Sidebar" = "Боковая панель";
"Sort" = "Сортировать";
@@ -181,25 +181,25 @@
/* SponsorBlock category name */
"Sponsor" = "Спонсорская реклама";
"Source" = "Исходный код";
"SponsorBlock API Instance" = "Истанс SponsorBlock API";
"Source" = "Источник";
"SponsorBlock API Instance" = "Инстанс SponsorBlock API";
"Subscribe" = "Подписаться";
/* Subscriptions title */
"Subscriptions" = "Подписчики";
"Subscriptions" = "Подписки";
"Switch to other public location" = "Переключиться на другую публичную локацию";
"This cannot be reverted" = "Это не может быть отменено";
"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." = "Это не может быть отменено. Возможно, вам потребуется переключаться между представлениями или перезапускать приложение, чтобы увидеть изменения.";
"This cannot be reverted" = "Это нельзя потом отменить";
"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." = "Это нельзя потом отменить. Возможно, вам потребуется переключиться между представлениями или перезапустить приложение, чтобы увидеть изменения.";
"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." = "Это удалит все ваши пользовательские профили и вернет их значения по умолчанию. Это не может быть отменено.";
"This will remove all your custom profiles and return their default values. This cannot be reverted." = "Это удалит все ваши пользовательские профили и вернет их значения по умолчанию. Это нельзя потом отменить.";
"Thumbnails" = "Миниатюры";
/* Video date filter in search */
"Today" = "Сегодня";
"Today" = "За сегодня";
"Trending" = "Тренды";
/* Player controls layout size for TV */
@@ -216,60 +216,60 @@
"Very Large" = "Очень большой";
/* Video sort order in search */
"Views" = "Просмотры";
"Views" = "По просмотрам";
"Watched" = "Просмотренные";
/* Selected video is being played */
"Watching now" = "Смотреть сейчас";
"Watching now" = "Смотрим сейчас";
"Yattee %@ (build %@)" = "Yattee %@ (сборка %@)";
/* Video date filter in search */
"Week" = "Неделя";
"Week" = "За эту неделю";
"Welcome" = "Добро пожаловать";
"Wiki" = "Вики";
"When partially watched video is played" = "При воспроизведении частично просмотренного видео";
"Yattee" = "Yattee";
/* Video date filter in search */
"Year" = "Год";
"Year" = "За этот год";
"You can find information about using Yattee in the Wiki pages." = "Вы можете найти информацию об использовании Yattee на страницах вики.";
"You can use automatic profile selection based on current device status or switch it in video playback settings controls." = "Вы можете использовать автоматический выбор профиля на основе текущего состояния устройства или переключить его в элементах управления настройками воспроизведения видео.";
"You need to create an instance and accounts\nto access %@ section" = "Вам нужно создать экземпляр и учетные\nзаписи для доступа к разделу %@";
"You can use automatic profile selection based on current device status or switch it in video playback settings controls." = "Вы можете использовать автоматический выбор профиля на основе текущего состояния устройства или переключить его в настройками воспроизведения видео.";
"You need to create an instance and accounts\nto access %@ section" = "Вам нужно создать инстанс и аккаунты\nдля доступа к %@ разделу";
"You have no Playlists" = "У вас нет плейлистов";
"You need to select an account\nto access %@ section" = "Вам нужно выбрать учетную\nзапись для доступа к разделу %@";
"You need to select an account\nto access %@ section" = "Вам нужно выбрать аккаунт\nдля доступа к %@ разделу";
"Public" = "Публично";
"Public" = "Публичный";
"Now Playing" = "Сейчас играет";
"Unlisted" = "По ссылке";
"Current Location" = "Текущая локация";
"Private" = "Лично";
"SponsorBlock" = "СпонсорБлок";
"Private" = "Личный";
"SponsorBlock" = "SponsorBlock";
"Playback queue is empty" = "Очередь воспроизведения пуста";
"Playing Next" = "Играет дальше";
"You can switch between profiles in playback settings controls." = "Вы можете переключаться между профилями в элементах управления настройками воспроизведения.";
"You can switch between profiles in playback settings controls." = "Вы можете переключаться между профилями в настройках воспроизведения видео.";
"Add Channels, Playlists and Searches to Favorites using" = "Добавить каналы, плейлисты и результаты поиска в избранное с помощью";
"Stream & Player" = "Прямая трансляция и плеер";
"Stream & Player" = "Стрим и плеер";
"Make default" = "Сделать по умолчанию";
"Statistics" = "Статистика";
"Visibility" = "Видимость";
"Current Playlist" = "Текущий плейлист";
"Hardware decoder" = "Аппаратный декодер";
"Stream FPS" = "К/с прямой трансляции";
"Stream FPS" = "К/с потока";
"Cached time" = "Кэшированное время";
"Rate & Captions" = "Оценка и субтитры";
"Rate & Captions" = "Скорость и субтитры";
"Dropped frames" = "Пропущенные кадры";
"Keep last played video in the queue after restart" = "Сохранить последнее воспроизведенное видео в очереди после перезапуска";
"Any format" = "Любой формат";
"%@ formats" = "%@ форматы";
"Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "Список воспроизведения пуст\n\nНажмите и удерживайте видео, а затем\n\"Добавить в список воспроизведения\"";
"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." = "Это можно изменить позже в настройках. Вы также можете использовать свои собственные локации.";
"Comments are disabled" = "Комментарии отключены";
"Press and hold remote button to open captions and quality menus" = "Нажмите и удерживайте кнопку дистанционного управления, чтобы открыть титры и меню качества";
"Press and hold remote button to open captions and quality menus" = "Нажмите и удерживайте кнопку дистанционного управления, чтобы открыть субтитры и меню качества";
"No comments" = "Комментариев нет";
"No chapters information available" = "Информация о главах недоступна";
"Share Logs..." = "Поделиться логами…";
"Could not load streams" = "Не удалось загрузить прямые трансляции";
"Could not load streams" = "Не удалось загрузить потоки";
"Channel could not be found" = "Не удалось найти канал";
"Could not open video" = "Не удалось открыть видео";
"Could not extract channel information" = "Не удалось извлечь информацию о канале";
@@ -277,9 +277,9 @@
"Could not update your token." = "Не удалось обновить ваш токен.";
"Could not refresh Trending" = "Не удалось обновить тренды";
"This URL could not be opened" = "Этот URL-адрес не удалось открыть";
"For custom locations you can configure Frontend URL in Locations settings" = "Для пользовательских местоположений вы можете настроить URL-адрес интерфейса в настройках местоположений";
"For custom locations you can configure Frontend URL in Locations settings" = "Для пользовательских локаций вы можете настроить Frontend URL в настройках Локации";
"Could not open channel" = "Не удалось открыть канал";
"Could not refresh Popular" = "Не удалось обновить вкладку \"Популярность\"";
"Could not refresh Popular" = "Не удалось обновить вкладку \"Популярное\"";
"Could not create share link" = "Не удалось создать ссылку для общего доступа";
"Could not extract playlist ID" = "Не удалось извлечь идентификатор плейлиста";
"Could not open playlist" = "Не удалось открыть плейлист";
@@ -289,24 +289,24 @@
"Could not refresh Playlists" = "Не удалось обновить плейлисты";
"If you want this app to be available in your language, join translation project." = "Если вы хотите, чтобы это приложение было доступно на вашем языке, присоединяйтесь к проекту перевода.";
"No documents" = "Нет документов";
"Recent Documents" = "Последние документы";
"Recent Documents" = "Недавние документы";
"Home" = "Дом";
"Show Home" = "Показать дом";
"Share files from Finder on a Mac\nor iTunes on Windows" = "Делитесь файлами из Finder на Mac\nили iTunes на Windows";
"Show Favorites" = "Показать избранное";
"Inspector visibility" = "Видимость инспектора";
"Edit Favorites…" = "Редактирование избранного…";
"Edit Favorites…" = "Редактировать избранное…";
"Close PiP when starting playing other video" = "Закрыть К-в-К, когда воспроизводится другое видео";
"Close player when closing video" = "Закрыть плеер, когда закрывается видео";
"Close player when closing video" = "Закрыть плеер при закрытии видео";
"Close player when starting PiP" = "Закрыть плеер, когда запущен К-в-К";
"Close video after playing last in the queue" = "Закрыть видео после проигрывания последнего в очереди";
"Close video after playing last in the queue" = "Закрыть видео после проигрывания очереди";
"Comments" = "Комментарии";
"Connected successfully (%@)" = "Подключение завершено (%@)";
"Contact" = "Контакт";
"Connection failed" = "Подключение провалено";
"Connected successfully (%@)" = "Подключение успешно (%@)";
"Contact" = "Обратная связь";
"Connection failed" = "Подключение не удалось";
"Continue" = "Продолжить";
"Continue from %@" = "Продолжить с %@";
"Contributing" = "Контрибуция";
"Contributing" = "Внести свой вклад";
"Controls" = "Управления";
"Copy %@ link" = "Копировать %@ ссылку";
"Copy %@ link with time" = "Копировать %@ ссылку со временем";
@@ -319,37 +319,37 @@
/* Locations settings, custom instance is selected as current */
"Custom" = "Пользовательские";
"Custom Locations" = "Пользовательские локации";
"Disabled" = "Отключить";
"Discord Server" = "Сервер на Discord";
"Disabled" = "Отключены";
"Discord Server" = "Discord сервер";
"Discussions take place in Discord and Matrix. It's a good spot for general questions." = "Обсуждения проходят в Discord и Matrix. Это хорошие места для общих вопросов.";
"Don't use public locations" = "Не использовать публичные локации";
"Duration" = "Продолжительность";
"Enable logging" = "Включить логирование";
"Enable logging" = "Включить ведение журнала";
"Enter fullscreen in landscape" = "Войти в полноэкранный режим в альбомной ориентации";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Явные напоминания о необходимости поставить лайк, подписаться или взаимодействовать с ними на любой платной или бесплатной платформе (платформах) (например, нажать на видео).";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Явные напоминания о необходимости поставить лайк, подписаться или взаимодействовать с автором на любой платной или бесплатной платформе (платформах) (например, нажать на видео).";
"Filter" = "Фильтр";
"Filter: active" = "Фильтр: активен";
"Finding something to play..." = "Ищу другие видео...";
"Formats will be selected in order as listed.\nHLS is an adaptive format (resolution setting does not apply)." = "Форматы будут выбраны в том порядке, как указано в списке.\nHLS - это адаптивный формат (настройка разрешения не применяется).";
"Formats will be selected in order as listed.\nHLS is an adaptive format (resolution setting does not apply)." = "Форматы будут выбраны в порядке, указанном в списке.\nHLS - это адаптивный формат (настройка разрешения не применяется).";
"Find Other" = "Найти другое";
"Frontend URL" = "URL инстанса";
"Fullscreen size" = "Размер видео в полноэкранном";
"Frontend URL" = "Frontend URL";
"Fullscreen size" = "Полноэкранный размер";
"Gaming" = "Игры";
"Help" = "Помощь";
"Hide sidebar" = "Скрыть боковую панель";
"Highest" = "Высшее";
"Highest" = "Наивысшая";
/* Video date filter in search */
"Hour" = "Час";
"Hour" = "За последний час";
"I am lost" = "Я заблудился";
"I found a bug /" = "Я нашёл баг /";
"High" = "Высокое";
"I have a feature request" = "Я придумал новую функцию";
"I found a bug /" = "Я нашёл ошибку /";
"High" = "Высокая";
"I have a feature request" = "У меня есть предложение";
"I like this app!" = "Мне нравится это приложение!";
"I want to ask a question" = "У меня есть вопрос";
"If you are reporting a bug, include all relevant details (especially: app version, used device and system version, steps to reproduce)." = "Если вы сообщаете об ошибке, укажите все соответствующие сведения (особенно: версию приложения, используемое устройство и версию системы, как пришли к ошибке).";
"If you are interested what's coming in future updates, you can track project Milestones." = "Если вам интересно, что будет в будущих обновлениях, вы можете отслеживать этапы проекта.";
"Increase rate" = "Увеличение скорости";
"If you are reporting a bug, include all relevant details (especially: app version, used device and system version, steps to reproduce)." = "Если вы сообщаете об ошибке, укажите все необходимые сведения (особенно: версию приложения, используемое устройство, версию операционной системы, а также шаги для воспроизведения этой ошибки).";
"If you are interested what's coming in future updates, you can track project Milestones." = "Если вам интересно, что будет в будущих обновлениях, вы можете следить за вехами (milestones) проекта на его странице GitHub.";
"Increase rate" = "Увеличить скорость";
"Instance of current account" = "Инстанс текущего аккаунта";
"Interface" = "Интерфейс";
"Info" = "Информация";
@@ -361,57 +361,57 @@
"Large" = "Большой";
/* SponsorBlock category name */
"Intro" = "Интро";
"Issues Tracker" = "Трекер ошибок";
"Large layout is not suitable for all devices and using it may cause controls not to fit on the screen." = "Большой лайаут подходит не для всех устройств, и ее использование может привести к тому, что элементы управления не поместятся на экране.";
"Intro" = "Заставка";
"Issues Tracker" = "Отслеживание ошибок";
"Large layout is not suitable for all devices and using it may cause controls not to fit on the screen." = "Большой размер не подходит для всех устройств, и его использование может привести к тому, что элементы управления не поместятся на экране.";
/* Selected video has just finished playing */
"Just watched" = "Просто наблюдал";
"Just watched" = "Только что посмотрел";
"LIVE" = "ПРЯМАЯ ТРАНСЛЯЦИЯ";
/* Loading stream OSD */
"Loading streams…" = "Загрузка прямой трансляции…";
"Loading streams…" = "Загрузка потока…";
"Loading..." = "Загрузка...";
"Lock portrait mode" = "Блокировка портретного режима";
"Low" = "Низкое";
"Lock portrait mode" = "Блокировать портретный режим";
"Low" = "Низкая";
"Low quality" = "Низкое качество";
/* Video duration filter in search */
"Long" = "Длинное";
"Lowest" = "Низшее";
"Mark as watched" = "Оцените видео";
"Mark video as watched after playing" = "Оцените видео после просмотра";
"Mark watched videos with" = "Оцените видео с";
"Matrix Channel" = "Канал на Matrix";
"Matrix Chat" = "Чат на Matrix";
"Long" = "Длинная";
"Lowest" = "Наименьшая";
"Mark as watched" = "Отметить как просмотренное";
"Mark video as watched after playing" = "Отметить видео как просмотренное после воспроизведения";
"Mark watched videos with" = "Отметить просмотренные видео";
"Matrix Channel" = "Matrix канал";
"Matrix Chat" = "Matrix чат";
/* Player controls layout size */
"Medium" = "Среднее";
"Medium" = "Средний";
"Medium quality" = "Среднее качество";
/* Video date filter in search */
"Month" = "Месяц";
"Milestones" = "Этапы проекта";
"Month" = "За этот месяц";
"Milestones" = "Вехи (Milestones)";
"Next" = "Далее";
"No Playlists" = "Без плейлистов";
"No description" = "Без описания";
"No results" = "Без результатов";
"Nothing" = "Ничего";
"Normal" = "Обычное";
"No Playlists" = "Нет плейлистов";
"No description" = "Нет описания";
"No results" = "Нет результатов";
"Nothing" = "Ничем";
"Normal" = "Обычная";
"Not available" = "Нет доступа";
"Not Playing" = "Нет просмотров";
"Not Playing" = "Не воспроизводится";
"Only when signed in" = "Только после входа в";
"Open \"Playlists\" tab to create new one" = "Открыть вкладку \"Плейлисты\", чтобы создать новый";
"Open Settings" = "Отрыть настройки";
"Open \"Playlists\" tab to create new one" = "Откройте вкладку \"Плейлисты\", чтобы создать новый";
"Open Settings" = "Открыть настройки";
/* Loading stream OSD */
"Opening %@ stream…" = "Открытие %@ прямой трансляции…";
"Opening audio stream…" = "Открытие прямой трансляции аудио…";
"Opening %@ stream…" = "Открытие %@ потока…";
"Opening audio stream…" = "Открытие аудиопотока…";
"Orientation" = "Ориентация";
/* SponsorBlock category name */
"Outro" = "Оутро";
"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." = "Часть видеоролика, рекламирующего продукт или услугу, не имеющие прямого отношения к создателю. Создатель получит оплату или компенсацию в виде денег или бесплатных продуктов.";
"Outro" = "Концовка";
"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 when player is closed" = "Пауза при закрытии плеера";
"Picture in Picture" = "Картинка-в-Картинке";
"Play" = "Играть";
@@ -422,36 +422,36 @@
"Player" = "Плеер";
"Playlist" = "Плейлист";
"Playlists" = "Плейлисты";
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "Список воспроизведения \"%@\" будет удален.\nЭтого не изменить.";
"Popular" = "Популярность";
"Promoting a product or service that is directly related to the creator themselves. This usually includes merchandise or promotion of monetized platforms." = "Продвижение продукта или услуги, которые непосредственно связаны с самим создателем. Обычно это включает в себя товары или продвижение монетизированных платформ.";
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "Плейлист \"%@\" будет удалён.\nНавсегда.";
"Popular" = "Популярное";
"Promoting a product or service that is directly related to the creator themselves. This usually includes merchandise or promotion of monetized platforms." = "Продвижение продукта или сервис, которые непосредственно связаны с самим создателем. Обычно это связано с продажей товаров или продвижением подписочной платформы.";
"Public Manifest" = "Публичный манифест";
"Quality" = "Качество";
"Public Locations" = "Публичные локации";
"Queue" = "Очередь";
"Quality Profile" = "Профиль качества";
"Queue is empty" = "Очередь пуста";
"Rate" = "Оценить";
"Rate" = "Скорость";
/* Video sort order in search */
"Rating" = "Рейтинг";
"Rating" = "По рейтингу";
"Recents" = "Недавние";
"Regular size" = "Обычный размер";
"Red" = "Красный";
"Refresh" = "Обновить";
"Regular Size" = "Обычный Размер";
"Remove" = "Стереть";
"Remove from Favorites" = "Стереть из избранного";
"Remove from history" = "Стереть из истории";
"Remove from Playlist" = "Стереть из плейлиста";
"Remove" = "Удалить";
"Remove from Favorites" = "Удалить из избранного";
"Remove from history" = "Удалить из истории";
"Remove from Playlist" = "Удалить из плейлиста";
/* Video sort order in search */
"Relevance" = "Актуальность";
"Remove from the queue" = "Стереть из очереди";
"Relevance" = "По релевантности";
"Remove from the queue" = "Удалить из очереди";
"Replies" = "Ответы";
"Reset search filters" = "Поисковые фильтры по умолчанию";
"Reset search filters" = "Сбросить фильтры поиска";
"Reset" = "По умолчанию";
"Reset watched status when playing again" = "Статус просмотра при повторном воспроизведении по умолчанию";
"Reset watched status when playing again" = "Сбросить состояние просмотра при повторном воспроизведении";
"Restart" = "Перезапустить";
"Resolution" = "Разрешение";
"Restart the app to apply the settings above." = "Перезапустите приложение, чтобы применить выбранные настройки.";
@@ -466,72 +466,72 @@
"\"%@\" will be irreversibly removed from this device." = "\"%@\" будет необратимо удален с этого устройства.";
"Could not delete document" = "Не удалось удалить документ";
"Open Video" = "Открыть Видео";
"Are you sure you want to remove %@ location?" = "Вы уверены, что хотите удалить локацию %@ ?";
"Are you sure you want to remove %@ location?" = "Вы уверены, что хотите удалить %@ локацию?";
"Live Streams" = "Прямые Трансляции";
"Shorts" = "Короткие видео";
"Verified" = "Проверенный";
"Channel" = "Канал";
"Mark channel feed as unwatched" = "Пометить канал как не просматриваемый";
"Mark channel feed as unwatched" = "Пометить канал как не просмотренный";
"Mark channel feed as watched" = "Пометить канал как просмотренный";
"Open expanded" = "Открыть расширенный";
"Open expanded" = "Открыть расширенным";
"Short videos: visible" = "Короткие видео: показать";
"Player Bar" = "Панель плеера";
"Short videos: hidden" = "Короткие видео: скрыть";
"Double tap gesture" = "Жест двойного касания";
"Tap and hold channel thumbnail to open context menu with more actions" = "Нажмите и удерживайте миниатюру канала, чтобы открыть контекстное меню с дополнительными действиями";
"Always show controls buttons" = "Всегда показывать кнопки управления";
"Single tap gesture" = "Жест \"однократное нажатие\"";
"Single tap gesture" = "Жест однократного касания";
"Maximum width expanded" = "Максимальная ширина в полноэкранном режиме";
"Clear all" = "Очистить весь кэш";
"Right click channel thumbnail to open context menu with more actions" = "Щелчок правой кнопкой мыши на миниатюре канала открывает контекстное меню с дополнительными действиями";
"Show unwatched feed badges" = "Показать значки непросмотренных каналов";
"Seeking" = "Искать";
"Seeking" = "Прокрутка";
"Gesture: fowards" = "Жест: вперед";
"Controls Buttons" = "Кнопки управления";
"System controls" = "Системные средства управления";
"Controls button: backwards" = "Кнопка управления: назад";
"Controls button: forwards" = "Кнопка управления: вперед";
"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 tap gesture on left/right side of the player. Changing system controls settings requires restart." = "Настройки жестов управляют интервалом пропуска жеста двойного касания левой/правой стороны плеера. Изменение настройки системного управления требует перезагрузки.";
"Hide player" = "Скрыть плеер";
"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." = "Настройки жестов управляют интервалом пропуска кнопок-стрелок пульта ДУ (для пульта Siri Remote 2-го поколения или новее). Изменение настроек управления системой требует перезагрузки.";
"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." = "Настройки жестов управляют интервалом пропуска кнопок-стрелок пульта ДУ (для пульта Siri Remote 2-го поколения или новее). Изменение настройки системного управления требует перезагрузки.";
"Close Video" = "Закрыть Видео";
"Open channels with description expanded" = "Открытые каналы с расширенным описанием";
"Open channels with description expanded" = "Открывать каналы с расширенным описанием";
"Show Next in Queue" = "Показать следующее в очереди";
"Subscribe/Unsubscribe" = "Подписаться/Отписаться";
"Maximum feed items" = "Максимальное количество объектов в ленте";
"Are you sure you want to clear cache?" = "Вы уверены, что хотите очистить кэш?";
"Show cache status" = "Показать статус кэша";
"Show toggle watch status button" = "Показать кнопку переключения состояния часов";
"Show toggle watch status button" = "Показать кнопку переключения статуса просмотра";
"Next in Queue" = "Следующий в очереди";
"List" = "Список";
"Cells" = "Клетки";
"Toggle size" = "Тогл размера";
"Toggle player" = "Тогл плеера";
"Toggle size" = "Размер переключателя";
"Toggle player" = "Переключить плеер";
"Do nothing" = "Ничего не делать";
"Feed" = "Лента";
"Mark all as watched" = "Пометить все как просмотренное";
"Mark all as unwatched" = "Пометить все как непросмотренное";
"Queue - shuffled" = "Очередь - перемещена";
"Rotate when entering fullscreen on landscape video" = "Поворот при переходе в полноэкранный режим на видео с ландшафтом";
"Landscape left" = "Ландшафт слева";
"Landscape right" = "Ландшафт справа";
"No rotation" = "Нет вращения";
"Mark all as watched" = "Пометить всё как просмотренное";
"Mark all as unwatched" = "Пометить всё как непросмотренное";
"Queue - shuffled" = "Очередь - перемешанная";
"Rotate when entering fullscreen on landscape video" = "Поворачивать при переходе в полноэкранный режим в альбомной ориентации";
"Landscape left" = "Альбомная ориентация слева";
"Landscape right" = "Альбомная ориентация справа";
"No rotation" = "Без вращений";
"Available" = "Доступно";
"Startup section" = "Раздел ввода в эксплуатацию";
"Startup section" = "Начальный раздел";
"Watched: hidden" = "Просмотренное: скрыто";
"Home Settings" = "Настройки дома";
"(watched and shorts hidden)" = "(просмотрено и короткие видео скрыты)";
"Watched: visible" = "Просморенные: показаны";
"(watched and shorts hidden)" = "(просмотренное и короткие видео скрыты)";
"Watched: visible" = "Просмотренные: показаны";
"No videos to show" = "Нет видео для показа";
"(shorts hidden)" = "(короткие видео скрыты)";
"Limit" = "Лимит";
"(watched hidden)" = "(просмотренные скрыты)";
"Disable filters" = "Отключить фильтры";
"Are you sure you want to remove %@ from Favorites?" = "Вы уверены, что хотите удалить %@ из избранного?";
"Keep channels with unwatched videos on top of subscriptions list" = "Сохранять каналы с непросмотренными видеороликами в верхней части списка подписок";
"Show video context menu options to force selected backend" = "Показывать опции контекстного меню видео для принудительного выбора бэкенда";
"Keep channels with unwatched videos on top of subscriptions list" = "Держать каналы с непросмотренными видео в верхней части списка подписок";
"Show video context menu options to force selected backend" = "Показать опции контекстного меню видео для принудительного выбора backend";
"Play Now in MPV" = "Играть сейчас в MPV";
"Play Now in AVPlayer" = "Играть сейчас в AVPlayer";
"Show channel avatars in videos lists" = "Показывать аватары каналов в списках видео";
@@ -539,17 +539,17 @@
"Fullscreen" = "Полный экран";
"Playback Settings" = "Настройки воспроизведения";
"Lock" = "Заблокировать";
"Replay" = "Воспроизвести";
"Replay" = "Повторить";
"Description" = "Описание";
"Loop one" = "Повторить одно";
"Loop one" = "Зациклить видео";
"Close video" = "Закрыть видео";
"Autoplay next" = "Автоплей следующего";
"Autoplay next" = "Авто-очередь";
"Stream" = "Прямая трансляция";
"Show scroll to top button in comments" = "Показать кнопку прокрутки к верху в комментариях";
"Enter account credentials to connect..." = "Введите данные аккаунта для подключения...";
"Opened File" = "Открытие файла";
"Seek" = "Искать следующее/предыдущее";
"Enter location address to connect..." = "Введите адрес местоположения для подключения...";
"Opened File" = "Открытый файл";
"Seek" = "Перемотать видео вперёд/назад";
"Enter location address to connect..." = "Введите адрес локации для подключения...";
"Show Inspector" = "Показать инспектора";
"Clear Queue before opening" = "Очистить очередь до открытия";
"URL to Open" = "URL-адрес для открытия";
@@ -581,7 +581,7 @@
"Documents" = "Документы";
"Video" = "Видео-файл";
"Sample Rate" = "Частота дискретизации";
"Could not find any links to open in your clipboard" = "Не удалось найти никаких ссылок для открытия в вашем буфере обмена";
"Could not find any links to open in your clipboard" = "Не удалось найти никаких ссылок для открытия из вашего буфера обмена";
"Audio" = "Аудио-файл";
"FPS" = "К/с";
"Address" = "Адрес";
@@ -590,15 +590,42 @@
"Remove…" = "Удалить…";
"Actions buttons" = "Кнопки действия";
"Show sidebar" = "Показать боковую панель";
"Browse without account" = "Искать без аккаунта";
"Opening file…" = "Отрытие файла…";
"Browse without account" = "Смотреть без аккаунта";
"Opening file…" = "Открытие файла…";
"Public account" = "Публичный аккаунт";
"Your Accounts" = "Ваши аккаунты";
"Close video and player on end" = "Закрыть видео и плеер в конце";
"Use system controls with AVPlayer" = "Использование системных элементов управления с помощью AVPlayer";
"Use system controls with AVPlayer" = "Использовать системных элементы управления с помощью AVPlayer";
"File Extension" = "Расширение файла";
"No preview" = "Без предварительного просмотра";
"Description preview" = "Предварительный просмотр описания";
"Podcasts" = "Подкасты";
"Releases" = "Релизы";
"Add %@" = "Добавить %@";
"Import Settings..." = "Импортировать настройки...";
"Export Settings" = "Экспортировать настройки";
"Accounts passwords (unencrypted)" = "Пароли аккаунтов (не зашифрованные)";
"Other" = "Другое";
"Other data" = "Другие данные";
"Export..." = "Экспортировать…";
"Export" = "Экспорт";
"Platform" = "Платформа";
"Import" = "Импорт";
"Icon only" = "Только иконка";
"Icon and text" = "Иконка и текст";
"File information" = "Информация о файле";
"Account already exists" = "Аккаунт уже существует";
"Chapters (if available)" = "Главы (если доступны)";
"Password saved in import file" = "Пароль сохранён в файле импорта";
"Password required to import" = "Для импорта требуется пароль";
"Other data include last used playback preferences and listing options" = "Другие данные включают в себя настройки последнего воспроизведения и параметры списка";
"Are you sure you want to export unencrypted passwords?" = "Вы точно хотите экспортировать не зашифрованные пароли?";
"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" = "Не передавайте этот файл никому, иначе вы можете потерять доступ к своим аккаунтам. Если вы не выберете экспорт паролей, вас попросят предоставить их при импорте";
"Build" = "Сборка";
"Action button labels" = "Надписи кнопок действия";
"Custom Location already exists" = "Пользовательская локация уже существует";
"Custom Location selected for import" = "Пользовательская локация выбрана для импорта";
"Custom Location not selected for import" = "Пользовательская локация не выбрана для импорта";
"Export in progress..." = "Экспортируем...";
"In progress..." = "Выполняется…";
"Open vertical chapters expanded" = "Открывать вертикальные главы развернутыми";

View File

@@ -5,30 +5,30 @@
"Continue" = "Продовжити";
/* Locations settings, custom instance is selected as current */
"Custom" = "Своє";
" subscribers" = " підписники";
"Custom" = "Користувальницькі";
" subscribers" = " підписників";
"%@ Channel" = "%@ Канал";
"%@ Playlist" = "%@ Плейлист";
"%@ subscribers" = "%@ підписників";
"%lld videos" = "%lld відео";
"10 seconds forwards/backwards" = "Перемотати 10 секунд";
"10 seconds forwards/backwards" = "10 секунд вперед/назад";
"Accounts" = "Акаунти";
"Accounts are not supported for the application of this instance" = "Акаунти не підтримуються для цього екземпляру";
"Accounts are not supported for the application of this instance" = "Акаунти не підтримуються програмою для цього інстанса";
"Add Account" = "Додати акаунт";
"Add Account..." = "Додати акаунт...";
"Add Location" = "Нова локація (API)";
"Add Location..." = "Додати локацію...";
"Add Location" = "Додати локацію (API)";
"Add Location..." = "Додати локацію..";
"Add profile..." = "Додати профіль...";
"Add Quality Profile" = "Додати профіль якості";
"Add to %@" = "Додати до %@";
"Add to Favorites" = "Додати в обране";
"Add to Favorites" = "Додати в улюблене";
"Add to Playlist" = "Додати в плейлист";
"Add to Playlist..." = "Додати в плейлист...";
"Advanced" = "Розширені";
/* Trending category, section containing all kinds of videos */
"All" = "Все";
"Anonymous" = "Анонімно";
"All" = "Всі";
"Anonymous" = "Анонімний";
/* Video date filter in search
Video duration filter in search */
@@ -37,46 +37,46 @@
"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?" = "Ви впевнені, що бажаєте відновити стандартні профілі якості?";
"Automatic" = "Автоматично";
"Automatic" = "Автоматичний";
"Autoplaying Next" = "Автовідтворення наступного";
"Backend" = "Бекенд";
"Backend" = "Backend";
"Badge color" = "Колір значка";
"Based on system color scheme" = "На основі системної кольорової гами";
"Battery" = "Заряд";
"Blue" = "Блакитний";
"Browsing" = "Перегляд";
"Based on system color scheme" = "Як в системі";
"Battery" = "Батарея";
"Blue" = "Синій";
"Browsing" = "Навігація";
"Buffering stream..." = "Буферизація потоку..";
"Button" = "Кнопка";
"Cancel" = "Скасування";
"Cancel" = "Скасувати";
"Captions" = "Субтитри";
"Continue from %@" = "Продовжити з %@";
"Contributing" = "Внесок";
"Could not load locations manifest" = "Не вдається загрузити маніфест локацій";
"Could not load locations manifest" = "Не вдалося завантажити маніфест локацій";
"Country" = "Країна";
"For videos which feature music as the primary content." = "Для відеороликів, основним змістом яких є музика.";
"Formats will be selected in order as listed.\nHLS is an adaptive format (resolution setting does not apply)." = "Формати будуть вибиратися в порядку, як зазначено в списку. \nHLS - адаптивний формат (налаштування роздільної здатності не застосовується).";
"Frontend URL" = "URL-адреса інтерфейсу";
"Hide sidebar" = "Сховати сайдбар";
"For videos which feature music as the primary content." = "Для відео, основним змістом яких є музика.";
"Formats will be selected in order as listed.\nHLS is an adaptive format (resolution setting does not apply)." = "Формати будуть обрані в порядку, зазначеному в списку.\nHLS - адаптивний формат (налаштування роздільної здатності не застосовується).";
"Frontend URL" = "Frontend URL";
"Hide sidebar" = "Сховати бічну панель";
"High" = "Висока";
"Highest" = "Найвища";
"Highest quality" = "Найвища якість";
"History" = "Історія";
/* Video date filter in search */
"Hour" = "Година";
"Hour" = "За останню годину";
"I am lost" = "Я загубився";
"I found a bug /" = "Я знайшов баг /";
"I have a feature request" = "В мене є пропозиція";
"I like this app!" = "Мені подобається цей застосунок!";
"I have a feature request" = "У мене є пропозиція";
"I like this app!" = "Мені подобається ця програма!";
"I want to ask a question" = "Хочу задати питання";
"If you are interested what's coming in future updates, you can track project Milestones." = "Якщо вам цікаво, що буде в наступних оновленнях, ви можете відстежувати етапи проєкту.";
"Increase rate" = "Підвищити якість";
"Info" = "Інфо";
"Instance of current account" = "Экземпляр поточного акаунта";
"Picture in Picture" = "Картинка в картинці";
"If you are interested what's coming in future updates, you can track project Milestones." = "Якщо вам цікаво, що буде в наступних оновленнях, ви можете стежити за віхами (milestones) проекту на його сторінці GitHub.";
"Increase rate" = "Збільшити швидкість";
"Info" = "Інфомація";
"Instance of current account" = "Інстанс поточного акаунта";
"Picture in Picture" = "Картинка-в-картинці";
"Play" = "Грати";
"Play All" = "Грати все";
"Play in PiP" = "Картинка в картинці";
"Play in PiP" = "Грати в К-в-К";
"Play Last" = "Грати останнє";
"Play Music" = "Грати музику";
"Play Next" = "Грати наступне";
@@ -84,36 +84,36 @@
"Playback" = "Відтворення";
"Player" = "Плеєр";
"Playlist" = "Плейлист";
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "Плейлист \"%@\" буде видалений. \nЦе не можна буде скасувати.";
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "Плейлист \"%@\" буде видалений.\nЦе не можна буде скасувати.";
"Playlists" = "Плейлисти";
"Popular" = "Популярне";
"Preferred Formats" = "Пріорітетні формати";
"Preferred Formats" = "Бажані формати";
"Profiles" = "Профілі";
"Proxy videos" = "Використовувати проксі";
"Public Locations" = "Публічні API";
"Public Locations" = "Публічні API (локації)";
"Public Manifest" = "Публічний маніфест";
"Quality" = "Якість";
"Quality Profile" = "Профіль якості";
"Queue" = "Черга";
"Queue is empty" = "Черга порожня";
"Rate" = "Якість";
"Rate" = "Швидкість";
/* Video sort order in search */
"Rating" = "Рейтинг";
"Rating" = "По рейтингу";
"Recents" = "Недавні";
/* Video sort order in search */
"Relevance" = "Відповідність";
"Relevance" = "По релевантності";
"Remove" = "Видалити";
"Remove from Favorites" = "Видалити з улюблених";
"Remove from history" = "Відалити з історії";
"Remove from history" = "Видалити з історії";
"Remove from Playlist" = "Видалити з плейлиста";
"Remove from the queue" = "Видалити з черги";
"Replies" = "Відповіді";
"Thumbnails" = "Мініатюри";
/* Video date filter in search */
"Today" = "Сьогодні";
"Today" = "За сьогодні";
"Trending" = "Тренди";
/* Player controls layout size for TV */
@@ -121,16 +121,16 @@
"Typically near or at the end of the video when the credits pop up and/or endcards are shown." = "Як правило, в кінці відео, коли з'являються титри.";
"unknown" = "невідомо";
"Unsubscribe" = "Відписатися";
"Upload date" = "Загрузити дату";
"Upload date" = "Дата завантаження";
"URL" = "URL";
"Username" = "Імя користувача";
"Username" = "Ім'я користувача";
/* Player controls layout size */
"Very Large" = "Дуже великі";
"Videos" = "Відео";
/* Video sort order in search */
"Views" = "Перегляди";
"Views" = "По переглядам";
"Watched" = "Переглянуто";
/* Selected video was played on given date */
@@ -140,24 +140,24 @@
"Watching now" = "Дивлюся зараз";
/* Video date filter in search */
"Week" = "Тиждень";
"Welcome" = "Привіт";
"Week" = "За тиждень";
"Welcome" = "Ласкаво просимо";
"Wi-Fi" = "Wi-Fi";
"Wiki" = "Вікі";
"Yattee %@ (build %@)" = "Yattee %@ (збірка %@)";
"You have no Playlists" = "У вас немає плейлистів";
"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для доступу до %@";
"You need to create an instance and accounts\nto access %@ section" = "Необхідно створити інстанс та акаунти\nдля доступу до %@ розділу";
"You need to select an account\nto access %@ section" = "Потрібно вибрати акаунт\nдля доступу до %@ розділу";
"Public" = "Публічний";
"Unlisted" = "Прихований";
"Unlisted" = "За посиланням";
"Now Playing" = "Зараз грає";
"Current Location" = "Поточний API (локація)";
"Private" = "Видимість плейлиста";
"Private" = "Приватний";
"Playback queue is empty" = "Черга перегляду порожня";
"Playing Next" = "Далі";
"Playing Next" = "Грає далі";
"You can switch between profiles in playback settings controls." = "Ви можете змінювати профіль під час перегляду.";
"Make default" = "Зробити стандартним";
"Visibility" = "Видимість";
@@ -165,16 +165,16 @@
"Stream & Player" = "Трансляція і відеоплеєр";
"Hardware decoder" = "Апаратний декодер";
"Stream FPS" = "FPS трансляції";
"Rate & Captions" = "Якість і субтитри";
"Rate & Captions" = "Швидкість і субтитри";
"Translations" = "Переклади";
"Could not open playlist" = "Не вдалося відкрити плейлист";
"Could not extract video ID" = "Не вдалося отримати ID відео";
"Always use AVPlayer for live videos" = "Використовуйте AVPlayer для live відео";
"Always use AVPlayer for live videos" = "Завжди використовувати AVPlayer для прямих ефірів";
"Are you sure you want to clear history of watched videos?" = "Ви впевнені, що хочете очистити історію переглядів?";
"Country Name or Code" = "Назва або код країни";
/* Video sort order in search */
"Date" = "Дата";
"Date" = "По Даті";
"Discord Server" = "Discord сервер";
"Are you sure you want to unsubscribe from %@?" = "Ви впевнені, що хочете відписатися від %@?";
"Bugs and great feature ideas can be sent to the GitHub issues tracker. " = "Баги та ідеї можна надсилати на GitHub Issues. ";
@@ -182,32 +182,32 @@
"Done" = "Готово";
"Create Playlist" = "Створити плейлист";
"Current: %@\n%@" = "Зараз: %@\n%@";
"Custom Locations" = "Свої локації";
"Custom Locations" = "Користувальницькі локації";
"Copy %@ link with time" = "Копіювати %@ посилання з часом";
"Decrease rate" = "Знизити рейт";
"Decreased opacity" = "Зменшена непрозорість";
"Decrease rate" = "Знизити швидкість";
"Decreased opacity" = "Прозорістю";
"Delete" = "Видалити";
"Disabled" = "Вимкнено";
"Discussions take place in Discord and Matrix. It's a good spot for general questions." = "Дискусії відбуваються в Discord та Matrix. Це гарні місця для загальних питань.";
"Don't use public locations" = "Не використовуйте публічні локації";
"Don't use public locations" = "Не використовувати публічні локації";
"Donations" = "Донати";
"Duration" = "Тривалість";
"Gaming" = "Ігри";
"Fullscreen size" = "На весь екран";
"Fullscreen size" = "Повноекранний розмір";
"Help" = "Допомога";
"Related" = "Схожі";
"Honor orientation lock" = "Блокування орієнтації";
"If you are reporting a bug, include all relevant details (especially: app version, used device and system version, steps to reproduce)." = "Якщо ви повідомляєте про помилку, вказуйте всі відповідні деталі (особливо: версію застосунку, використовуваний пристрій, версію системи і кроки для відтворення).";
"Promoting a product or service that is directly related to the creator themselves. This usually includes merchandise or promotion of monetized platforms." = "Просування продукту або послуги, які безпосередньо пов'язані з самим автором. Зазвичай це стосується товарів або просування монетизованих платформ.";
"Honor orientation lock" = "Враховувати блокування орієнтації";
"If you are reporting a bug, include all relevant details (especially: app version, used device and system version, steps to reproduce)." = "Якщо ви повідомляєте про баг, вказуйте всі відповідні деталі (особливо: версію застосунку, використовуваний пристрій, версію системи і кроки для відтворення).";
"Promoting a product or service that is directly related to the creator themselves. This usually includes merchandise or promotion of monetized platforms." = "Просування продукту або послуги, які безпосередньо пов'язані з самим автором. Зазвичай це стосується товарів або просування підписочної платформи.";
"Red" = "Червоний";
"Refresh" = "Поновити";
"Refresh" = "Оновити";
"Regular size" = "Звичайний розмір";
"Regular Size" = "Звичайний розмір";
"Used to create links from videos, channels and playlists" = "Використовується для створення посилань на відео, канали ті плейлисти";
"Regular Size" = "Звичайний Розмір";
"Used to create links from videos, channels and playlists" = "Використовується для створення посилань на відео, канали та плейлисти";
/* Video date filter in search */
"Year" = "Рік";
"You can find information about using Yattee in the Wiki pages." = "Додаткову інформацію про Yattee можна знайти на Вікі-сторінках.";
"Year" = "За рік";
"You can find information about using Yattee in the Wiki pages." = "Додаткову інформацію про Yattee можна знайти на просторах Вікі.";
"You can use automatic profile selection based on current device status or switch it in video playback settings controls." = "Ви можете використовувати автоматичний вибір профілю якості на основі поточного стану пристрою або перемикати його в налаштуваннях відтворення відео.";
"When partially watched video is played" = "При відтворенні частково переглянутого відео";
"Yattee" = "Yattee";
@@ -215,11 +215,11 @@
"Statistics" = "Статистика";
"Could not create share link" = "Не вдалося створити посилання";
"Dropped frames" = "Втрата кадрів";
"Badge" = "Значок";
"Badge & Decreased opacity" = "Значок та зменшення непрозорості";
"Badge" = "Значком";
"Badge & Decreased opacity" = "Значком та прозорістю";
"Categories to Skip" = "Категорії для пропуску";
"Category" = "Категорія";
"Cellular" = "Сотовий звязок";
"Cellular" = "Сотовий зв'язок";
"Chapters" = "Розділи";
"Charging" = "Зарядка";
"Clear" = "Очистити";
@@ -229,20 +229,20 @@
"Clear Search History" = "Очистити історію пошуку";
"Clear Search History..." = "Очистити історію пошуку...";
"Clear the queue" = "Очистити чергу";
"Close PiP and open player when application enters foreground" = "Закрийте PiP і відкрийте плеєр, коли застосунок відкриється";
"Close player when closing video" = "Закрийте плеєр, коли зупините відео";
"Close player when starting PiP" = "Закрийте плеєр при старті PiP";
"Close PiP and open player when application enters foreground" = "Закрити К-в-К і відкрити плеєр, коли застосунок на передньому плані";
"Close player when closing video" = "Закрити плеєр при закритті відео";
"Close player when starting PiP" = "Закрити плеєр при старті К-в-К";
"Close Video" = "Закрити відео";
"Close video after playing last in the queue" = "Закрити відео після завершення останнього відео в черзі";
"Close video after playing last in the queue" = "Закрити відео після програвання черги";
"Comments" = "Коментарі";
"Contact" = "Контакт";
"Contact" = "Зворотній зв'язок";
"Error when accessing playlist" = "Помилка при доступі до плейлиста";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Прямі нагадування залишити лайк, підписатися або взаємодіяти з ними на будь-якій платній або безкоштовній платформі (наприклад, натиснути на відео).";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Прямі нагадування залишити лайк, підписатися або взаємодіяти з автором на будь-якій платній чи безкоштовній платформі (платформах) (наприклад, натиснути на відео).";
"Favorites" = "Улюблене";
"Filter" = "ФІльтер";
"Filter: active" = "Фільтер: активний";
"Filter" = "Фільтр";
"Filter: active" = "Фільтр: активний";
"Find Other" = "Найти інше";
"Finding something to play..." = "Знайти, у що пограти...";
"Finding something to play..." = "Шукаю що відтворити...";
/* SponsorBlock category name */
"Interaction" = "Взаємодія";
@@ -268,21 +268,21 @@
"Lowest" = "Найнища";
"Mark as watched" = "Позначити як переглянуте";
"Mark video as watched after playing" = "Помітити відео як переглянуте після перегляду";
"Mark watched videos with" = "Відмітити переглянуті відео з";
"Mark watched videos with" = "Відмітити переглянуті відео";
"Matrix Channel" = "Канал Matrix";
"Matrix Chat" = "Чат Matrix";
"Medium quality" = "Середня якість";
"Milestones" = "Етапи";
"Milestones" = "Віхи (Milestones)";
/* Video date filter in search */
"Month" = "Місяці";
"Month" = "За останній місяць";
"More info can be found in:" = "Більше інформації можна знайти за посиланням:";
"Movies" = "Фільми";
"MPV Documentation" = "Документація MPV";
"Music" = "Музика";
"Name" = "Імʼя";
"Name" = "Ім'я";
"New Playlist" = "Новий плейлист";
"Next" = "Наступне";
"Next" = "Далі";
"No description" = "Без опису";
"No Playlists" = "Нема плейлистів";
"No results" = "Без результатів";
@@ -292,19 +292,19 @@
"Nothing" = "Нічого";
/* SponsorBlock category name */
"Offtopic in Music Videos" = "Офтоп в музичних кліпах";
"Offtopic in Music Videos" = "Не по темі в музичних кліпах";
"Only when signed in" = "Тільки після авторизації";
"Open \"Playlists\" tab to create new one" = "Відкрийте розділ \"Плейлисти\" для створення нового";
"Open \"Playlists\" tab to create new one" = "Відкрити розділ \"Плейлисти\" для створення нового";
"Open Settings" = "Відрити налаштування";
/* Loading stream OSD */
"Opening %@ stream…" = "Запуск трансляції %@…";
"Opening %@ stream…" = "Запуск %@ ефіру…";
"Opening audio stream…" = "Запуск аудіо трансляції…";
"Orientation" = "Орієнтація";
/* SponsorBlock category name */
"Outro" = "Кінцівка";
"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." = "Частина відеоролика, що рекламує товар або послугу, не пов'язану безпосередньо з каналом. Власник каналу отримає оплату або безкоштовної продукції.";
"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." = "Частина відеоролика, що рекламує товар або послугу, не пов'язану безпосередньо з автором. Власник каналу отримає оплату або безкоштовну продукції.";
"Password" = "Пароль";
"Pause" = "Пауза";
"Pause when entering background" = "Пауза при переключенні на фоновий режим";
@@ -315,7 +315,7 @@
"Restart" = "Перезавантажити";
"Restart/Play next" = "Перезапустити/Відтворити далі";
"Restore default profiles..." = "Відновити стандартні профілі...";
"Rotate to portrait when exiting fullscreen" = "Перехід до портретного режиму при виході з полноекранного режиму";
"Rotate to portrait when exiting fullscreen" = "Перехід до портретного режиму при виході з повноекранного режиму";
"Round corners" = "Закруглення кутів";
"Save" = "Зберегти";
"Save history of played videos" = "Зберегти історію переглянутих відео";
@@ -326,24 +326,24 @@
"Sections" = "Розділи";
"Seek gesture sensitivity" = "Чутливість жестів";
"Seek gesture speed" = "Швидкість жестів";
"Seek with horizontal swipe on video" = "Пошук за допомогою горизонтального свайпу на відео";
"Seek with horizontal swipe on video" = "Прокручувати відео горизонтальним свайпом";
"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:" = "Виберіть найблищу локацію:";
/* SponsorBlock category name */
"Self-promotion" = "Самореклама";
"Settings" = "Налаштування";
"Share %@ link" = "Поділитися посиланням %@";
"Share %@ link" = "Поділитися %@ посиланням";
"Share..." = "Поділитися...";
/* Video duration filter in search */
"Short" = "Короткі";
"Show history" = "Показувати історію";
"Show keywords" = "Показувати ключові слова";
"Show playback statistics" = "Показувати статистику переглядів";
"Show progress of watching on thumbnails" = "Показувати прогрес перегляду на мініатюрі";
"Show sidebar when space permits" = "Показувати бокову панель, якщо вистачає простору";
"Show video length" = "Показувати тривалість відео";
"Show history" = "Відображати історію";
"Show keywords" = "Відображати ключові слова";
"Show playback statistics" = "Відображати статистику переглядів";
"Show progress of watching on thumbnails" = "Відображати прогрес перегляду на мініатюрі";
"Show sidebar when space permits" = "Відображати бокову панель, якщо вистачає простору";
"Show video length" = "Відображати тривалість відео";
"Shuffle" = "Вперемішку";
"Shuffle All" = "Перемішати все";
"Sidebar" = "Бокова панель";
@@ -360,23 +360,23 @@
/* SponsorBlock category name */
"Sponsor" = "Спонсор";
"SponsorBlock" = "SponsorBlock";
"SponsorBlock API Instance" = "Екземпляр API SponsorBlock";
"SponsorBlock API Instance" = "Інстанс SponsorBlock API";
"Subscribe" = "Підписатися";
/* Subscriptions title */
"Subscriptions" = "Підписки";
"Switch to other public location" = "Переключитися на іншу публічну локацію";
"Switch to public locations" = "Переключитися на публічні локації";
"System controls show buttons for %@" = "На елементах керування системою відображаються кнопки %@";
"System controls show buttons for %@" = "На системних елементах керування відображаються кнопки %@";
"This cannot be reverted" = "Це не можна буде скасувати";
"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." = "Це призведе до видалення всіх ваших користувацьких профілів і повернення їхніх значень до стандартних. Це не може бути скасовано.";
"This will remove all your custom profiles and return their default values. This cannot be reverted." = "Це призведе до видалення всіх ваших користувацьких профілів і повернення їхніх значень до стандартних. Це не можна буде скасувати.";
"Cached time" = "Час кешування";
"Any format" = "Будь-який формат";
"%@ formats" = "%@ формати";
"Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "Плейлист порожній\n\nНатисніть і утримуйте кнопку на відео, а потім натисніть на\n\"Додати до плейлиста\"";
"Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "Плейлист порожній\n\nНатисніть і утримуйте на відео, а потім \n\"Додати до плейлиста\"";
"Press and hold remote button to open captions and quality menus" = "Натисніть і утримуйте кнопку пульта, щоб відкрити меню субтитрів і якості";
"Comments are disabled" = "Коментування вимкнуто";
"Comments are disabled" = "Коментарі вимкнені";
"No comments" = "Коментарі відсутні";
"No chapters information available" = "Інформація про епізоди відсутня";
"Share Logs..." = "Поділитися логами…";
@@ -388,19 +388,19 @@
"Could not update your token." = "Не вдалося оновити ваш токен.";
"Could not refresh Trending" = "Не вдалося оновити тренди";
"For custom locations you can configure Frontend URL in Locations settings" = "Для користувацьких локацій ви можете налаштувати Frontend URL в налаштуваннях локацій";
"Connected successfully (%@)" = "Успішно підключено (%@)";
"Close PiP when player is opened" = "Закрийте PiP, поки плеєр відкритий";
"Close PiP when starting playing other video" = "Закрийте PiP, коли починаєте перегляд іншого відео";
"Connected successfully (%@)" = "Підключено успішно (%@)";
"Close PiP when player is opened" = "Закрити К-в-К, поки плеєр відкритий";
"Close PiP when starting playing other video" = "Закрити К-в-К, коли відтворюється інше відео";
"Connection failed" = "Помилка при підключенні";
"Enable logging" = "Ввімкнути запис логів";
"Enable logging" = "Увімкнути ведення журналу";
"Edit" = "Редагувати";
"Edit Playlist" = "Редагувати плейлист";
"Edit Quality Profile" = "Редагувати профіль якості";
"Edit..." = "Редагувати...";
"Error" = "Помилка";
"Large layout is not suitable for all devices and using it may cause controls not to fit on the screen." = "Великий розмір підходить не для всіх пристроїв і його використання може призвести до того, що елементи керування не будуть поміщатися на екрані.";
"Enable Return YouTube Dislike" = "Ввімкнути повернення дизлайку YouTube";
"Enter fullscreen in landscape" = "Увімкнути повноекранний режим в горизонтальному положенні";
"Enable Return YouTube Dislike" = "Увімкнути Return YouTube Dislike";
"Enter fullscreen in landscape" = "Увійти в повноекранний режим в альбомній орієнтації";
"Interface" = "Інтерфейс";
/* Video duration filter in search */
@@ -408,15 +408,15 @@
/* Player controls layout size */
"Medium" = "Середній";
"Share %@ link with time" = "Поділитися посиланням %@ з часом";
"Share %@ link with time" = "Поділитися %@ посиланням з часом";
"Reset watched status when playing again" = "Скинути статус перегляду при повторному відкритті";
"Show account username" = "Відображати імʼя акаунту";
"Restart the app to apply the settings above." = "Перезапустіть застосунок, щоб застосувати нові налаштування.";
"Show anonymous accounts" = "Показувати анонімні акаунти";
"Show channel name" = "Показувати назву каналу";
"Show account username" = "Відображати ім'я акаунту";
"Restart the app to apply the settings above." = "Перезапустіть програму, щоб застосувати нові налаштування.";
"Show anonymous accounts" = "Відображати анонімні акаунти";
"Show channel name" = "Відображати назву каналу";
"Sign In Required" = "Необхідно увійти в систему";
"It can be changed later in settings. You can use your own locations too." = "Пізніше це можна змінити в налаштуваннях. Ви також можете використовувати власні локації.";
"System controls buttons" = "Кнопки керування системою";
"System controls buttons" = "Системні кнопки керування";
"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." = "Це не можна буде скасувати. Можливо, вам доведеться перемкнутися між режимами перегляду або перезапустити застосунок, щоб побачити зміни.";
"Keep last played video in the queue after restart" = "Залишати останнє проглянуте відео в черзі після перезапуску";
@@ -429,18 +429,18 @@
"This video could not be opened" = "Не вдалося відкрити відео";
"Could not load video" = "Не вдалося завантажити відео";
"No locations available at the moment" = "Зараз немає доступних API (локацій)";
"Could not refresh Playlists" = "Не вдалося поновити плейлисти";
"Could not refresh Playlists" = "Не вдалося оновити плейлисти";
"If you want this app to be available in your language, join translation project." = "Ви можете змінити або створити свій переклад на сторінці проєкту.";
"No documents" = "Немає документів";
"Are you sure you want to remove this document?" = "Ви впевнені, що бажаєте видалити цей документ?";
"Recent Documents" = "Нещодавні документи";
"Recent History" = "Останнє з історії";
"Show Open Videos quick actions" = "Показати швидкі дії з відкритими відео";
"Show Favorites" = "Показати вибране";
"Show Favorites" = "Показати улюблене";
"Home" = "Головна";
"Share files from Finder on a Mac\nor iTunes on Windows" = "Обмін файлами з Finder на Mac\nабо iTunes на Windows";
"Show Home" = "Показати головну";
"URL to Open" = "Відкрити посилання";
"URL to Open" = "Посилання для відкриття";
"Enter link to open" = "Введіть посилання для відкриття";
"Video" = "Відео";
"Sample Rate" = "Частота дискретизації";
@@ -448,7 +448,7 @@
"Buttons labels" = "Написи на кнопках";
"Files" = "Файли";
"Video Details" = "Детальніше про відео";
"Show Inspector" = "Показати інспектор";
"Show Inspector" = "Показати інспектора";
"Reload manifest" = "Перезавантажити маніфест";
"Clear Queue before opening" = "Очищати чергу перед відкриттям";
"Open" = "Відкрити";
@@ -479,7 +479,7 @@
"Playback history is empty" = "Історія перегляду відсутня";
"Address" = "Адреса";
"Actions buttons" = "Кнопки дій";
"Show sidebar" = "Показ сайдбару";
"Show sidebar" = "Показати бокову панель";
"Locations Manifest" = "Маніфест локації";
"Remove Location" = "Видалити локацію";
"Open Video" = "Відкрити відео";
@@ -494,15 +494,15 @@
"Right" = "Праворуч";
"File" = "Файл";
"Show only icons" = "Показувати тільки іконки";
"Edit Favorites…" = "Редагувати обране…";
"Edit Favorites…" = "Редагувати улюблене…";
"\"%@\" will be irreversibly removed from this device." = "\"%@\" буде безповоротно видалено з цього пристрою.";
"Could not delete document" = "Не вдалося видалити документ";
"Are you sure you want to remove %@ location?" = "Ви впевнені, що хочете видалити локацію %@?";
"Are you sure you want to remove %@ location?" = "Ви впевнені, що хочете видалити %@ локацію?";
"Live Streams" = "Наживо";
"Shorts" = "Shorts";
"Verified" = "Перевірений";
"Channel" = "Канал";
"Open expanded" = "Відкрито розширено";
"Open expanded" = "Відкрити розширенним";
"Mark channel feed as unwatched" = "Позначити стрiчку каналу не переглянутою";
"Mark channel feed as watched" = "Позначити стрiчку каналу переглянутою";
"Short videos: visible" = "Короткi вiдео: виднi";
@@ -517,10 +517,10 @@
"Clear all" = "Зачистити все";
"Right click channel thumbnail to open context menu with more actions" = "Клацніть правою кнопкою миші на мініатюрі каналу, щоб відкрити контекстне меню з іншими діями";
"Show unwatched feed badges" = "Показати бейджики непроглянутих стрічок";
"Seeking" = "У пошуках";
"Seeking" = "Прокрутка";
"Gesture: fowards" = "Жест: вперед";
"Controls Buttons" = "Кнопки керування";
"System controls" = "Керування системою";
"System controls" = "Системні елементи керування";
"Controls button: backwards" = "Кнопка керування: назад";
"Controls button: forwards" = "Кнопка керування: вперед";
"Gesture: backwards" = "Жест: назад";
@@ -552,18 +552,80 @@
"Open channel" = "Відкрити канал";
"Inspector" = "Інспектор";
"Open video description expanded" = "Відкрити опис відео розширено";
"Mark all as unwatched" = "Позначити всі не переклянутими";
"Mark all as unwatched" = "Позначити всі не переглянутими";
"Mark all as watched" = "Позначити всі переглянутими";
"Queue - shuffled" = "Черга - перемішана";
"Playback Settings" = "Налаштування відтворення";
"Replay" = "Повтор";
"Fullscreen" = "Повний екран";
"Lock" = "Замок";
"Lock" = "Заблокувати";
"Description" = "Опис";
"Loop one" = "Цикл один";
"Loop one" = "Зациклити відео";
"Autoplay next" = "Автовідтворення далі";
"Stream" = "Потік";
"Enter account credentials to connect..." = "Введіть облікові дані для з'єднання...";
"Seek" = "Шукати";
"Seek" = "Перемотати відео вперед/назад";
"Show scroll to top button in comments" = "Показати кнопку прокрутки вгору в коментарях";
"Enter location address to connect..." = "Введіть адресу розташування для з'єднання...";
"Enter location address to connect..." = "Введіть адресу локації для з'єднання...";
"Import Settings..." = "Імпортувати налаштування...";
"Export Settings" = "Експортувати налаштування";
"Accounts passwords (unencrypted)" = "Паролі акаунтів (не зашифровані)";
"Other" = "Інше";
"Other data" = "Інші дані";
"Other data include last used playback preferences and listing options" = "Інші дані містять у собі налаштування останнього відтворення та параметри списку";
"File information" = "Інформація про файл";
"Build" = "Збірка";
"Import" = "Імпорт";
"Action button labels" = "Написи кнопок дій";
"Icon and text" = "Іконка і текст";
"Platform" = "Платформа";
"Password required to import" = "Для імпорту потрібен пароль";
"Custom Location not selected for import" = "Користувацька локація не обрана для імпорту";
"Custom Location already exists" = "Користувацька локація вже існує";
"Custom Location selected for import" = "Користувацька локація обрана для імпорту";
"Account already exists" = "Акаунт уже існує";
"Password saved in import file" = "Пароль збережений у файлі імпорту";
"Export in progress..." = "Експорт у процесі...";
"In progress..." = "У процесі…";
"Play Now in AVPlayer" = "Грати зараз в AVPlayer";
"Opening file…" = "Відкриваю файл…";
"Show channel avatars in videos lists" = "Показати аватари каналів у списках відео";
"Keep channels with unwatched videos on top of subscriptions list" = "Тримати канали з непроглянутими відео у верхній частині списку підписок";
"Show video context menu options to force selected backend" = "Показати опції контекстного меню відео для примусового вибору backend";
"Play Now in MPV" = "Грати зараз в MPV";
"Description preview" = "Попередній перегляд опису";
"No preview" = "Без попереднього перегляду";
"Open vertical chapters expanded" = "Відкривати вертикальні розділи розгорнутими";
"Chapters (if available)" = "Розділи (якщо доступні)";
"Opened File" = "Відкритий файл";
"File Extension" = "Розширення файлу";
"Close video and player on end" = "Закрити відео і плеєр в кінці";
"Public account" = "Публічний акаунт";
"Browse without account" = "Дивитися без акаунта";
"Landscape left" = "Альбомна орієнтація зліва";
"Landscape right" = "Альбомна орієнтація праворуч";
"Use system controls with AVPlayer" = "Використовувати системні елементи керування за допомогою AVPlayer";
"Your Accounts" = "Ваші акаунти";
"Rotate when entering fullscreen on landscape video" = "Повертати під час переходу в повноекранний режим в альбомній орієнтації";
"No rotation" = "Без поворотів";
"Show channel avatars in channels lists" = "Показувати аватари каналів у списках каналів";
"Export..." = "Експортувати…";
"Are you sure you want to export unencrypted passwords?" = "Ви впевнені, що хочете експортувати не зашифровані паролі?";
"Icon only" = "Тільки іконка";
"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" = "Експорт";
"Available" = "Доступно";
"Startup section" = "Початковий розділ";
"Home Settings" = "Налаштування головної";
"Watched: hidden" = "Переглянуті: сховані";
"Watched: visible" = "Переглянуті: показані";
"(watched and shorts hidden)" = "(переглянуті та shorts сховані)";
"Disable filters" = "Вимкнути фільтри";
"No videos to show" = "Немає відео для показу";
"(watched hidden)" = "(переглянуті сховані)";
"(shorts hidden)" = "(shorts сховані)";
"Limit" = "Ліміт";
"Are you sure you want to remove %@ from Favorites?" = "Ви впевнені, що хочете видалити %@ з улюбленого?";
"Podcasts" = "Подкасти";
"Releases" = "Релізи";
"Add %@" = "Додати %@";

View File

@@ -72,8 +72,6 @@
3703206627D2BB35007A0CB8 /* PINCache in Frameworks */ = {isa = PBXBuildFile; productRef = 3703206527D2BB35007A0CB8 /* PINCache */; };
3703206827D2BB45007A0CB8 /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 3703206727D2BB45007A0CB8 /* Defaults */; };
3703206A27D2BB49007A0CB8 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 3703206927D2BB49007A0CB8 /* Alamofire */; };
3704BDFF2ABF260C00370FF7 /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3704BDFE2ABF260C00370FF7 /* MPVKit */; };
3704BE012ABF263E00370FF7 /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3704BE002ABF263E00370FF7 /* MPVKit */; };
3705B180267B4DFB00704544 /* TrendingCountry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17F267B4DFB00704544 /* TrendingCountry.swift */; };
3705B182267B4E4900704544 /* TrendingCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B181267B4E4900704544 /* TrendingCategory.swift */; };
3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B181267B4E4900704544 /* TrendingCategory.swift */; };
@@ -1079,10 +1077,12 @@
E258F38A2BF61BD2005B8C28 /* URLTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = E258F3892BF61BD2005B8C28 /* URLTester.swift */; };
E258F38B2BF61BD2005B8C28 /* URLTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = E258F3892BF61BD2005B8C28 /* URLTester.swift */; };
E258F38C2BF61BD2005B8C28 /* URLTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = E258F3892BF61BD2005B8C28 /* URLTester.swift */; };
E265D0C22C7D217000D2BB8E /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = E265D0C12C7D217000D2BB8E /* MPVKit */; };
E265D0C42C7D218A00D2BB8E /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = E265D0C32C7D218A00D2BB8E /* MPVKit */; };
E265D0C62C7D21A300D2BB8E /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = E265D0C52C7D21A300D2BB8E /* MPVKit */; };
E27568B92BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */; };
E27568BA2BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */; };
E27568BB2BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */; };
FA97174C2A494700001FF53D /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = FA97174B2A494700001FF53D /* MPVKit */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -1597,9 +1597,9 @@
37BADCA52699FB72009BE4FB /* Alamofire in Frameworks */,
377FC7D5267A080300A6BBAF /* SwiftyJSON in Frameworks */,
3797104928D3D10600D5F53C /* SDWebImageSwiftUI in Frameworks */,
FA97174C2A494700001FF53D /* MPVKit in Frameworks */,
37BD07B92698AB2E003EBB87 /* Siesta in Frameworks */,
37FB284D2722099E00A57617 /* SDWebImageWebPCoder in Frameworks */,
E265D0C22C7D217000D2BB8E /* MPVKit in Frameworks */,
37CF8B8428535E4F00B71E37 /* SDWebImage in Frameworks */,
37C7367A2AC33010007630E1 /* SwiftUIIntrospect in Frameworks */,
);
@@ -1624,7 +1624,7 @@
375B8AB728B583BD00397B31 /* KeychainAccess in Frameworks */,
3703205E27D2BB12007A0CB8 /* SDWebImageWebPCoder in Frameworks */,
37CF8B8628535E5A00B71E37 /* SDWebImage in Frameworks */,
3704BDFF2ABF260C00370FF7 /* MPVKit in Frameworks */,
E265D0C42C7D218A00D2BB8E /* MPVKit in Frameworks */,
3703205C27D2BAF3007A0CB8 /* SwiftyJSON in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1670,7 +1670,7 @@
372915E42687E33E00F5A35B /* Defaults in Frameworks */,
3772003B27E8EEC800CB2475 /* libbz2.tbd in Frameworks */,
37BADCA9269A570B009BE4FB /* Alamofire in Frameworks */,
3704BE012ABF263E00370FF7 /* MPVKit in Frameworks */,
E265D0C62C7D21A300D2BB8E /* MPVKit in Frameworks */,
37D4B19D2671817900C925CA /* SwiftyJSON in Frameworks */,
3797757D268922D100DD52A8 /* Siesta in Frameworks */,
);
@@ -2584,8 +2584,8 @@
377F9F73294403770043F856 /* Cache */,
371AC0AB294D1A490085989E /* CachedAsyncImage */,
379325D429A265A300181CF1 /* Logging */,
FA97174B2A494700001FF53D /* MPVKit */,
37C736792AC33010007630E1 /* SwiftUIIntrospect */,
E265D0C12C7D217000D2BB8E /* MPVKit */,
);
productName = "Yattee (iOS)";
productReference = 37D4B0C92671614900C925CA /* Yattee.app */;
@@ -2623,8 +2623,8 @@
374D11E62943C56300CB4350 /* Cache */,
371AC0B1294D1C230085989E /* CachedAsyncImage */,
379325D629A265AE00181CF1 /* Logging */,
3704BDFE2ABF260C00370FF7 /* MPVKit */,
37C736772AC32B28007630E1 /* SwiftUIIntrospect */,
E265D0C32C7D218A00D2BB8E /* MPVKit */,
);
productName = "Yattee (macOS)";
productReference = 37D4B0CF2671614900C925CA /* Yattee.app */;
@@ -2702,7 +2702,7 @@
377F9F75294403880043F856 /* Cache */,
371AC0B3294D1C290085989E /* CachedAsyncImage */,
379325D829A265B500181CF1 /* Logging */,
3704BE002ABF263E00370FF7 /* MPVKit */,
E265D0C52C7D21A300D2BB8E /* MPVKit */,
);
productName = Yattee;
productReference = 37D4B158267164AE00C925CA /* Yattee.app */;
@@ -2734,7 +2734,7 @@
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1500;
LastUpgradeCheck = 1500;
LastUpgradeCheck = 1600;
TargetAttributes = {
37095E7E291DC85400301883 = {
CreatedOnToolsVersion = 14.1;
@@ -2822,7 +2822,7 @@
374D11E52943C56300CB4350 /* XCRemoteSwiftPackageReference "Cache" */,
371AC0AA294D1A490085989E /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */,
379325D329A265A300181CF1 /* XCRemoteSwiftPackageReference "swift-log" */,
FA97174A2A494700001FF53D /* XCRemoteSwiftPackageReference "MPVKit" */,
E265D0C02C7D217000D2BB8E /* XCRemoteSwiftPackageReference "MPVKit" */,
);
productRefGroup = 37D4B0CA2671614900C925CA /* Products */;
projectDirPath = "";
@@ -4103,7 +4103,7 @@
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 187;
CURRENT_PROJECT_VERSION = 193;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
@@ -4134,7 +4134,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 187;
CURRENT_PROJECT_VERSION = 193;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist";
@@ -4165,7 +4165,7 @@
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 187;
CURRENT_PROJECT_VERSION = 193;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 11.0;
@@ -4185,7 +4185,7 @@
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 187;
CURRENT_PROJECT_VERSION = 193;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 11.0;
@@ -4342,14 +4342,13 @@
37D4B0ED2671614900C925CA /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_CXX_LANGUAGE_STANDARD = "c++14";
CODE_SIGN_ENTITLEMENTS = "iOS/Yattee (iOS).entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 187;
CURRENT_PROJECT_VERSION = 193;
ENABLE_PREVIEWS = YES;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
@@ -4395,14 +4394,13 @@
37D4B0EE2671614900C925CA /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "iOS/Yattee (iOS).entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 187;
CURRENT_PROJECT_VERSION = 193;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
ENABLE_PREVIEWS = YES;
GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1";
@@ -4454,7 +4452,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 187;
CURRENT_PROJECT_VERSION = 193;
DEAD_CODE_STRIPPING = YES;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
@@ -4493,7 +4491,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 187;
CURRENT_PROJECT_VERSION = 193;
DEAD_CODE_STRIPPING = YES;
"DEVELOPMENT_TEAM[sdk=macosx*]" = 78Z5H3M6RJ;
ENABLE_APP_SANDBOX = YES;
@@ -4526,9 +4524,8 @@
37D4B0F32671614900C925CA /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 187;
CURRENT_PROJECT_VERSION = 193;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4550,9 +4547,8 @@
37D4B0F42671614900C925CA /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 187;
CURRENT_PROJECT_VERSION = 193;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4575,10 +4571,9 @@
37D4B0F62671614900C925CA /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 187;
CURRENT_PROJECT_VERSION = 193;
DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4600,10 +4595,9 @@
37D4B0F72671614900C925CA /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 187;
CURRENT_PROJECT_VERSION = 193;
DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4629,7 +4623,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 187;
CURRENT_PROJECT_VERSION = 193;
DEVELOPMENT_ASSET_PATHS = "";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -4669,7 +4663,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 187;
CURRENT_PROJECT_VERSION = 193;
DEVELOPMENT_ASSET_PATHS = "";
"DEVELOPMENT_TEAM[sdk=appletvos*]" = 78Z5H3M6RJ;
ENABLE_PREVIEWS = YES;
@@ -4708,9 +4702,8 @@
37D4B17E267164B000C925CA /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 187;
CURRENT_PROJECT_VERSION = 193;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -4732,9 +4725,8 @@
37D4B17F267164B000C925CA /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 187;
CURRENT_PROJECT_VERSION = 193;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -5040,12 +5032,12 @@
minimumVersion = 0.3.0;
};
};
FA97174A2A494700001FF53D /* XCRemoteSwiftPackageReference "MPVKit" */ = {
E265D0C02C7D217000D2BB8E /* XCRemoteSwiftPackageReference "MPVKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/cxfksword/MPVKit.git";
repositoryURL = "https://github.com/mpvkit/MPVKit.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.38.0;
kind = exactVersion;
version = "0.38.0-fix";
};
};
/* End XCRemoteSwiftPackageReference section */
@@ -5086,16 +5078,6 @@
package = 37BADCA32699FB72009BE4FB /* XCRemoteSwiftPackageReference "Alamofire" */;
productName = Alamofire;
};
3704BDFE2ABF260C00370FF7 /* MPVKit */ = {
isa = XCSwiftPackageProductDependency;
package = FA97174A2A494700001FF53D /* XCRemoteSwiftPackageReference "MPVKit" */;
productName = MPVKit;
};
3704BE002ABF263E00370FF7 /* MPVKit */ = {
isa = XCSwiftPackageProductDependency;
package = FA97174A2A494700001FF53D /* XCRemoteSwiftPackageReference "MPVKit" */;
productName = MPVKit;
};
371AC0AB294D1A490085989E /* CachedAsyncImage */ = {
isa = XCSwiftPackageProductDependency;
package = 371AC0AA294D1A490085989E /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */;
@@ -5331,9 +5313,19 @@
package = 37FB285227220D8400A57617 /* XCRemoteSwiftPackageReference "SDWebImagePINPlugin" */;
productName = SDWebImagePINPlugin;
};
FA97174B2A494700001FF53D /* MPVKit */ = {
E265D0C12C7D217000D2BB8E /* MPVKit */ = {
isa = XCSwiftPackageProductDependency;
package = FA97174A2A494700001FF53D /* XCRemoteSwiftPackageReference "MPVKit" */;
package = E265D0C02C7D217000D2BB8E /* XCRemoteSwiftPackageReference "MPVKit" */;
productName = MPVKit;
};
E265D0C32C7D218A00D2BB8E /* MPVKit */ = {
isa = XCSwiftPackageProductDependency;
package = E265D0C02C7D217000D2BB8E /* XCRemoteSwiftPackageReference "MPVKit" */;
productName = MPVKit;
};
E265D0C52C7D21A300D2BB8E /* MPVKit */ = {
isa = XCSwiftPackageProductDependency;
package = E265D0C02C7D217000D2BB8E /* XCRemoteSwiftPackageReference "MPVKit" */;
productName = MPVKit;
};
/* End XCSwiftPackageProductDependency section */

View File

@@ -1,5 +1,5 @@
{
"originHash" : "9899ef48b3ee49eae175e25421b8330438e40c30a266d96473b299a6ab7c4188",
"originHash" : "515d8e68c4a31658288fb3f94789ee539399b042082c08c39f4c03c27fd8860c",
"pins" : [
{
"identity" : "activelabel.swift",
@@ -25,7 +25,7 @@
"location" : "https://github.com/hyperoslo/Cache.git",
"state" : {
"branch" : "master",
"revision" : "dff9930c559aa2d1f7ed818d490d30c8852f57a6"
"revision" : "81a0277cbc6b63f4e0cd6f42c4abefa1011bbfa9"
}
},
{
@@ -58,10 +58,10 @@
{
"identity" : "mpvkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/cxfksword/MPVKit.git",
"location" : "https://github.com/mpvkit/MPVKit.git",
"state" : {
"revision" : "f646e4b625e9c8a2ff22a7e0bb5557306300be5d",
"version" : "0.38.0"
"revision" : "ee72059235566df8b455bff15e3f83a1c9053e78",
"version" : "0.38.0-fix"
}
},
{
@@ -105,8 +105,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImage",
"state" : {
"revision" : "be0bcd7823ce56629948491f2eaeaa19979514f7",
"version" : "5.19.4"
"revision" : "8a1be70a625683bc04d6903e2935bf23f3c6d609",
"version" : "5.19.7"
}
},
{

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1600"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1600"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1600"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1600"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1600"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -66,6 +66,11 @@
value = "Yes"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "IDELogRedirectionPolicy"
value = "oslogToStdio"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1600"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1600"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

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

View File

@@ -65,9 +65,11 @@ public class OrientationTracker {
guard newDeviceOrientation != self.currentDeviceOrientation else { return }
self.currentDeviceOrientation = newDeviceOrientation
NotificationCenter.default.post(name: Self.deviceOrientationChangedNotification,
object: nil,
userInfo: nil)
NotificationCenter.default.post(
name: Self.deviceOrientationChangedNotification,
object: nil,
userInfo: nil
)
}
}

View File

@@ -4,7 +4,7 @@ import SwiftUI
struct ShareSheet: UIViewControllerRepresentable {
typealias Callback = (_ activityType: UIActivity.ActivityType?,
_ completed: Bool,
_ returnedItems: [Any]?,
_ returnedItems: [Any],
_ error: Error?) -> Void
let activityItems: [Any]
@@ -19,7 +19,10 @@ struct ShareSheet: UIViewControllerRepresentable {
)
controller.excludedActivityTypes = excludedActivityTypes
controller.completionWithItemsHandler = callback
controller.completionWithItemsHandler = { activityType, completed, returnedItems, error in
callback?(activityType, completed, returnedItems ?? [], error)
}
return controller
}

View File

@@ -1,5 +1,5 @@
import Cocoa
import MPVKit
import Libmpv
import OpenGL.GL
import OpenGL.GL3
@@ -38,10 +38,12 @@ final class VideoLayer: CAOpenGLLayer {
glGetIntegerv(GLenum(GL_DRAW_FRAMEBUFFER_BINDING), &i)
if client.mpvGL != nil {
var data = mpv_opengl_fbo(fbo: Int32(i),
w: Int32(bounds.size.width),
h: Int32(bounds.size.height),
internal_format: 0)
var data = mpv_opengl_fbo(
fbo: Int32(i),
w: Int32(bounds.size.width),
h: Int32(bounds.size.height),
internal_format: 0
)
var params: [mpv_render_param] = [
mpv_render_param(type: MPV_RENDER_PARAM_OPENGL_FBO, data: &data),
mpv_render_param(type: MPV_RENDER_PARAM_FLIP_Y, data: &flip),
@@ -106,8 +108,10 @@ final class VideoLayer: CAOpenGLLayer {
let displayId = UInt32(NSScreen.main?.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as! Int)
CVDisplayLinkCreateWithCGDisplay(displayId, &client.link)
CVDisplayLinkSetOutputCallback(client.link!, displayLinkCallback,
UnsafeMutableRawPointer(Unmanaged.passUnretained(client.layer).toOpaque()))
CVDisplayLinkSetOutputCallback(
client.link!, displayLinkCallback,
UnsafeMutableRawPointer(Unmanaged.passUnretained(client.layer).toOpaque())
)
CVDisplayLinkStart(client.link!)
}

View File

@@ -11,6 +11,7 @@ struct NowPlayingView: View {
var inInfoViewController = false
@State private var repliesID: Comment.ID?
@State private var availableWidth = 0.0
@FetchRequest(sortDescriptors: [.init(key: "watchedAt", ascending: false)])
var watches: FetchedResults<Watch>
@@ -109,7 +110,7 @@ struct NowPlayingView: View {
} else {
Section {
ForEach(comments.all) { comment in
CommentView(comment: comment, repliesID: $repliesID)
CommentView(comment: comment, repliesID: $repliesID, availableWidth: availableWidth)
}
if comments.nextPageAvailable {
Text("Scroll to load more...")
@@ -120,6 +121,12 @@ struct NowPlayingView: View {
}
}
}
.background(GeometryReader { geometry in
Color.clear
.onAppear {
self.availableWidth = Double(geometry.size.width)
}
})
}
}