Compare commits

..

84 Commits

Author SHA1 Message Date
Arkadiusz Fal
af75afa912 Bump build number to 187 2024-07-06 13:51:40 +02:00
Arkadiusz Fal
f32bcae5eb Update CHANGELOG 2024-07-06 13:51:04 +02:00
Arkadiusz Fal
c55161b6b6 Fix typo 2024-07-06 13:44:40 +02:00
Arkadiusz Fal
3fd99f464a Revert change to OSD positions 2024-07-06 13:44:40 +02:00
Arkadiusz Fal
43c8484514 Allow import of accounts to manually added (not imported) instances 2024-07-06 13:44:40 +02:00
Arkadiusz Fal
7755b392b7 Fix seek OSD layout on tvOS
Fix #707
2024-07-06 12:51:07 +02:00
Arkadiusz Fal
e0998638b1 Fix macOS settings windows
Fix #699
Fix #710
2024-07-06 12:45:44 +02:00
Arkadiusz Fal
511a528eb6 Add import export of missing settings 2024-07-06 12:32:31 +02:00
Arkadiusz Fal
89dfbdb5c7 Fix swiftformat offenses 2024-07-06 11:48:49 +02:00
Arkadiusz Fal
f88a1d17d6 Update dependecies 2024-07-06 11:45:15 +02:00
Toni Förster
9e05909659 fix duplicated share button
fixes #691

add `--initial-audio-sync=<yes|no>` to MPV settings. Might fix #690 but needs to be toggled by the user. We leave the standard settings.

Also added links to the icons on macOS.
2024-07-06 11:35:21 +02:00
Arkadiusz Fal
b966f4509a Merge pull request #704 from patelhiren/subscriptions-account-picker
tvOS: Allow account picker by long pressing channels button in subscriptions view
2024-07-06 11:35:15 +02:00
Arkadiusz Fal
dc7073dcb5 Merge pull request #701 from weblate/weblate-yattee-localizable-strings
Translations update from Hosted Weblate
2024-07-06 11:28:49 +02:00
Hyunjae Chung
a258ee3be4 Translated using Weblate (Korean)
Currently translated at 11.2% (63 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ko/
2024-06-19 15:09:16 +02:00
Hiren Patel
b626f50adc - tvOS: Allow account picker by long pressing channels button in subscription view. 2024-06-16 23:21:49 -04:00
Pieter Janssens
78dc47dc24 Translated using Weblate (Dutch)
Currently translated at 80.7% (454 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/nl/
2024-06-14 19:09:21 +00:00
Arkadiusz Fal
46725bf4d9 Bump build number to 186 2024-06-13 19:12:19 +02:00
Arkadiusz Fal
8697ec8faf Update packages 2024-06-13 19:12:19 +02:00
Arkadiusz Fal
8a015d29c3 Update CHANGELOG 2024-06-13 19:12:19 +02:00
Arkadiusz Fal
4097d11b5e Fix build issues 2024-06-13 19:12:19 +02:00
Arkadiusz Fal
5323d53f9e Fix swiftformat offenses 2024-06-13 19:05:09 +02:00
Arkadiusz Fal
e3e0c4a92f Remove duplicated button 2024-06-13 19:04:46 +02:00
Arkadiusz Fal
9e5efc1aa6 Merge pull request #697 from patelhiren/main
tvOS: Refined Subscriptions View
2024-06-13 19:00:15 +02:00
Arkadiusz Fal
1ed4c20c3a Merge pull request #696 from stonerl/improved-conditional-proxying
improved conditional proxying
2024-06-13 18:59:42 +02:00
Arkadiusz Fal
ced9eb28d7 Merge pull request #695 from stonerl/more-async-work
more responsive UI when Favorites are used.
2024-06-13 18:59:30 +02:00
Arkadiusz Fal
ea49758ed2 Merge pull request #694 from weblate/weblate-yattee-localizable-strings
Translations update from Hosted Weblate
2024-06-13 18:58:58 +02:00
Hiren Patel
65ec675859 - Improved Subscription View for tvOS
- Refined Subscriptions view on tvOS

- Refined Subscriptions view on tvOS

- Refined Subscriptions view on tvOS

- Refined Subscriptions view on tvOS
2024-05-31 19:06:15 -04:00
Hyunjae Chung
9a650799d3 Translated using Weblate (Korean)
Currently translated at 6.4% (36 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ko/
2024-05-25 08:09:13 +02:00
Ophiushi
ddd1f243f7 Translated using Weblate (French)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/fr/
2024-05-25 08:09:12 +02:00
Toni Förster
94f19d55c8 more responsive UI when Favorites are used.
- The reloading of items in the favorite widgets is now done async, so it does not block the UI when opening Home.
- Tasks that check for changes of the widget and favorites are now terminated when another view e.g. Subscriptions is opened.
- We only update the Favourites when the player is not in the foreground, to avoid unresponsiveness.
2024-05-24 19:46:42 +02:00
Toni Förster
30cdaf88e1 improved conditional proxying
We don't test every single stream anymore. If we get an 200, 206 or 403 we immediately stop testing streams. Unknown streams are also skipped. This speeds up starting playback, since we don'T have to wait for the network anymore.
2024-05-24 18:44:41 +02:00
Hyunjae Chung
8139bba31e Added translation using Weblate (Korean) 2024-05-24 07:37:13 +02:00
Arkadiusz Fal
d6cfadab9a Bump build number to 185 2024-05-23 12:11:58 +02:00
Arkadiusz Fal
5b917ef91d Update CHANGELOG 2024-05-23 11:55:31 +02:00
Arkadiusz Fal
34cb7860b3 Fix build issues 2024-05-23 11:55:31 +02:00
Arkadiusz Fal
934bd65752 Fix swiftformat offenses 2024-05-23 11:44:58 +02:00
Arkadiusz Fal
e53985534e Update packages 2024-05-23 11:44:26 +02:00
Arkadiusz Fal
03e4c6d4e6 Merge pull request #689 from stonerl/more-mpv-settings
MPV: speed up playback start
2024-05-23 11:41:56 +02:00
Arkadiusz Fal
335e99cb7b Merge pull request #680 from stonerl/add-user-agent-to-header
Add User-Agent to request
2024-05-23 11:37:53 +02:00
Arkadiusz Fal
ae9aa6fac7 Merge branch 'main' into add-user-agent-to-header 2024-05-23 11:37:46 +02:00
Arkadiusz Fal
2f4fb9fc67 Merge pull request #684 from stonerl/better-caption-handling
Improved Captions handling
2024-05-23 11:37:03 +02:00
Arkadiusz Fal
f6bea6e045 Merge pull request #685 from stonerl/chapter-images-for-invidious
Invidious: add images to chapters
2024-05-23 11:35:51 +02:00
Arkadiusz Fal
fa712d8177 Merge pull request #688 from patelhiren/main
Fix thumbnails failing to load on tvOS
2024-05-23 11:34:19 +02:00
Arkadiusz Fal
03d24fbc42 Merge pull request #682 from stonerl/faster-chapter-extraction
faster chapter extraction
2024-05-23 11:34:07 +02:00
Arkadiusz Fal
4fd3a37705 Merge pull request #681 from stonerl/speed-up-sorting
speed up sorting for Stream
2024-05-23 11:33:51 +02:00
Arkadiusz Fal
a66857b1fb Merge pull request #683 from weblate/weblate-yattee-localizable-strings
Translations update from Hosted Weblate
2024-05-23 11:33:14 +02:00
Toni Förster
e44c7f84c8 MPV: speed up playback start
We initialize MPV with more options so it does not have to guess. This dramatically speeds up playback start.
2024-05-23 10:53:45 +02:00
Hiren Patel
6b5ecbdd8b - Fix thumbnails failing to load on tvOS
Thumbnails fail to load on tvOS when using SDImageAWebPCoder. Use SDImageWebPCoder on tvOS.
2024-05-21 22:58:14 -04:00
Toni Förster
15ce82a686 added en-GB to LanguageCodes 2024-05-20 21:53:48 +02:00
Toni Förster
7e3e393c65 Invidious: add images to chapters
Invidious, by design, has no images attached to chapters, in contrast to Piped.

Since the majority of videos with chapters don't have chapter-specific images and only use the videos' thumbnail, there is no difference here when compared to Piped's native thumbnail support.
2024-05-20 20:11:41 +02:00
Toni Förster
108b4de483 allow user to choose captions color 2024-05-20 17:50:47 +02:00
Toni Förster
7c9810ddf0 tvOS does not support WebKit 2024-05-20 16:03:13 +02:00
Toni Förster
96df7fdec5 let the user select caption size 2024-05-20 15:47:35 +02:00
Toni Förster
4fa5a15ad4 fallback language for captions 2024-05-20 14:42:24 +02:00
Toni Förster
c9125644ed improvements to captions on tvOS 2024-05-20 14:20:08 +02:00
Toni Förster
4db02b2638 Improved Captions handling
New options for captions in `Settings-Player`:

- Always show captions
- Default language

User can now select whether they want to show captions automatically when the video starts, and select the language.

Captions selector now shows proper name -> `English (en)` instead of only `en`
2024-05-20 03:50:51 +02:00
Mohammed Al Otaibi
9c5f066e55 Translated using Weblate (Arabic)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ar/
2024-05-19 23:01:50 +02:00
joaooliva
c7908d08ae Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/pt_BR/
2024-05-19 23:01:49 +02:00
Toni Förster
c9fb41c8e8 faster chapter extraction
The extraction of chapters is now faster since it is run in parallel for each pattern. Also a new pattern hast been added: "(start) title"
2024-05-19 17:43:35 +02:00
Toni Förster
2e9cceafa5 Add User-Agent to request
We generate a User-Agent for the platform we are running on and add it to the URLTester requests and also to MPV and AVPlayer
2024-05-19 13:46:14 +02:00
Toni Förster
fa09b2021c speed up sorting for Stream
This should help to start playback a bit faster.
2024-05-19 12:39:47 +02:00
Arkadiusz Fal
90777d91f6 Bump build number to 184 2024-05-19 11:52:38 +02:00
Arkadiusz Fal
6959778775 Update CHANGELOG 2024-05-19 11:52:38 +02:00
Arkadiusz Fal
0f43efef6f Fix build issue 2024-05-19 11:52:37 +02:00
Arkadiusz Fal
959fb0d1fc Merge pull request #676 from stonerl/fix-pip-broken-with-mpv
fix PiP Mode Not Working Using MPV
2024-05-19 09:51:38 +02:00
Arkadiusz Fal
81be57904b Merge pull request #679 from stonerl/mpv-settings
Advanced Settings: cache-pause-initial
2024-05-19 09:51:26 +02:00
Arkadiusz Fal
a42345896d Merge pull request #677 from stonerl/quality-reorderdering-iOS16
changed description for Format reordering
2024-05-19 09:51:10 +02:00
Toni Förster
43fc9e20c0 Advanced Settings: cache-pause-initial
`cache-pause-initial` status can now be selected by the user.

Also, on macOS and iOS, a link next to the option leads the user to the info on the mpv website.
2024-05-19 03:50:33 +02:00
Toni Förster
1a1bd1ba5b fix PiP Mode Not Working Using MPV
fixes #674

I accidentally broke PiP when using MPV. While testing this, I noticed that PiP sometimes does not start, so I tried to make MPV to PiP a bit more robust.
2024-05-19 00:58:52 +02:00
Toni Förster
99aca8e23c changed description for Format reordering
reordering Formats only works on iOS 16 and newer
2024-05-19 00:46:01 +02:00
Arkadiusz Fal
ddee3b74f0 Bump build number to 183 2024-05-18 11:55:59 +02:00
Arkadiusz Fal
b271aed52b Update CHANGELOG 2024-05-18 11:55:32 +02:00
Arkadiusz Fal
1c608c78a1 Update packages 2024-05-18 11:49:32 +02:00
Arkadiusz Fal
0ec227ba80 Fix application groups 2024-05-18 11:48:32 +02:00
Arkadiusz Fal
2a93ff52a3 Merge pull request #673 from weblate/weblate-yattee-localizable-strings
Translations update from Hosted Weblate
2024-05-18 11:42:26 +02:00
Arkadiusz Fal
896d46d0cf Merge pull request #662 from stonerl/piped-proxy
Conditional proxying
2024-05-18 11:42:11 +02:00
mere
ad79180530 Translated using Weblate (Romanian)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ro/
2024-05-18 11:42:04 +02:00
gallegonovato
101f20c538 Translated using Weblate (Spanish)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/es/
2024-05-18 11:42:03 +02:00
Arkadiusz Fal
f28cec79ba Merge pull request #672 from stonerl/favorites-view-tweaks
HomeView: Changes to Favourites and History Widget
2024-05-18 11:41:57 +02:00
Arkadiusz Fal
a12755ec4b Merge pull request #671 from stonerl/snappy-ui
Snappy UI - Offloading non UI task to background threads
2024-05-18 11:41:09 +02:00
Toni Förster
38c4ddbe43 HomeView: Changes to Favourites and History Widget
The History Widget in the Home View was hard-coded to 10 items. Now it uses the limit set in the settings.

The items weren't immediate updated when the limit was changed.

List Views had a hard-coded limit of 10 items. Now they use the limit supplied in the parameter.
2024-05-18 00:36:40 +02:00
Toni Förster
e35f8b7892 Snappy UI - Offloading non UI task to background threads
This gives a huge increase in perceived performance. The UI is now much more responsive since some tasks are run in the background and don't block the UI anymore.
2024-05-17 21:36:02 +02:00
Toni Förster
c3e4c074d6 Add HTTP Response StatusCode List and fix potential race condition 2024-05-17 19:30:24 +02:00
Toni Förster
6eba2a45c8 Conditional proxying
I added a new feature. When instances are not proxied, Yattee first checks the URL to make sure it is not a restricted video. Usually, music videos and sports content can only be played back by the same IP address that requested the URL in the first place. That is why some videos do not play when the proxy is disabled.

This approach has multiple advantages. First and foremost, It reduced the load on Invidious/Piped instances, since users can now directly access the videos without going through the instance, which might be severely bandwidth limited. Secondly, users don't need to manually turn on the proxy when they want to watch IP address bound content, since Yattee automatically proxies such content.

Furthermore, adding the proxy option allows mitigating some severe playback issues with invidious instances. Invidious by default returns proxied URLs for videos, and due to some bug in the Invidious proxy, scrubbing or continuing playback at a random timestamp can lead to severe wait times for the users.

This should fix numerous playback issues: #666, #626, #590, #585, #498, #457, #400
2024-05-16 19:35:31 +02:00
77 changed files with 2324 additions and 494 deletions

View File

@@ -1,4 +1,27 @@
## Build 181
## 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
## Previous builds
* Add skip, play/pause, and fullscreen shortcuts to macOS player (by @rickykresslein)
* Added Settings Import/Export
* Export all settings, instances and accounts
* Import selected elements from the file
* Include unencrypted passwords in the export or provide them during the import
* Import via URL for tvOS
* Added Controls setting "Action button labels" icon or icon and text
* Added Advanced setting for MPV: "deinterlace"
* Add help text to all header buttons (by @rickykresslein)
* 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)
* 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
* Don't show related in sidebar when disabled in settings by @stonerl in https://github.com/yattee/yattee/pull/635
* Handle audio session interrupts by other media by @stonerl in https://github.com/yattee/yattee/pull/640
* Only show Queue header in sidebar view by @stonerl in https://github.com/yattee/yattee/pull/642
@@ -18,26 +41,23 @@
* HLS: set target bitrate / AVPlayer: higher resolution by @stonerl in https://github.com/yattee/yattee/pull/667
* Fix #619: Remove ports from shared YouTube links by @0x000C in https://github.com/yattee/yattee/pull/627
* XCode enable IDEPreferLogStreaming by @stonerl in https://github.com/yattee/yattee/pull/638
* Updated localizations
* Upgraded dependencies
## Previous builds
* Add skip, play/pause, and fullscreen shortcuts to macOS player (by @rickykresslein)
* Added Settings Import/Export
* Export all settings, instances and accounts
* Import selected elements from the file
* Include unencrypted passwords in the export or provide them during the import
* Import via URL for tvOS
* Added Controls setting "Action button labels" icon or icon and text
* Added Advanced setting for MPV: "deinterlace"
* Add help text to all header buttons (by @rickykresslein)
* 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)
* Downgrade MPVKit to 0.36.0-1 due to issues with WebVTT subtitles
* Conditional proxying by @stonerl in https://github.com/yattee/yattee/pull/662
* HomeView: Changes to Favourites and History Widget by @stonerl in https://github.com/yattee/yattee/pull/672
* Snappy UI - Offloading non UI task to background threads by @stonerl in https://github.com/yattee/yattee/pull/671
* Fix PiP Mode Not Working Using MPV by @stonerl in https://github.com/yattee/yattee/pull/676
* Fix thumbnails failing to load on tvOS by @patelhiren in https://github.com/yattee/yattee/pull/688
* speed up sorting for Stream by @stonerl in https://github.com/yattee/yattee/pull/681
* faster chapter extraction by @stonerl in https://github.com/yattee/yattee/pull/682
* Invidious: add images to chapters by @stonerl in https://github.com/yattee/yattee/pull/685
* Improved Captions handling by @stonerl in https://github.com/yattee/yattee/pull/684
* Add User-Agent to request by @stonerl in https://github.com/yattee/yattee/pull/680
* MPV: speed up playback start by @stonerl in https://github.com/yattee/yattee/pull/689
* Advanced Settings: cache-pause-initial by @stonerl in https://github.com/yattee/yattee/pull/679
* Changed description for Format reordering by @stonerl in https://github.com/yattee/yattee/pull/677
* Add Chinese (Traditional) localization (by @rexcsk)
* Localization fixes
* Updated localizations
* Upgraded dependencies
* Fixed reported crash
* Other minor changes and improvements

View File

@@ -1,6 +1,6 @@
source "https://rubygems.org"
gem 'fastlane', git: 'https://github.com/nekrich/fastlane.git', branch: 'fix/match-tvos-devices-fetch'
gem 'fastlane'
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)

View File

@@ -1,50 +1,3 @@
GIT
remote: https://github.com/nekrich/fastlane.git
revision: d2d51a9af37f9b04a157e78fd25d147cecc89980
branch: fix/match-tvos-devices-fetch
specs:
fastlane (2.219.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored (~> 1.2)
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.0.0)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
naturally (~> 2.2)
optparse (>= 0.1.1, < 1.0.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (~> 3)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
GEM
remote: https://rubygems.org/
specs:
@@ -52,24 +5,24 @@ GEM
base64
nkf
rexml
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.929.0)
aws-sdk-core (3.196.1)
aws-partitions (1.952.0)
aws-sdk-core (3.201.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.81.0)
aws-sdk-core (~> 3, >= 3.193.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.151.0)
aws-sdk-core (~> 3, >= 3.194.0)
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-core (~> 3, >= 3.201.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
@@ -115,6 +68,47 @@ GEM
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.3.1)
fastlane (2.221.1)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored (~> 1.2)
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.0.0)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
naturally (~> 2.2)
optparse (>= 0.1.1, < 1.0.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.5)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (~> 3)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
@@ -153,14 +147,14 @@ GEM
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.5)
http-cookie (1.0.6)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.2)
json (2.7.2)
jwt (2.8.1)
jwt (2.8.2)
base64
mini_magick (4.12.0)
mini_magick (4.13.1)
mini_mime (1.1.5)
multi_json (1.15.0)
multipart-post (2.4.1)
@@ -170,19 +164,19 @@ GEM
optparse (0.5.0)
os (1.1.4)
plist (3.7.1)
public_suffix (5.0.5)
public_suffix (6.0.0)
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.8)
strscan (>= 3.0.9)
rexml (3.2.9)
strscan
rouge (2.0.7)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
security (0.1.5)
signet (0.19.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
@@ -222,7 +216,7 @@ PLATFORMS
x86_64-linux
DEPENDENCIES
fastlane!
fastlane
BUNDLED WITH
2.3.6

View File

@@ -502,7 +502,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
keywords: json["keywords"].arrayValue.compactMap { $0.string },
streams: extractStreams(from: json),
related: extractRelated(from: json),
chapters: extractChapters(from: description),
chapters: createChapters(from: description, thumbnails: json),
captions: extractCaptions(from: json)
)
}
@@ -575,6 +575,22 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
}
}
private func createChapters(from description: String, thumbnails: JSON) -> [Chapter] {
var chapters = extractChapters(from: description)
if !chapters.isEmpty {
let thumbnailsData = extractThumbnails(from: thumbnails)
let thumbnailURL = thumbnailsData.first { $0.quality == .medium }?.url
for chapter in chapters.indices {
if let url = thumbnailURL {
chapters[chapter].image = url
}
}
}
return chapters
}
private static var contentItemsKeys = ["items", "videos", "latestVideos", "playlists", "relatedChannels"]
private func extractChannelPage(from json: JSON, forceNotLast: Bool = false) -> ChannelPage {
@@ -655,7 +671,8 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
kind: .adaptive,
encoding: videoStream["encoding"].string,
videoFormat: videoStream["type"].string,
bitrate: videoStream["bitrate"].int
bitrate: videoStream["bitrate"].int,
requestRange: videoStream["init"].string ?? videoStream["index"].string
)
}
}

View File

@@ -415,6 +415,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
if let channel = extractChannel(from: content) {
return ContentItem(channel: channel)
}
default:
return nil
}
@@ -491,6 +492,35 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
)
}
static func nonProxiedAsset(asset: AVURLAsset, completion: @escaping (AVURLAsset?) -> Void) {
guard var urlComponents = URLComponents(url: asset.url, resolvingAgainstBaseURL: false) else {
completion(asset)
return
}
guard let hostItem = urlComponents.queryItems?.first(where: { $0.name == "host" }),
let hostValue = hostItem.value
else {
completion(asset)
return
}
urlComponents.host = hostValue
guard let newUrl = urlComponents.url else {
completion(asset)
return
}
completion(AVURLAsset(url: newUrl))
}
// Overload used for hlsURLS
static func nonProxiedAsset(url: URL, completion: @escaping (AVURLAsset?) -> Void) {
let asset = AVURLAsset(url: url)
nonProxiedAsset(asset: asset, completion: completion)
}
private func extractVideo(from content: JSON) -> Video? {
let details = content.dictionaryValue
@@ -579,10 +609,11 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
return nil
}
return URL(string: thumbnailURL
.absoluteString
.replacingOccurrences(of: "hqdefault", with: quality.filename)
.replacingOccurrences(of: "maxresdefault", with: quality.filename)
return URL(
string: thumbnailURL
.absoluteString
.replacingOccurrences(of: "hqdefault", with: quality.filename)
.replacingOccurrences(of: "maxresdefault", with: quality.filename)
)
}
@@ -688,6 +719,19 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
let resolution = Stream.Resolution.from(resolution: quality, fps: fps)
let videoFormat = videoStream.dictionaryValue["format"]?.string
let bitrate = videoStream.dictionaryValue["bitrate"]?.int
var requestRange: String?
if let initStart = videoStream.dictionaryValue["initStart"]?.int,
let initEnd = videoStream.dictionaryValue["initEnd"]?.int
{
requestRange = "\(initStart)-\(initEnd)"
} else if let indexStart = videoStream.dictionaryValue["indexStart"]?.int,
let indexEnd = videoStream.dictionaryValue["indexEnd"]?.int
{
requestRange = "\(indexStart)-\(indexEnd)"
} else {
requestRange = nil
}
if videoOnly {
streams.append(
@@ -698,7 +742,8 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
resolution: resolution,
kind: .adaptive,
videoFormat: videoFormat,
bitrate: bitrate
bitrate: bitrate,
requestRange: requestRange
)
)
} else {

View File

@@ -152,58 +152,94 @@ extension VideosAPI {
/*
The following chapter patterns are covered:
start - end - title / start - end: Title / start - end title
start - title / start: title / start title / [start] - title / [start]: title / [start] title
index. title - start / index. title start
title: (start)
1) "start - end - title" / "start - end: Title" / "start - end title"
2) "start - title" / "start: title" / "start title" / "[start] - title" / "[start]: title" / "[start] title"
3) "index. title - start" / "index. title start"
4) "title: (start)"
5) "(start) title"
The order is important!
These represent:
- "start" and "end" are timestamps, defining the start and end of the individual chapter
- "title" is the name of the chapter
- "index" is the chapter's position in a list
The order of these patterns is important as it determines the priority. The patterns listed first have a higher priority.
In the case of multiple matches, the pattern with the highest priority will be chosen - lower number means higher priority.
*/
let patterns = [
"(?<=\\n|^)\\s*(?:►\\s*)?\\[?(?<start>(?:[0-9]+:){1,2}[0-9]+)\\]?(?:\\s*-\\s*)?(?<end>(?:[0-9]+:){1,2}[0-9]+)?(?:\\s*-\\s*|\\s*[:]\\s*)?(?<title>.*)(?=\\n|$)",
"(?<=\\n|^)\\s*(?:►\\s*)?\\[?(?<start>(?:[0-9]+:){1,2}[0-9]+)\\]?\\s*[-:]?\\s*(?<title>.+)(?=\\n|$)",
"(?<=\\n|^)(?<index>[0-9]+\\.\\s)(?<title>.+?)(?:\\s*-\\s*)?(?<start>(?:[0-9]+:){1,2}[0-9]+)(?=\\n|$)",
"(?<=\\n|^)(?<title>.+?):\\s*\\((?<start>(?:[0-9]+:){1,2}[0-9]+)\\)(?=\\n|$)"
"(?<=\\n|^)(?<title>.+?):\\s*\\((?<start>(?:[0-9]+:){1,2}[0-9]+)\\)(?=\\n|$)",
"(?<=^|\\n)\\((?<start>(?:[0-9]+:){1,2}[0-9]+)\\)\\s*(?<title>.+?)(?=\\n|$)"
]
for pattern in patterns {
guard let chaptersRegularExpression = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) else { continue }
let chapterLines = chaptersRegularExpression.matches(in: description, range: NSRange(description.startIndex..., in: description))
let extractChaptersGroup = DispatchGroup()
var capturedChapters: [Int: [Chapter]] = [:]
let lock = NSLock()
if !chapterLines.isEmpty {
return chapterLines.compactMap { line in
let titleRange = line.range(withName: "title")
let startRange = line.range(withName: "start")
guard let titleSubstringRange = Range(titleRange, in: description),
let startSubstringRange = Range(startRange, in: description)
else {
return nil
}
let titleCapture = String(description[titleSubstringRange]).trimmingCharacters(in: .whitespaces)
let startCapture = String(description[startSubstringRange])
let startComponents = startCapture.components(separatedBy: ":")
guard startComponents.count <= 3 else { return nil }
for (index, pattern) in patterns.enumerated() {
extractChaptersGroup.enter()
DispatchQueue.global().async {
if let chaptersRegularExpression = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) {
let chapterLines = chaptersRegularExpression.matches(in: description, range: NSRange(description.startIndex..., in: description))
let extractedChapters = chapterLines.compactMap { line -> Chapter? in
let titleRange = line.range(withName: "title")
let startRange = line.range(withName: "start")
var hours: Double?
var minutes: Double?
var seconds: Double?
guard let titleSubstringRange = Range(titleRange, in: description),
let startSubstringRange = Range(startRange, in: description)
else {
return nil
}
if startComponents.count == 3 {
hours = Double(startComponents[0])
minutes = Double(startComponents[1])
seconds = Double(startComponents[2])
} else if startComponents.count == 2 {
minutes = Double(startComponents[0])
seconds = Double(startComponents[1])
let titleCapture = String(description[titleSubstringRange]).trimmingCharacters(in: .whitespaces)
let startCapture = String(description[startSubstringRange])
let startComponents = startCapture.components(separatedBy: ":")
guard startComponents.count <= 3 else { return nil }
var hours: Double?
var minutes: Double?
var seconds: Double?
if startComponents.count == 3 {
hours = Double(startComponents[0])
minutes = Double(startComponents[1])
seconds = Double(startComponents[2])
} else if startComponents.count == 2 {
minutes = Double(startComponents[0])
seconds = Double(startComponents[1])
}
guard var startSeconds = seconds else { return nil }
startSeconds += (minutes ?? 0) * 60
startSeconds += (hours ?? 0) * 60 * 60
return Chapter(title: titleCapture, start: startSeconds)
}
guard var startSeconds = seconds else { return nil }
startSeconds += (minutes ?? 0) * 60
startSeconds += (hours ?? 0) * 60 * 60
return .init(title: titleCapture, start: startSeconds)
if !extractedChapters.isEmpty {
lock.lock()
capturedChapters[index] = extractedChapters
lock.unlock()
}
}
extractChaptersGroup.leave()
}
}
extractChaptersGroup.wait()
// Now we sort the keys of the capturedChapters dictionary.
// These keys correspond to the priority of each pattern.
let sortedKeys = Array(capturedChapters.keys).sorted(by: <)
// Return first non-empty result in the order of patterns
for key in sortedKeys {
if let chapters = capturedChapters[key], !chapters.isEmpty {
return chapters
}
}
return []

View File

@@ -95,7 +95,7 @@ enum VideosApp: String, CaseIterable {
}
var allowsDisablingVidoesProxying: Bool {
self == .invidious
self == .invidious || self == .piped
}
var supportsOpeningVideosByID: Bool {

View File

@@ -9,7 +9,11 @@ final class AdvancedSettingsGroupExporter: SettingsGroupExporter {
"mpvEnableLogging": Defaults[.mpvEnableLogging],
"mpvCacheSecs": Defaults[.mpvCacheSecs],
"mpvCachePauseWait": Defaults[.mpvCachePauseWait],
"mpvCachePauseInital": Defaults[.mpvCachePauseInital],
"mpvDeinterlace": Defaults[.mpvDeinterlace],
"mpvHWdec": Defaults[.mpvHWdec],
"mpvDemuxerLavfProbeInfo": Defaults[.mpvDemuxerLavfProbeInfo],
"mpvInitialAudioSync": Defaults[.mpvInitialAudioSync],
"showCacheStatus": Defaults[.showCacheStatus],
"feedCacheSize": Defaults[.feedCacheSize]
]

View File

@@ -6,6 +6,9 @@ final class HistorySettingsGroupExporter: SettingsGroupExporter {
[
"saveRecents": Defaults[.saveRecents],
"saveHistory": Defaults[.saveHistory],
"showRecents": Defaults[.showRecents],
"limitRecents": Defaults[.limitRecents],
"limitRecentsAmount": Defaults[.limitRecentsAmount],
"showWatchingProgress": Defaults[.showWatchingProgress],
"saveLastPlayed": Defaults[.saveLastPlayed],
@@ -15,11 +18,7 @@ final class HistorySettingsGroupExporter: SettingsGroupExporter {
"watchedVideoStyle": Defaults[.watchedVideoStyle].rawValue,
"watchedVideoBadgeColor": Defaults[.watchedVideoBadgeColor].rawValue,
"showToggleWatchedStatusButton": Defaults[.showToggleWatchedStatusButton],
"showRecents": Defaults[.showRecents],
"limitRecents": Defaults[.limitRecents],
"limitRecentsAmount": Defaults[.limitRecentsAmount]
"showToggleWatchedStatusButton": Defaults[.showToggleWatchedStatusButton]
]
}
}

View File

@@ -10,6 +10,8 @@ final class PlayerSettingsGroupExporter: SettingsGroupExporter {
"expandVideoDescription": Defaults[.expandVideoDescription],
"collapsedLinesDescription": Defaults[.collapsedLinesDescription],
"showChapters": Defaults[.showChapters],
"showChapterThumbnails": Defaults[.showChapterThumbnails],
"showChapterThumbnailsOnlyWhenDifferent": Defaults[.showChapterThumbnailsOnlyWhenDifferent],
"expandChapters": Defaults[.expandChapters],
"showRelated": Defaults[.showRelated],
"showInspector": Defaults[.showInspector].rawValue,
@@ -18,7 +20,12 @@ final class PlayerSettingsGroupExporter: SettingsGroupExporter {
"enableReturnYouTubeDislike": Defaults[.enableReturnYouTubeDislike],
"closePiPOnNavigation": Defaults[.closePiPOnNavigation],
"closePiPOnOpeningPlayer": Defaults[.closePiPOnOpeningPlayer],
"closePlayerOnOpeningPiP": Defaults[.closePlayerOnOpeningPiP]
"closePlayerOnOpeningPiP": Defaults[.closePlayerOnOpeningPiP],
"captionsAutoShow": Defaults[.captionsAutoShow],
"captionsDefaultLanguageCode": Defaults[.captionsDefaultLanguageCode],
"captionsFallbackLanguageCode": Defaults[.captionsFallbackLanguageCode],
"captionsFontScaleSize": Defaults[.captionsFontScaleSize],
"captionsFontColor": Defaults[.captionsFontColor]
]
}

View File

@@ -5,7 +5,11 @@ final class SponsorBlockSettingsGroupExporter: SettingsGroupExporter {
override var globalJSON: JSON {
[
"sponsorBlockInstance": Defaults[.sponsorBlockInstance],
"sponsorBlockCategories": Array(Defaults[.sponsorBlockCategories])
"sponsorBlockCategories": Array(Defaults[.sponsorBlockCategories]),
"sponsorBlockColors": Defaults[.sponsorBlockColors],
"sponsorBlockShowTimeWithSkipsRemoved": Defaults[.sponsorBlockShowTimeWithSkipsRemoved],
"sponsorBlockShowCategoriesInTimeline": Defaults[.sponsorBlockShowCategoriesInTimeline],
"sponsorBlockShowNoticeAfterSkip": Defaults[.sponsorBlockShowNoticeAfterSkip]
]
}
}

View File

@@ -25,10 +25,26 @@ struct AdvancedSettingsGroupImporter {
Defaults[.mpvCachePauseWait] = mpvCachePauseWait
}
if let mpvCachePauseInital = json["mpvCachePauseInital"].bool {
Defaults[.mpvCachePauseInital] = mpvCachePauseInital
}
if let mpvDeinterlace = json["mpvDeinterlace"].bool {
Defaults[.mpvDeinterlace] = mpvDeinterlace
}
if let mpvHWdec = json["mpvHWdec"].string {
Defaults[.mpvHWdec] = mpvHWdec
}
if let mpvDemuxerLavfProbeInfo = json["mpvDemuxerLavfProbeInfo"].string {
Defaults[.mpvDemuxerLavfProbeInfo] = mpvDemuxerLavfProbeInfo
}
if let mpvInitialAudioSync = json["mpvInitialAudioSync"].bool {
Defaults[.mpvInitialAudioSync] = mpvInitialAudioSync
}
if let showCacheStatus = json["showCacheStatus"].bool {
Defaults[.showCacheStatus] = showCacheStatus
}

View File

@@ -13,6 +13,18 @@ struct HistorySettingsGroupImporter {
Defaults[.saveHistory] = saveHistory
}
if let showRecents = json["showRecents"].bool {
Defaults[.showRecents] = showRecents
}
if let limitRecents = json["limitRecents"].bool {
Defaults[.limitRecents] = limitRecents
}
if let limitRecentsAmount = json["limitRecentsAmount"].int {
Defaults[.limitRecentsAmount] = limitRecentsAmount
}
if let showWatchingProgress = json["showWatchingProgress"].bool {
Defaults[.showWatchingProgress] = showWatchingProgress
}
@@ -50,17 +62,5 @@ struct HistorySettingsGroupImporter {
if let showToggleWatchedStatusButton = json["showToggleWatchedStatusButton"].bool {
Defaults[.showToggleWatchedStatusButton] = showToggleWatchedStatusButton
}
if let showRecents = json["showRecents"].bool {
Defaults[.showRecents] = showRecents
}
if let limitRecents = json["limitRecents"].bool {
Defaults[.limitRecents] = limitRecents
}
if let limitRecentsAmount = json["limitRecentsAmount"].int {
Defaults[.limitRecentsAmount] = limitRecentsAmount
}
}
}

View File

@@ -68,7 +68,7 @@ struct LocationsSettingsGroupImporter {
if let password,
!password.isEmpty,
let instanceID = account.instanceID,
let instance = InstancesModel.shared.find(instanceID)
let instance = InstancesModel.shared.find(instanceID) ?? InstancesModel.shared.findByURLString(account.urlString)
{
if !instance.accounts.contains(where: { instanceAccount in
let (username, _) = instanceAccount.credentials

View File

@@ -29,6 +29,14 @@ struct PlayerSettingsGroupImporter {
Defaults[.showChapters] = showChapters
}
if let showChapterThumbnails = json["showChapterThumbnails"].bool {
Defaults[.showChapterThumbnails] = showChapterThumbnails
}
if let showChapterThumbnailsOnlyWhenDifferent = json["showChapterThumbnailsOnlyWhenDifferent"].bool {
Defaults[.showChapterThumbnailsOnlyWhenDifferent] = showChapterThumbnailsOnlyWhenDifferent
}
if let expandChapters = json["expandChapters"].bool {
Defaults[.expandChapters] = expandChapters
}
@@ -96,5 +104,25 @@ struct PlayerSettingsGroupImporter {
Defaults[.rotateToLandscapeOnEnterFullScreen] = rotateToLandscapeOnEnterFullScreen
}
#endif
if let captionsAutoShow = json["captionsAutoShow"].bool {
Defaults[.captionsAutoShow] = captionsAutoShow
}
if let captionsDefaultLanguageCode = json["captionsDefaultLanguageCode"].string {
Defaults[.captionsDefaultLanguageCode] = captionsDefaultLanguageCode
}
if let captionsFallbackLanguageCode = json["captionsFallbackLanguageCode"].string {
Defaults[.captionsFallbackLanguageCode] = captionsFallbackLanguageCode
}
if let captionsFontScaleSize = json["captionsFontScaleSize"].string {
Defaults[.captionsFontScaleSize] = captionsFontScaleSize
}
if let captionsFontColor = json["captionsFontColor"].string {
Defaults[.captionsFontColor] = captionsFontColor
}
}
}

View File

@@ -12,5 +12,22 @@ struct SponsorBlockSettingsGroupImporter {
if let sponsorBlockCategories = json["sponsorBlockCategories"].array {
Defaults[.sponsorBlockCategories] = Set(sponsorBlockCategories.compactMap { $0.string })
}
if let sponsorBlockColors = json["sponsorBlockColors"].dictionary {
let colors = sponsorBlockColors.mapValues { json in json.stringValue }
Defaults[.sponsorBlockColors] = colors
}
if let sponsorBlockShowTimeWithSkipsRemoved = json["sponsorBlockShowTimeWithSkipsRemoved"].bool {
Defaults[.sponsorBlockShowTimeWithSkipsRemoved] = sponsorBlockShowTimeWithSkipsRemoved
}
if let sponsorBlockShowCategoriesInTimeline = json["sponsorBlockShowCategoriesInTimeline"].bool {
Defaults[.sponsorBlockShowCategoriesInTimeline] = sponsorBlockShowCategoriesInTimeline
}
if let sponsorBlockShowNoticeAfterSkip = json["sponsorBlockShowNoticeAfterSkip"].bool {
Defaults[.sponsorBlockShowNoticeAfterSkip] = sponsorBlockShowNoticeAfterSkip
}
}
}

View File

@@ -26,7 +26,7 @@ final class NetworkStateModel: ObservableObject {
}
var bufferingStateText: String? {
guard detailsAvailable else { return nil }
guard detailsAvailable && player.hasStarted else { return nil }
return String(format: "%.0f%%", bufferingState)
}

View File

@@ -40,6 +40,11 @@ final class AVPlayerBackend: PlayerBackend {
var isLoadingVideo = false
var hasStarted = false
var isPaused: Bool {
avPlayer.timeControlStatus == .paused
}
var isPlaying: Bool {
avPlayer.timeControlStatus == .playing
}
@@ -158,6 +163,12 @@ final class AVPlayerBackend: PlayerBackend {
}
avPlayer.play()
// Setting hasStarted to true the first time player started
if !hasStarted {
hasStarted = true
}
model.objectWillChange.send()
}
@@ -180,6 +191,7 @@ final class AVPlayerBackend: PlayerBackend {
func stop() {
avPlayer.replaceCurrentItem(with: nil)
hasStarted = false
}
func cancelLoads() {
@@ -220,7 +232,10 @@ final class AVPlayerBackend: PlayerBackend {
upgrading: Bool = false
) {
asset?.cancelLoading()
asset = AVURLAsset(url: url)
asset = AVURLAsset(
url: url,
options: ["AVURLAssetHTTPHeaderFieldsKey": ["User-Agent": "\(UserAgentManager.shared.userAgent)"]]
)
asset?.loadValuesAsynchronously(forKeys: Self.assetKeysToLoad) { [weak self] in
var error: NSError?
switch self?.asset?.statusOfValue(forKey: "duration", error: &error) {
@@ -527,6 +542,7 @@ final class AVPlayerBackend: PlayerBackend {
}
}
}
case .failed:
DispatchQueue.main.async {
self.model.playerError = item.error

View File

@@ -10,7 +10,7 @@ import SwiftUI
final class MPVBackend: PlayerBackend {
static var timeUpdateInterval = 0.5
static var networkStateUpdateInterval = 1.0
static var networkStateUpdateInterval = 0.1
private var logger = Logger(label: "mpv-backend")
@@ -44,6 +44,8 @@ final class MPVBackend: PlayerBackend {
}
}}
var hasStarted = false
var isPaused = false
var isPlaying = true { didSet {
networkStateTimer.start()
@@ -215,9 +217,22 @@ final class MPVBackend: PlayerBackend {
#endif
var captions: Captions?
if let captionsLanguageCode = Defaults[.captionsLanguageCode] {
captions = video.captions.first { $0.code == captionsLanguageCode } ??
video.captions.first { $0.code.contains(captionsLanguageCode) }
if Defaults[.captionsAutoShow] == true {
let captionsDefaultLanguageCode = Defaults[.captionsDefaultLanguageCode],
captionsFallbackLanguageCode = Defaults[.captionsFallbackLanguageCode]
// Try to get captions with the default language code first
captions = video.captions.first { $0.code == captionsDefaultLanguageCode } ??
video.captions.first { $0.code.contains(captionsDefaultLanguageCode) }
// If there are still no captions, try to get captions with the fallback language code
if captions.isNil && !captionsFallbackLanguageCode.isEmpty {
captions = video.captions.first { $0.code == captionsFallbackLanguageCode } ??
video.captions.first { $0.code.contains(captionsFallbackLanguageCode) }
}
} else {
captions = nil
}
let updateCurrentStream = {
@@ -252,9 +267,8 @@ final class MPVBackend: PlayerBackend {
self.startClientUpdates()
// Captions should only be displayed when selected by the user,
// not when the video starts. So, we remove them.
self.client?.removeSubs()
if Defaults[.captionsAutoShow] { self.client?.setSubToAuto() } else { self.client?.setSubToNo() }
PlayerModel.shared.captions = self.captions
if !preservingTime,
!upgrading,
@@ -337,7 +351,6 @@ final class MPVBackend: PlayerBackend {
}
func play() {
isPlaying = true
startClientUpdates()
if controls.presentingControls {
@@ -354,13 +367,22 @@ final class MPVBackend: PlayerBackend {
}
client?.play()
isPlaying = true
isPaused = false
// Setting hasStarted to true the first time player started
if !hasStarted {
hasStarted = true
}
}
func pause() {
isPlaying = false
stopClientUpdates()
client?.pause()
isPaused = true
isPlaying = false
}
func togglePlay() {
@@ -377,6 +399,9 @@ final class MPVBackend: PlayerBackend {
func stop() {
client?.stop()
isPlaying = false
isPaused = false
hasStarted = false
}
func seek(to time: CMTime, seekType _: SeekType, completionHandler: ((Bool) -> Void)?) {
@@ -392,8 +417,8 @@ final class MPVBackend: PlayerBackend {
}
func closeItem() {
client?.pause()
client?.stop()
pause()
stop()
self.video = nil
self.stream = nil
}

View File

@@ -60,14 +60,44 @@ final class MPVClient: ObservableObject {
checkError(mpv_set_option_string(mpv, "input-media-keys", "yes"))
#endif
checkError(mpv_set_option_string(mpv, "cache-pause-initial", "yes"))
// CACHING //
checkError(mpv_set_option_string(mpv, "cache-pause-initial", Defaults[.mpvCachePauseInital] ? "yes" : "no"))
checkError(mpv_set_option_string(mpv, "cache-secs", Defaults[.mpvCacheSecs]))
checkError(mpv_set_option_string(mpv, "cache-pause-wait", Defaults[.mpvCachePauseWait]))
// PLAYBACK //
checkError(mpv_set_option_string(mpv, "keep-open", "yes"))
checkError(mpv_set_option_string(mpv, "hwdec", machine == "x86_64" ? "no" : "auto-safe"))
checkError(mpv_set_option_string(mpv, "vo", "libmpv"))
checkError(mpv_set_option_string(mpv, "demuxer-lavf-analyzeduration", "1"))
checkError(mpv_set_option_string(mpv, "deinterlace", Defaults[.mpvDeinterlace] ? "yes" : "no"))
checkError(mpv_set_option_string(mpv, "sub-scale", Defaults[.captionsFontScaleSize]))
checkError(mpv_set_option_string(mpv, "sub-color", Defaults[.captionsFontColor]))
checkError(mpv_set_option_string(mpv, "user-agent", UserAgentManager.shared.userAgent))
checkError(mpv_set_option_string(mpv, "initial-audio-sync", Defaults[.mpvInitialAudioSync] ? "yes" : "no"))
// GPU //
checkError(mpv_set_option_string(mpv, "hwdec", Defaults[.mpvHWdec]))
checkError(mpv_set_option_string(mpv, "vo", "libmpv"))
// We set set everything to OpenGL so MPV doesn't have to probe for other APIs.
checkError(mpv_set_option_string(mpv, "gpu-api", "opengl"))
checkError(mpv_set_option_string(mpv, "opengl-swapinterval", "0"))
#if !os(macOS)
checkError(mpv_set_option_string(mpv, "opengl-es", "yes"))
#endif
// We set this to ordered since we use OpenGL and Apple's implementation is ancient.
checkError(mpv_set_option_string(mpv, "dither", "ordered"))
// DEMUXER //
// We request to test for lavf first and skip probing other demuxer.
checkError(mpv_set_option_string(mpv, "demuxer", "lavf"))
checkError(mpv_set_option_string(mpv, "audio-demuxer", "lavf"))
checkError(mpv_set_option_string(mpv, "sub-demuxer", "lavf"))
checkError(mpv_set_option_string(mpv, "demuxer-lavf-analyzeduration", "1"))
checkError(mpv_set_option_string(mpv, "demuxer-lavf-probe-info", Defaults[.mpvDemuxerLavfProbeInfo]))
checkError(mpv_initialize(mpv))
@@ -405,6 +435,22 @@ final class MPVClient: ObservableObject {
setString("video", "no")
}
func setSubToAuto() {
setString("sub", "auto")
}
func setSubToNo() {
setString("sub", "no")
}
func setSubFontSize(scaleSize: String) {
setString("sub-scale", scaleSize)
}
func setSubFontColor(color: String) {
setString("sub-color", color)
}
var tracksCount: Int {
Int(getString("track-list/count") ?? "-1") ?? -1
}

View File

@@ -19,6 +19,8 @@ protocol PlayerBackend {
var loadedVideo: Bool { get }
var isLoadingVideo: Bool { get }
var hasStarted: Bool { get }
var isPaused: Bool { get }
var isPlaying: Bool { get }
var isSeeking: Bool { get }
var playerItemDuration: CMTime? { get }
@@ -131,23 +133,22 @@ extension PlayerBackend {
}
func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting, formatOrder: [QualityProfile.Format]) -> Stream? {
// filter out non HLS streams
let nonHLSStreams = streams.filter { $0.kind != .hls }
// filter out non-HLS streams and streams with resolution more than maxResolution
let nonHLSStreams = streams.filter {
$0.kind != .hls && $0.resolution <= maxResolution.value
}
// find max resolution from non HLS streams
let bestResolution = nonHLSStreams
.filter { $0.resolution <= maxResolution.value }
.max { $0.resolution < $1.resolution }
// 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 }
// finde max bitrate from non HLS streams
let bestBitrate = nonHLSStreams
.filter { $0.resolution <= maxResolution.value }
.max { $0.bitrate ?? 0 < $1.bitrate ?? 0 }
let bestResolution = bestResolutionStream?.resolution ?? maxResolution.value
let bestBitrate = bestBitrateStream?.bitrate ?? bestResolutionStream?.resolution.bitrate ?? maxResolution.value.bitrate
return streams.map { stream in
if stream.kind == .hls {
stream.resolution = bestResolution?.resolution ?? maxResolution.value
stream.bitrate = bestBitrate?.bitrate ?? (bestResolution?.resolution.bitrate ?? maxResolution.value.bitrate)
stream.resolution = bestResolution
stream.bitrate = bestBitrate
stream.format = .hls
} else if stream.kind == .stream {
stream.format = .stream

View File

@@ -298,6 +298,14 @@ final class PlayerModel: ObservableObject {
backend.isPlaying
}
var isPaused: Bool {
backend.isPaused
}
var hasStarted: Bool {
backend.hasStarted
}
var playerItemDuration: CMTime? {
guard !currentItem.isNil else {
return nil
@@ -675,10 +683,11 @@ final class PlayerModel: ObservableObject {
return
}
guard let video = currentVideo else { return }
guard let stream = backend.bestPlayable(availableStreams, maxResolution: .hd720p30, formatOrder: qualityProfile!.formats) else { return }
// First, we need to create an array with supported formats.
let formatOrderPiP: [QualityProfile.Format] = [.hls, .stream, .mp4]
exitFullScreen()
guard let video = currentVideo else { return }
guard let stream = avPlayerBackend.bestPlayable(availableStreams, maxResolution: .hd720p30, formatOrder: formatOrderPiP) else { return }
if avPlayerBackend.video == video {
if activeBackend != .appleAVPlayer {
@@ -690,7 +699,19 @@ final class PlayerModel: ObservableObject {
playStream(stream, of: video, preservingTime: true, upgrading: true, withBackend: avPlayerBackend)
}
controls.objectWillChange.send()
var retryCount = 0
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
if let pipController = self?.pipController, pipController.isPictureInPictureActive, self?.avPlayerBackend.isPlaying == true {
self?.exitFullScreen()
self?.controls.objectWillChange.send()
timer.invalidate()
} else if retryCount < 3, self?.activeBackend == .appleAVPlayer, self?.avPlayerBackend.startPictureInPictureOnSwitch == false {
// If PiP didn't start, try starting it again up to 3 times,
self?.avPlayerBackend.startPictureInPictureOnSwitch = true
self?.avPlayerBackend.tryStartingPictureInPicture()
retryCount += 1
}
}
}
var transitioningToPiP: Bool {
@@ -718,12 +739,19 @@ final class PlayerModel: ObservableObject {
show()
#endif
backend.closePiP()
if previousActiveBackend == .mpv {
saveTime {
self.changeActiveBackend(from: self.activeBackend, to: .mpv, isInClosePip: true)
self.controls.resetTimer()
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
if self?.activeBackend == .mpv, self?.mpvBackend.isPlaying == true {
self?.backend.closePiP()
self?.controls.resetTimer()
timer.invalidate()
}
}
}
} else {
backend.closePiP()
}
}

View File

@@ -74,7 +74,7 @@ extension PlayerModel {
preservedTime = currentItem.playbackTime
DispatchQueue.main.async { [weak self] in
guard let self else { return }
guard let self = self else { return }
guard let video = item.video else {
return
}
@@ -94,7 +94,9 @@ extension PlayerModel {
}
} else {
self.videoBeingOpened = nil
self.availableStreams = self.streamsWithInstance(instance: playerInstance, streams: video.streams)
self.streamsWithInstance(instance: playerInstance, streams: video.streams) { processedStreams in
self.availableStreams = processedStreams
}
}
}
}

View File

@@ -1,3 +1,4 @@
import AVFoundation
import Foundation
import Siesta
import SwiftUI
@@ -41,7 +42,9 @@ extension PlayerModel {
self.logger.info("ignoring loaded streams from \(instance.description) as current video has changed")
return
}
self.availableStreams = self.streamsWithInstance(instance: instance, streams: video.streams)
self.streamsWithInstance(instance: instance, streams: video.streams) { processedStreams in
self.availableStreams = processedStreams
}
} else {
self.logger.critical("no streams available from \(instance.description)")
}
@@ -53,28 +56,172 @@ extension PlayerModel {
}
}
func streamsWithInstance(instance: Instance, streams: [Stream]) -> [Stream] {
streams.map { stream in
stream.instance = instance
func streamsWithInstance(instance: Instance, streams: [Stream], completion: @escaping ([Stream]) -> Void) {
// Queue for stream processing
let streamProcessingQueue = DispatchQueue(label: "stream.yattee.streamProcessing.Queue")
// Queue for accessing the processedStreams array
let processedStreamsQueue = DispatchQueue(label: "stream.yattee.processedStreams.Queue")
// DispatchGroup for managing multiple tasks
let streamProcessingGroup = DispatchGroup()
if instance.app == .invidious, instance.proxiesVideos {
if let audio = stream.audioAsset {
stream.audioAsset = InvidiousAPI.proxiedAsset(instance: instance, asset: audio)
var processedStreams = [Stream]()
let instance = instance
var hasForbiddenAsset = false
var hasAllowedAsset = false
for stream in streams {
streamProcessingQueue.async(group: streamProcessingGroup) {
let forbiddenAssetTestGroup = DispatchGroup()
if !hasAllowedAsset, !hasForbiddenAsset, !instance.proxiesVideos, stream.format != Stream.Format.unknown {
let (nonHLSAssets, hlsURLs) = self.getAssets(from: [stream])
if let firstStream = nonHLSAssets.first {
let asset = firstStream.0
let url = firstStream.1
let requestRange = firstStream.2
if instance.app == .invidious {
self.testAsset(url: url, range: requestRange, isHLS: false, forbiddenAssetTestGroup: forbiddenAssetTestGroup) { status in
switch status {
case HTTPStatus.Forbidden:
hasForbiddenAsset = true
case HTTPStatus.PartialContent:
hasAllowedAsset = true
case HTTPStatus.OK:
hasAllowedAsset = true
default:
break
}
}
} else if instance.app == .piped {
self.testPipedAssets(asset: asset!, requestRange: requestRange, isHLS: false, forbiddenAssetTestGroup: forbiddenAssetTestGroup) { status in
switch status {
case HTTPStatus.Forbidden:
hasForbiddenAsset = true
case HTTPStatus.PartialContent:
hasAllowedAsset = true
case HTTPStatus.OK:
hasAllowedAsset = true
default:
break
}
}
}
} else if let firstHLS = hlsURLs.first {
let asset = AVURLAsset(url: firstHLS)
if instance.app == .piped {
self.testPipedAssets(asset: asset, requestRange: nil, isHLS: true, forbiddenAssetTestGroup: forbiddenAssetTestGroup) { status in
switch status {
case HTTPStatus.Forbidden:
hasForbiddenAsset = true
case HTTPStatus.PartialContent:
hasAllowedAsset = true
case HTTPStatus.OK:
hasAllowedAsset = true
default:
break
}
}
}
}
}
if let video = stream.videoAsset {
stream.videoAsset = InvidiousAPI.proxiedAsset(instance: instance, asset: video)
forbiddenAssetTestGroup.wait()
// Post-processing code
if instance.app == .invidious, hasForbiddenAsset || instance.proxiesVideos {
if let audio = stream.audioAsset {
stream.audioAsset = InvidiousAPI.proxiedAsset(instance: instance, asset: audio)
}
if let video = stream.videoAsset {
stream.videoAsset = InvidiousAPI.proxiedAsset(instance: instance, asset: video)
}
} else if instance.app == .piped, !instance.proxiesVideos, !hasForbiddenAsset {
if let hlsURL = stream.hlsURL {
PipedAPI.nonProxiedAsset(url: hlsURL) { possibleNonProxiedURL in
if let nonProxiedURL = possibleNonProxiedURL {
stream.hlsURL = nonProxiedURL.url
}
}
} else {
if let audio = stream.audioAsset {
PipedAPI.nonProxiedAsset(asset: audio) { nonProxiedAudioAsset in
stream.audioAsset = nonProxiedAudioAsset
}
}
if let video = stream.videoAsset {
PipedAPI.nonProxiedAsset(asset: video) { nonProxiedVideoAsset in
stream.videoAsset = nonProxiedVideoAsset
}
}
}
}
// Append to processedStreams within the processedStreamsQueue
processedStreamsQueue.sync {
processedStreams.append(stream)
}
}
}
return stream
streamProcessingGroup.notify(queue: .main) {
// Access and pass processedStreams within the processedStreamsQueue block
processedStreamsQueue.sync {
completion(processedStreams)
}
}
}
func streamsSorter(_ lhs: Stream, _ rhs: Stream) -> Bool {
if lhs.resolution.isNil || rhs.resolution.isNil {
private func getAssets(from streams: [Stream]) -> (nonHLSAssets: [(AVURLAsset?, URL, String?)], hlsURLs: [URL]) {
var nonHLSAssets = [(AVURLAsset?, URL, String?)]()
var hlsURLs = [URL]()
for stream in streams {
if stream.isHLS {
if let url = stream.hlsURL?.url {
hlsURLs.append(url)
}
} else {
if let asset = stream.audioAsset {
nonHLSAssets.append((asset, asset.url, stream.requestRange))
}
if let asset = stream.videoAsset {
nonHLSAssets.append((asset, asset.url, stream.requestRange))
}
}
}
return (nonHLSAssets, hlsURLs)
}
private func testAsset(url: URL, range: String?, isHLS: Bool, forbiddenAssetTestGroup: DispatchGroup, completion: @escaping (Int) -> Void) {
// In case the range is nil, generate a random one.
let randomEnd = Int.random(in: 200 ... 800)
let requestRange = range ?? "0-\(randomEnd)"
forbiddenAssetTestGroup.enter()
URLTester.testURLResponse(url: url, range: requestRange, isHLS: isHLS) { statusCode in
completion(statusCode)
forbiddenAssetTestGroup.leave()
}
}
private func testPipedAssets(asset: AVURLAsset, requestRange: String?, isHLS: Bool, forbiddenAssetTestGroup: DispatchGroup, completion: @escaping (Int) -> Void) {
PipedAPI.nonProxiedAsset(asset: asset) { possibleNonProxiedAsset in
if let nonProxiedAsset = possibleNonProxiedAsset {
self.testAsset(url: nonProxiedAsset.url, range: requestRange, isHLS: isHLS, forbiddenAssetTestGroup: forbiddenAssetTestGroup, completion: completion)
} else {
completion(0)
}
}
}
func streamsSorter(lhs: Stream, rhs: Stream) -> Bool {
// Use optional chaining to simplify nil handling
guard let lhsRes = lhs.resolution?.height, let rhsRes = rhs.resolution?.height else {
return lhs.kind < rhs.kind
}
return lhs.kind == rhs.kind ? (lhs.resolution.height > rhs.resolution.height) : (lhs.kind < rhs.kind)
// Compare either kind or resolution based on conditions
return lhs.kind == rhs.kind ? (lhsRes > rhsRes) : (lhs.kind < rhs.kind)
}
}

View File

@@ -170,6 +170,7 @@ class Stream: Equatable, Hashable, Identifiable {
var encoding: String?
var videoFormat: String?
var bitrate: Int?
var requestRange: String?
init(
instance: Instance? = nil,
@@ -181,7 +182,8 @@ class Stream: Equatable, Hashable, Identifiable {
kind: Kind = .hls,
encoding: String? = nil,
videoFormat: String? = nil,
bitrate: Int? = nil
bitrate: Int? = nil,
requestRange: String? = nil
) {
self.instance = instance
self.audioAsset = audioAsset
@@ -193,6 +195,7 @@ class Stream: Equatable, Hashable, Identifiable {
self.encoding = encoding
format = .from(videoFormat ?? "")
self.bitrate = bitrate
self.requestRange = requestRange
}
var isLocal: Bool {

View File

@@ -103,9 +103,8 @@ enum Constants {
#elseif os(iOS)
if isIPad {
return .sidebar
} else {
return .tab
}
return .tab
#else
return .tab
#endif

View File

@@ -106,6 +106,12 @@ extension Defaults.Keys {
static let closePiPAndOpenPlayerOnEnteringForeground = Key<Bool>("closePiPAndOpenPlayerOnEnteringForeground", default: false)
#endif
static let captionsAutoShow = Key<Bool>("captionsAutoShow", default: false)
static let captionsDefaultLanguageCode = Key<String>("captionsDefaultLanguageCode", default: LanguageCodes.English.rawValue)
static let captionsFallbackLanguageCode = Key<String>("captionsDefaultFallbackCode", default: LanguageCodes.English.rawValue)
static let captionsFontScaleSize = Key<String>("captionsFontScale", default: "1.0")
static let captionsFontColor = Key<String>("captionsFontColor", default: "#FFFFFF")
// MARK: GROUP - Controls
static let avPlayerUsesSystemControls = Key<Bool>("avPlayerUsesSystemControls", default: true)
@@ -170,7 +176,7 @@ extension Defaults.Keys {
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: .hd720p60, formats: [.hls, .stream], 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))
#if os(iOS)
@@ -266,7 +272,11 @@ extension Defaults.Keys {
static let mpvEnableLogging = Key<Bool>("mpvEnableLogging", default: false)
static let mpvCacheSecs = Key<String>("mpvCacheSecs", default: "120")
static let mpvCachePauseWait = Key<String>("mpvCachePauseWait", default: "3")
static let mpvCachePauseInital = Key<Bool>("mpvCachePauseInitial", default: false)
static let mpvDeinterlace = Key<Bool>("mpvDeinterlace", default: false)
static let mpvHWdec = Key<String>("hwdec", default: "auto-safe")
static let mpvDemuxerLavfProbeInfo = Key<String>("mpvDemuxerLavfProbeInfo", default: "no")
static let mpvInitialAudioSync = Key<Bool>("mpvInitialAudioSync", default: true)
static let showCacheStatus = Key<Bool>("showCacheStatus", default: false)
static let feedCacheSize = Key<String>("feedCacheSize", default: "50")
@@ -301,7 +311,6 @@ extension Defaults.Keys {
static let activeBackend = Key<PlayerBackendType>("activeBackend", default: .mpv)
static let captionsLanguageCode = Key<String?>("captionsLanguageCode")
static let lastUsedPlaylistID = Key<Playlist.ID?>("lastPlaylistID")
static let lastAccountIsPublic = Key<Bool>("lastAccountIsPublic", default: false)

81
Shared/HTTPStatus.swift Normal file
View File

@@ -0,0 +1,81 @@
// HTTP response status codes
enum HTTPStatus {
// Informational responses (100 - 199)
static let Continue = 100
static let SwitchingProtocols = 101
static let Processing = 102
static let EarlyHints = 103
// Successful responses (200 - 299)
static let OK = 200
static let Created = 201
static let Accepted = 202
static let NonAuthoritativeInformation = 203
static let NoContent = 204
static let ResetContent = 205
static let PartialContent = 206
static let MultiStatus = 207
static let AlreadyReported = 208
static let IMUsed = 226
// Redirection messages (300 - 399)
static let MultipleChoices = 300
static let MovedPermanently = 301
static let Found = 302
static let SeeOther = 303
static let NotModified = 304
static let UseProxy = 305
static let SwitchProxy = 306
static let TemporaryRedirect = 307
static let PermanentRedirect = 308
// Client error responses (400 - 499)
static let BadRequest = 400
static let Unauthorized = 401
static let PaymentRequired = 402
static let Forbidden = 403
static let NotFound = 404
static let MethodNotAllowed = 405
static let NotAcceptable = 406
static let ProxyAuthenticationRequired = 407
static let RequestTimeout = 408
static let Conflict = 409
static let Gone = 410
static let LengthRequired = 411
static let PreconditionFailed = 412
static let PayloadTooLarge = 413
static let URITooLong = 414
static let UnsupportedMediaType = 415
static let RangeNotSatisfiable = 416
static let ExpectationFailed = 417
static let IAmATeapot = 418
static let MisdirectedRequest = 421
static let UnprocessableEntity = 422
static let Locked = 423
static let FailedDependency = 424
static let TooEarly = 425
static let UpgradeRequired = 426
static let PreconditionRequired = 428
static let TooManyRequests = 429
static let RequestHeaderFieldsTooLarge = 431
static let UnavailableForLegalReasons = 451
// Server error responses (500 - 599)
static let InternalServerError = 500
static let NotImplemented = 501
static let BadGateway = 502
static let ServiceUnavailable = 503
static let GatewayTimeout = 504
static let HTTPVersionNotSupported = 505
static let VariantAlsoNegotiates = 506
static let InsufficientStorage = 507
static let LoopDetected = 508
static let NotExtended = 510
static let NetworkAuthenticationRequired = 511
}

View File

@@ -5,6 +5,7 @@ import UniformTypeIdentifiers
struct FavoriteItemView: View {
var item: FavoriteItem
@Binding var favoritesChanged: Bool
@Environment(\.navigationStyle) private var navigationStyle
@StateObject private var store = FavoriteResourceObserver()
@@ -25,8 +26,9 @@ struct FavoriteItemView: View {
@Default(.widgetsSettings) private var widgetsSettings
@Default(.visibleSections) private var visibleSections
init(item: FavoriteItem) {
init(item: FavoriteItem, favoritesChanged: Binding<Bool>) {
self.item = item
_favoritesChanged = favoritesChanged
}
var body: some View {
@@ -86,21 +88,36 @@ struct FavoriteItemView: View {
reloadVisibleWatches()
} else {
resource?.addObserver(store)
loadCacheAndResource()
DispatchQueue.main.async {
self.loadCacheAndResource()
}
}
}
.onDisappear {
resource?.removeObservers(ownedBy: store)
}
.onChange(of: player.currentVideo) { _ in if !player.presentingPlayer { reloadVisibleWatches() } }
.onChange(of: hideShorts) { _ in if !player.presentingPlayer { reloadVisibleWatches() } }
.onChange(of: hideWatched) { _ in if !player.presentingPlayer { reloadVisibleWatches() } }
// Delay is necessary to update the list with the new items.
.onChange(of: favoritesChanged) { _ in if !player.presentingPlayer { Delay.by(1.0) { reloadVisibleWatches() } } }
.onChange(of: player.presentingPlayer) { _ in
if player.presentingPlayer {
resource?.removeObservers(ownedBy: store)
} else {
resource?.addObserver(store)
}
}
.onChange(of: player.currentVideo) { _ in reloadVisibleWatches() }
.onChange(of: hideShorts) { _ in reloadVisibleWatches() }
.onChange(of: hideWatched) { _ in reloadVisibleWatches() }
}
}
.id(watchModel.historyToken)
.onChange(of: accounts.current) { _ in
resource?.addObserver(store)
loadCacheAndResource(force: true)
DispatchQueue.main.async {
loadCacheAndResource(force: true)
}
}
.onChange(of: watchModel.historyToken) { _ in
Delay.by(0.5) {
if !player.presentingPlayer {
reloadVisibleWatches()
}
}
@@ -154,22 +171,27 @@ struct FavoriteItemView: View {
}
func reloadVisibleWatches() {
guard item.section == .history else { return }
DispatchQueue.main.async {
guard item.section == .history else { return }
visibleWatches = []
visibleWatches = []
let watches = Array(
watches
.filter { $0.videoID != player.currentVideo?.videoID && itemVisible(.init(video: $0.video)) }
.prefix(favoritesModel.limit(item))
)
let last = watches.last
for watch in watches {
player.loadHistoryVideoDetails(watch) {
guard let video = player.historyVideo(watch.videoID), itemVisible(.init(video: video)) else { return }
visibleWatches.append(watch)
guard watch == last else { return }
visibleWatches.sort { $0.watchedAt ?? Date() > $1.watchedAt ?? Date() }
let watches = Array(
watches
.filter { $0.videoID != player.currentVideo?.videoID && itemVisible(.init(video: $0.video)) }
.prefix(favoritesModel.limit(item))
)
let last = watches.last
for watch in watches {
player.loadHistoryVideoDetails(watch) {
guard let video = player.historyVideo(watch.videoID), itemVisible(.init(video: video)) else { return }
visibleWatches.append(watch)
if watch == last {
visibleWatches.sort { $0.watchedAt ?? Date() > $1.watchedAt ?? Date() }
}
}
}
}
}
@@ -219,6 +241,9 @@ struct FavoriteItemView: View {
onSuccess = { response in
if let videos: [Video] = response.typedContent() {
FeedCacheModel.shared.storeFeed(account: accounts.current, videos: videos)
DispatchQueue.main.async {
store.contentItems = contentItems
}
}
}
case let .channel(_, id, name):
@@ -231,19 +256,21 @@ struct FavoriteItemView: View {
}
onSuccess = { response in
if let channel: Channel = response.typedContent() {
ChannelsCacheModel.shared.store(channel)
store.contentItems = ContentItem.array(of: channel.videos)
} else if let videos: [Video] = response.typedContent() {
channel.videos = videos
ChannelsCacheModel.shared.store(channel)
store.contentItems = ContentItem.array(of: videos)
} else if let channelPage: ChannelPage = response.typedContent() {
if let channel = channelPage.channel {
DispatchQueue.main.async {
if let channel: Channel = response.typedContent() {
ChannelsCacheModel.shared.store(channel)
}
store.contentItems = ContentItem.array(of: channel.videos)
} else if let videos: [Video] = response.typedContent() {
channel.videos = videos
ChannelsCacheModel.shared.store(channel)
store.contentItems = ContentItem.array(of: videos)
} else if let channelPage: ChannelPage = response.typedContent() {
if let channel = channelPage.channel {
ChannelsCacheModel.shared.store(channel)
}
store.contentItems = channelPage.results
store.contentItems = channelPage.results
}
}
}
case let .channelPlaylist(_, id, title):
@@ -256,6 +283,9 @@ struct FavoriteItemView: View {
onSuccess = { response in
if let playlist: ChannelPlaylist = response.typedContent() {
ChannelPlaylistsCacheModel.shared.storePlaylist(playlist: playlist)
DispatchQueue.main.async {
store.contentItems = contentItems
}
}
}
case let .playlist(_, id):
@@ -264,12 +294,16 @@ struct FavoriteItemView: View {
if let playlist = playlists.first(where: { $0.id == id }) {
contentItems = ContentItem.array(of: playlist.videos)
}
DispatchQueue.main.async {
store.contentItems = contentItems
}
default:
contentItems = []
}
if !contentItems.isEmpty {
store.contentItems = contentItems
DispatchQueue.main.async {
store.contentItems = contentItems
}
}
if force {
@@ -435,6 +469,7 @@ struct FavoriteItemView: View {
switch item.section {
case .history:
return nil
case .subscriptions:
if accounts.app.supportsSubscriptions {
return accounts.api.feed(1)
@@ -486,14 +521,22 @@ struct FavoriteItemView: View {
}
struct FavoriteItemView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
VStack {
FavoriteItemView(item: .init(section: .channel("peerTube", "a", "Search: resistance body upper band workout")))
.environment(\.navigationStyle, .tab)
FavoriteItemView(item: .init(section: .channel("peerTube", "a", "Marques")))
.environment(\.navigationStyle, .sidebar)
struct PreviewWrapper: View {
@State private var favoritesChanged = false
var body: some View {
NavigationView {
VStack {
FavoriteItemView(item: .init(section: .channel("peerTube", "a", "Search: resistance body upper band workout")), favoritesChanged: $favoritesChanged)
.environment(\.navigationStyle, .tab)
FavoriteItemView(item: .init(section: .channel("peerTube", "a", "Marques")), favoritesChanged: $favoritesChanged)
.environment(\.navigationStyle, .sidebar)
}
}
}
}
static var previews: some View {
PreviewWrapper()
}
}

View File

@@ -5,18 +5,52 @@ final class FavoriteResourceObserver: ObservableObject, ResourceObserver {
@Published var contentItems = [ContentItem]()
func resourceChanged(_ resource: Resource, event _: ResourceEvent) {
// swiftlint:disable discouraged_optional_collection
var newVideos: [Video]?
var newItems: [ContentItem]?
// swiftlint:enable discouraged_optional_collection
var newChannel: Channel?
var newChannelPlaylist: ChannelPlaylist?
var newPlaylist: Playlist?
var newPage: SearchPage?
if let videos: [Video] = resource.typedContent() {
contentItems = videos.map { ContentItem(video: $0) }
newVideos = videos
} else if let channel: Channel = resource.typedContent() {
contentItems = channel.videos.map { ContentItem(video: $0) }
newChannel = channel
} else if let playlist: ChannelPlaylist = resource.typedContent() {
contentItems = playlist.videos.map { ContentItem(video: $0) }
newChannelPlaylist = playlist
} else if let playlist: Playlist = resource.typedContent() {
contentItems = playlist.videos.map { ContentItem(video: $0) }
newPlaylist = playlist
} else if let page: SearchPage = resource.typedContent() {
contentItems = page.results
newPage = page
} else if let items: [ContentItem] = resource.typedContent() {
contentItems = items
newItems = items
}
DispatchQueue.global(qos: .userInitiated).async {
var newContentItems: [ContentItem] = []
if let videos = newVideos {
newContentItems = videos.map { ContentItem(video: $0) }
} else if let channel = newChannel {
newContentItems = channel.videos.map { ContentItem(video: $0) }
} else if let playlist = newChannelPlaylist {
newContentItems = playlist.videos.map { ContentItem(video: $0) }
} else if let playlist = newPlaylist {
newContentItems = playlist.videos.map { ContentItem(video: $0) }
} else if let page = newPage {
newContentItems = page.results
} else if let items = newItems {
newContentItems = items
}
DispatchQueue.main.async {
if !newContentItems.isEmpty {
self.contentItems = newContentItems
}
}
}
}
}

View File

@@ -1,19 +1,14 @@
import SwiftUI
struct HistoryView: View {
var limit = 10
var limit: Int
@FetchRequest(sortDescriptors: [.init(key: "watchedAt", ascending: false)])
var watches: FetchedResults<Watch>
@ObservedObject private var player = PlayerModel.shared
@State private var visibleWatches = [Watch]()
init(limit: Int = 10) {
self.limit = limit
}
var body: some View {
LazyVStack {
if visibleWatches.isEmpty {
@@ -38,10 +33,14 @@ struct HistoryView: View {
func reloadVisibleWatches() {
visibleWatches = Array(watches.filter { $0.videoID != player.currentVideo?.videoID }.prefix(limit))
}
init(limit: Int = 10) {
self.limit = limit
}
}
struct HistoryView_Previews: PreviewProvider {
static var previews: some View {
HistoryView()
HistoryView(limit: 10)
}
}

View File

@@ -5,9 +5,11 @@ import UniformTypeIdentifiers
struct HomeView: View {
@ObservedObject private var accounts = AccountsModel.shared
@ObservedObject private var player = PlayerModel.shared
@State private var presentingHomeSettings = false
@State private var favoritesChanged = false
@State private var updateTask: Task<Void, Never>?
@FetchRequest(sortDescriptors: [.init(key: "watchedAt", ascending: false)])
var watches: FetchedResults<Watch>
@@ -16,8 +18,6 @@ struct HomeView: View {
@State private var recentDocumentsID = UUID()
#endif
var favoritesObserver: Any?
#if !os(tvOS)
@Default(.favorites) private var favorites
@Default(.widgetsSettings) private var widgetsSettings
@@ -97,11 +97,11 @@ struct HomeView: View {
VStack(alignment: .leading) {
#if os(tvOS)
ForEach(Defaults[.favorites]) { item in
FavoriteItemView(item: item)
FavoriteItemView(item: item, favoritesChanged: $favoritesChanged)
}
#else
ForEach(favorites) { item in
FavoriteItemView(item: item)
FavoriteItemView(item: item, favoritesChanged: $favoritesChanged)
#if os(macOS)
.workaroundForVerticalScrollingBug()
#endif
@@ -124,6 +124,24 @@ struct HomeView: View {
}
}
}
.onDisappear {
updateTask?.cancel()
}
.onChange(of: player.presentingPlayer) { _ in
if player.presentingPlayer {
updateTask?.cancel()
} else {
Task {
for await _ in Defaults.updates(.favorites) {
favoritesChanged.toggle()
}
for await _ in Defaults.updates(.widgetsSettings) {
favoritesChanged.toggle()
}
}
}
}
.redrawOn(change: favoritesChanged)

109
Shared/LanguageCodes.swift Normal file
View File

@@ -0,0 +1,109 @@
enum LanguageCodes: String, CaseIterable {
case Afrikaans = "af"
case Arabic = "ar"
case Azerbaijani = "az"
case Bengali = "bn"
case Catalan = "ca"
case Czech = "cs"
case Welsh = "cy"
case Danish = "da"
case German = "de"
case Greek = "el"
case English = "en"
case English_GB = "en-GB"
case Spanish = "es"
case Persian = "fa"
case Finnish = "fi"
case Filipino = "fil"
case French = "fr"
case Irish = "ga"
case Hebrew = "he"
case Hindi = "hi"
case Hungarian = "hu"
case Indonesian = "id"
case Italian = "it"
case Japanese = "ja"
case Javanese = "jv"
case Korean = "ko"
case Lithuanian = "lt"
case Malay = "ms"
case Maltese = "mt"
case Dutch = "nl"
case Norwegian = "no"
case Polish = "pl"
case Portuguese = "pt"
case Romanian = "ro"
case Russian = "ru"
case Slovak = "sk"
case Slovene = "sl"
case Swedish = "sv"
case Swahili = "sw"
case Thai = "th"
case Tagalog = "tl"
case Turkish = "tr"
case Ukrainian = "uk"
case Urdu = "ur"
case Uzbek = "uz"
case Vietnamese = "vi"
case Xhosa = "xh"
case Chinese = "zh"
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"
}
}
static func languageName(for code: String) -> String {
return LanguageCodes(rawValue: code)?.description ?? "Unknown"
}
}

View File

@@ -333,9 +333,19 @@ struct ControlsOverlay: View {
} label: {
HStack(spacing: 4) {
Image(systemName: "text.bubble")
if let captions = captionsBinding.wrappedValue {
Text(captions.code)
.foregroundColor(.primary)
if let captions = captionsBinding.wrappedValue,
let language = LanguageCodes(rawValue: captions.code)
{
Text("\(language.description.capitalized) (\(language.rawValue))")
.foregroundColor(.accentColor)
} else {
if captionsBinding.wrappedValue == nil {
Text("Not available")
} else {
Text("Disabled")
.foregroundColor(.accentColor)
}
}
}
.frame(width: 240)
@@ -351,8 +361,18 @@ struct ControlsOverlay: View {
ControlsOverlayButton(focusedField: $focusedField, field: .captions) {
HStack(spacing: 8) {
Image(systemName: "text.bubble")
if let captions = captionsBinding.wrappedValue {
Text(captions.code)
if let captions = captionsBinding.wrappedValue,
let language = LanguageCodes(rawValue: captions.code)
{
Text("\(language.description.capitalized) (\(language.rawValue))")
.foregroundColor(.accentColor)
} else {
if captionsBinding.wrappedValue == nil {
Text("Not available")
} else {
Text("Disabled")
.foregroundColor(.accentColor)
}
}
}
.frame(maxWidth: 320)

View File

@@ -10,26 +10,28 @@ struct OpeningStream: View {
}
var visible: Bool {
(!player.currentItem.isNil && !player.videoBeingOpened.isNil) || (player.isLoadingVideo && !model.pausedForCache && !player.isSeeking)
(!player.currentItem.isNil && !player.videoBeingOpened.isNil) ||
(player.isLoadingVideo && !model.pausedForCache && !player.isSeeking) ||
!player.hasStarted
}
var reason: String {
guard player.videoBeingOpened == nil else {
return "Loading streams...".localized()
return "Loading streams".localized()
}
if player.musicMode {
return "Opening audio stream...".localized()
return "Opening audio stream".localized()
}
if let selection = player.streamSelection {
if selection.isLocal {
return "Opening file...".localized()
return "Opening file".localized()
}
return String(format: "Opening %@ stream...".localized(), selection.shortQuality)
return String(format: "Opening %@ stream".localized(), selection.shortQuality)
}
return "Loading streams...".localized()
return "Loading streams".localized()
}
var state: String? {

View File

@@ -30,7 +30,6 @@ enum PlayerControlsLayout: String, CaseIterable, Defaults.Serializable {
#else
return isIPad
#endif
case .large:
return true
case .medium:
@@ -264,7 +263,7 @@ enum PlayerControlsLayout: String, CaseIterable, Defaults.Serializable {
var seekOSDWidth: Double {
switch self {
case .tvRegular:
return 240
return 280
case .veryLarge:
return 240
case .large:

View File

@@ -1,3 +1,4 @@
import Combine
import Defaults
import SwiftUI
@@ -383,23 +384,35 @@ struct PlaybackSettings: View {
}
@ViewBuilder private var captionsButton: some View {
let videoCaptions = player.currentVideo?.captions
#if os(macOS)
captionsPicker
.labelsHidden()
.frame(maxWidth: 300)
#elseif os(iOS)
Menu {
captionsPicker
if videoCaptions?.isEmpty == false {
captionsPicker
}
} label: {
HStack(spacing: 4) {
Image(systemName: "text.bubble")
if let captions = player.captions {
Text(captions.code)
if let captions = player.captions,
let language = LanguageCodes(rawValue: captions.code)
{
Text("\(language.description.capitalized) (\(language.rawValue))")
.foregroundColor(.accentColor)
} else {
if videoCaptions?.isEmpty == true {
Text("Not available")
} else {
Text("Disabled")
}
}
}
.frame(alignment: .trailing)
.frame(height: 40)
.disabled(videoCaptions?.isEmpty == true)
}
.transaction { t in t.animation = .none }
.buttonStyle(.plain)

View File

@@ -65,7 +65,7 @@ import SwiftUI
}
static var thumbnailHeight: Double {
thumbnailWidth / 1.7777
thumbnailWidth / (16 / 9)
}
}

View File

@@ -164,7 +164,6 @@ struct VideoActions: View {
}
case .musicMode:
actionButton("Music", systemImage: "music.note", active: player.musicMode, action: player.toggleMusicMode)
case .settings:
actionButton("Settings", systemImage: "gear") {
withAnimation(ControlOverlaysModel.animation) {
@@ -179,7 +178,6 @@ struct VideoActions: View {
actionButton("Hide", systemImage: "chevron.down") {
player.hide(animate: true)
}
case .close:
actionButton("Close", systemImage: "xmark") {
player.closeCurrentItem()

View File

@@ -5,8 +5,12 @@ struct AdvancedSettings: View {
@Default(.showMPVPlaybackStats) private var showMPVPlaybackStats
@Default(.mpvCacheSecs) private var mpvCacheSecs
@Default(.mpvCachePauseWait) private var mpvCachePauseWait
@Default(.mpvCachePauseInital) private var mpvCachePauseInital
@Default(.mpvDeinterlace) private var mpvDeinterlace
@Default(.mpvEnableLogging) private var mpvEnableLogging
@Default(.mpvHWdec) private var mpvHWdec
@Default(.mpvDemuxerLavfProbeInfo) private var mpvDemuxerLavfProbeInfo
@Default(.mpvInitialAudioSync) private var mpvInitialAudioSync
@Default(.showCacheStatus) private var showCacheStatus
@Default(.feedCacheSize) private var feedCacheSize
@Default(.showPlayNowInBackendContextMenu) private var showPlayNowInBackendContextMenu
@@ -68,9 +72,45 @@ struct AdvancedSettings: View {
mpvEnableLoggingToggle
#endif
Toggle(isOn: $mpvCachePauseInital) {
HStack {
Text("cache-pause-initial")
#if !os(tvOS)
Image(systemName: "link")
.accessibilityAddTraits([.isButton, .isLink])
.font(.footnote)
#if os(iOS)
.onTapGesture {
UIApplication.shared.open(URL(string: "https://mpv.io/manual/stable/#options-cache-pause-initial")!)
}
#elseif os(macOS)
.onTapGesture {
NSWorkspace.shared.open(URL(string: "https://mpv.io/manual/stable/#options-cache-pause-initial")!)
}
.onHover(perform: onHover(_:))
#endif
#endif
}
}
HStack {
Text("cache-secs")
.frame(minWidth: 140, alignment: .leading)
#if !os(tvOS)
Image(systemName: "link")
.accessibilityAddTraits([.isButton, .isLink])
.font(.footnote)
#if os(iOS)
.onTapGesture {
UIApplication.shared.open(URL(string: "https://mpv.io/manual/stable/#options-cache-secs")!)
}
#elseif os(macOS)
.onTapGesture {
NSWorkspace.shared.open(URL(string: "https://mpv.io/manual/stable/#options-cache-secs")!)
}
.onHover(perform: onHover(_:))
#endif
#endif
TextField("cache-secs", text: $mpvCacheSecs)
#if !os(macOS)
.keyboardType(.numberPad)
@@ -79,8 +119,25 @@ struct AdvancedSettings: View {
.multilineTextAlignment(.trailing)
HStack {
Text("cache-pause-wait")
.frame(minWidth: 140, alignment: .leading)
Group {
Text("cache-pause-wait")
#if !os(tvOS)
Image(systemName: "link")
.accessibilityAddTraits([.isButton, .isLink])
.font(.footnote)
#if os(iOS)
.onTapGesture {
UIApplication.shared.open(URL(string: "https://mpv.io/manual/stable/#options-cache-pause-wait")!)
}
#elseif os(macOS)
.onTapGesture {
NSWorkspace.shared.open(URL(string: "https://mpv.io/manual/stable/#options-cache-pause-wait")!)
}
.onHover(perform: onHover(_:))
#endif
#endif
}.frame(minWidth: 140, alignment: .leading)
TextField("cache-pause-wait", text: $mpvCachePauseWait)
#if !os(macOS)
.keyboardType(.numberPad)
@@ -88,7 +145,105 @@ struct AdvancedSettings: View {
}
.multilineTextAlignment(.trailing)
Toggle("deinterlace", isOn: $mpvDeinterlace)
Toggle(isOn: $mpvDeinterlace) {
HStack {
Text("deinterlace")
#if !os(tvOS)
Image(systemName: "link")
.accessibilityAddTraits([.isButton, .isLink])
.font(.footnote)
#if os(iOS)
.onTapGesture {
UIApplication.shared.open(URL(string: "https://mpv.io/manual/stable/#options-deinterlace")!)
}
#elseif os(macOS)
.onTapGesture {
NSWorkspace.shared.open(URL(string: "https://mpv.io/manual/stable/#options-deinterlace")!)
}
.onHover(perform: onHover(_:))
#endif
#endif
}
}
Toggle(isOn: $mpvInitialAudioSync) {
HStack {
Text("initial-audio-sync")
#if !os(tvOS)
Image(systemName: "link")
.accessibilityAddTraits([.isButton, .isLink])
.font(.footnote)
#if os(iOS)
.onTapGesture {
UIApplication.shared.open(URL(string: "https://mpv.io/manual/stable/#options-initial-audio-sync")!)
}
#elseif os(macOS)
.onTapGesture {
NSWorkspace.shared.open(URL(string: "https://mpv.io/manual/stable/#options-initial-audio-sync")!)
}
.onHover(perform: onHover(_:))
#endif
#endif
}
}
HStack {
Text("hwdec")
#if !os(tvOS)
Image(systemName: "link")
.accessibilityAddTraits([.isButton, .isLink])
.font(.footnote)
#if os(iOS)
.onTapGesture {
UIApplication.shared.open(URL(string: "https://mpv.io/manual/stable/#options-hwdec")!)
}
#elseif os(macOS)
.onTapGesture {
NSWorkspace.shared.open(URL(string: "https://mpv.io/manual/stable/#options-hwdec")!)
}
.onHover(perform: onHover(_:))
#endif
#endif
Picker("", selection: $mpvHWdec) {
ForEach(["auto", "auto-safe", "auto-copy"], id: \.self) {
Text($0)
}
}
#if !os(tvOS)
.pickerStyle(MenuPickerStyle())
#endif
}
HStack {
Text("demuxer-lavf-probe-info")
#if !os(tvOS)
Image(systemName: "link")
.accessibilityAddTraits([.isButton, .isLink])
.font(.footnote)
#if os(iOS)
.onTapGesture {
UIApplication.shared.open(URL(string: "https://mpv.io/manual/stable/#options-demuxer-lavf-probe-info")!)
}
#elseif os(macOS)
.onTapGesture {
NSWorkspace.shared.open(URL(string: "https://mpv.io/manual/stable/#options-demuxer-lavf-probe-info")!)
}
.onHover(perform: onHover(_:))
#endif
#endif
Picker("", selection: $mpvDemuxerLavfProbeInfo) {
ForEach(["yes", "no", "auto", "nostreams"], id: \.self) {
Text($0)
}
}
#if !os(tvOS)
.pickerStyle(MenuPickerStyle())
#endif
}
if mpvEnableLogging {
logButton
@@ -103,20 +258,19 @@ struct AdvancedSettings: View {
}
@ViewBuilder var mpvFooter: some View {
let url = "https://mpv.io/manual/master"
let url = "https://mpv.io/manual/stable/"
VStack(alignment: .leading) {
Text("Restart the app to apply the settings above.")
.padding(.bottom, 1)
VStack(alignment: .leading, spacing: 2) {
#if os(tvOS)
Text("More info can be found in MPV Documentation:")
Text("More info can be found in MPV reference manual:")
Text(url)
#else
Text("More info can be found in:")
Link("MPV Documentation", destination: URL(string: url)!)
#if os(macOS)
.onHover(perform: onHover(_:))
#endif
Text("Further information can be found in the ")
+ Text("MPV reference manual").underline().bold()
+ Text(" by clicking on the link icon next to the option.")
#endif
}
}

View File

@@ -62,7 +62,7 @@ struct ImportSettingsAccountRow: View {
}
} else {
Group {
if InstancesModel.shared.find(instanceID) != nil {
if InstancesModel.shared.find(instanceID) != nil || InstancesModel.shared.findByURLString(account.urlString) != nil {
HStack {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)

View File

@@ -42,7 +42,7 @@ final class ImportSettingsSheetViewModel: ObservableObject {
return ((account.password != nil && !account.password!.isEmpty) ||
importableAccounts.contains(account.id)) && (
(InstancesModel.shared.find(instanceID) != nil) ||
(InstancesModel.shared.find(instanceID) != nil || InstancesModel.shared.findByURLString(account.urlString) != nil) ||
selectedInstances.contains(instanceID)
)
}

View File

@@ -30,12 +30,19 @@ struct PlayerSettings: View {
@Default(.enableReturnYouTubeDislike) private var enableReturnYouTubeDislike
@Default(.showRelated) private var showRelated
@Default(.showInspector) private var showInspector
@Default(.showChapters) private var showChapters
@Default(.showChapterThumbnails) private var showThumbnails
@Default(.showChapterThumbnailsOnlyWhenDifferent) private var showThumbnailsOnlyWhenDifferent
@Default(.expandChapters) private var expandChapters
@Default(.showRelated) private var showRelated
@Default(.captionsAutoShow) private var captionsAutoShow
@Default(.captionsDefaultLanguageCode) private var captionsDefaultLanguageCode
@Default(.captionsFallbackLanguageCode) private var captionsFallbackLanguageCode
@Default(.captionsFontScaleSize) private var captionsFontScaleSize
@Default(.captionsFontColor) private var captionsFontColor
@ObservedObject private var accounts = AccountsModel.shared
@@ -45,6 +52,11 @@ struct PlayerSettings: View {
}
#endif
#if os(tvOS)
@State private var isShowingDefaultLanguagePicker = false
@State private var isShowingFallbackLanguagePicker = false
#endif
var body: some View {
Group {
#if os(macOS)
@@ -93,7 +105,54 @@ struct PlayerSettings: View {
inspectorVisibilityPicker
#endif
}
#endif
Section(header: SettingsHeader(text: "Captions".localized())) {
#if os(tvOS)
Text("Size").font(.subheadline)
#endif
captionsFontScaleSizePicker
#if os(tvOS)
Text("Color").font(.subheadline)
#endif
captionsFontColorPicker
showCaptionsAutoShowToggle
#if !os(tvOS)
captionDefaultLanguagePicker
captionFallbackLanguagePicker
#else
Button(action: { isShowingDefaultLanguagePicker = true }) {
HStack {
Text("Default language")
Spacer()
Text("\(LanguageCodes(rawValue: captionsDefaultLanguageCode)!.description.capitalized) (\(captionsDefaultLanguageCode))").foregroundColor(.secondary)
}
}
.frame(maxWidth: .infinity).sheet(isPresented: $isShowingDefaultLanguagePicker) {
defaultLanguagePickerTVOS(
selectedLanguage: $captionsDefaultLanguageCode,
isShowing: $isShowingDefaultLanguagePicker
)
}
Button(action: { isShowingFallbackLanguagePicker = true }) {
HStack {
Text("Fallback language")
Spacer()
Text("\(LanguageCodes(rawValue: captionsFallbackLanguageCode)!.description.capitalized) (\(captionsFallbackLanguageCode))").foregroundColor(.secondary)
}
}
.frame(maxWidth: .infinity).sheet(isPresented: $isShowingDefaultLanguagePicker) {
fallbackLanguagePickerTVOS(
selectedLanguage: $captionsFallbackLanguageCode,
isShowing: $isShowingFallbackLanguagePicker
)
}
#endif
}
#if !os(tvOS)
Section(header: SettingsHeader(text: "Chapters".localized())) {
showChaptersToggle
showThumbnailsToggle
@@ -279,6 +338,103 @@ struct PlayerSettings: View {
}
#endif
private var showCaptionsAutoShowToggle: some View {
Toggle("Always show captions", isOn: $captionsAutoShow)
}
private var captionsFontScaleSizePicker: some View {
Picker("Size", selection: $captionsFontScaleSize) {
Text("Small").tag(String("0.5"))
Text("Medium").tag(String("1.0"))
Text("Large").tag(String("2.0"))
}
.onChange(of: captionsFontScaleSize) { _ in
PlayerModel.shared.mpvBackend.client.setSubFontSize(scaleSize: captionsFontScaleSize)
}
#if os(macOS)
.labelsHidden()
#endif
}
private var captionsFontColorPicker: some View {
Picker("Color", selection: $captionsFontColor) {
Text("White").tag(String("#FFFFFF"))
Text("Yellow").tag(String("#FFFF00"))
Text("Red").tag(String("#FF0000"))
Text("Orange").tag(String("#FFA500"))
Text("Green").tag(String("#008000"))
Text("Blue").tag(String("#0000FF"))
}
.onChange(of: captionsFontColor) { _ in
PlayerModel.shared.mpvBackend.client.setSubFontColor(color: captionsFontColor)
}
#if os(macOS)
.labelsHidden()
#endif
}
#if !os(tvOS)
private var captionDefaultLanguagePicker: some View {
Picker("Default language", selection: $captionsDefaultLanguageCode) {
ForEach(LanguageCodes.allCases, id: \.self) { language in
Text("\(language.description.capitalized) (\(language.rawValue))").tag(language.rawValue)
}
}
#if os(macOS)
.labelsHidden()
#endif
}
private var captionFallbackLanguagePicker: some View {
Picker("Fallback language", selection: $captionsFallbackLanguageCode) {
ForEach(LanguageCodes.allCases, id: \.self) { language in
Text("\(language.description.capitalized) (\(language.rawValue))").tag(language.rawValue)
}
}
#if os(macOS)
.labelsHidden()
#endif
}
#else
struct defaultLanguagePickerTVOS: View {
@Binding var selectedLanguage: String
@Binding var isShowing: Bool
var body: some View {
NavigationView {
List(LanguageCodes.allCases, id: \.self) { language in
Button(action: {
selectedLanguage = language.rawValue
isShowing = false
}) {
Text("\(language.description.capitalized) (\(language.rawValue))")
}
}
.navigationTitle("Select Default Language")
}
}
}
struct fallbackLanguagePickerTVOS: View {
@Binding var selectedLanguage: String
@Binding var isShowing: Bool
var body: some View {
NavigationView {
List(LanguageCodes.allCases, id: \.self) { language in
Button(action: {
selectedLanguage = language.rawValue
isShowing = false
}) {
Text("\(language.description.capitalized) (\(language.rawValue))")
}
}
.navigationTitle("Select Fallback Language")
}
}
}
#endif
#if !os(tvOS)
private var inspectorVisibilityPicker: some View {
Picker("Inspector", selection: $showInspector) {

View File

@@ -57,7 +57,7 @@ struct QualityProfileForm: View {
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.background(Color.background(scheme: colorScheme))
#else
.frame(width: 400, height: 400)
.frame(width: 400, height: 450)
.padding(.vertical, 10)
#endif
}
@@ -136,9 +136,20 @@ struct QualityProfileForm: View {
var formatsFooter: some View {
VStack(alignment: .leading) {
Text("Formats can be reordered and will be selected in this order.")
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
if #available(iOS 16.0, *) {
Text("Formats can be reordered and will be selected in this order.")
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
} else if #available(iOS 14.0, *) {
Text("Formats will be selected in the order they are listed.")
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
} else {
Text("Formats will be selected in the order they are listed.")
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
Text("**Note:** HLS is an adaptive format where specific resolution settings don't apply.")
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
@@ -304,7 +315,7 @@ struct QualityProfileForm: View {
func isResolutionDisabled(_ resolution: ResolutionSetting) -> Bool {
guard backend == .appleAVPlayer else { return false }
return resolution.value > .hd1080p60
return resolution.value > .hd720p30
}
func initializeForm() {

View File

@@ -316,19 +316,19 @@ struct SettingsView: View {
case .browsing:
return 800
case .player:
return 550
return 800
case .controls:
return 920
case .quality:
return 420
case .history:
return 500
return 600
case .sponsorBlock:
return 700
return 970
case .locations:
return 600
case .advanced:
return 500
return 550
case .importExport:
return 580
case .help:

View File

@@ -74,7 +74,7 @@ struct SponsorBlockSettings: View {
)
)
} label: {
Text("Restore Default Colors ")
Text("Restore Default Colors…")
.foregroundColor(.red)
}
#endif

View File

@@ -4,18 +4,183 @@ import SwiftUI
struct FeedView: View {
@ObservedObject private var feed = FeedModel.shared
@ObservedObject private var subscriptions = SubscribedChannelsModel.shared
@ObservedObject private var feedCount = UnwatchedFeedCountModel.shared
@Default(.showCacheStatus) private var showCacheStatus
#if os(tvOS)
@Default(.subscriptionsListingStyle) private var subscriptionsListingStyle
@StateObject private var accountsModel = AccountsViewModel()
#endif
var videos: [ContentItem] {
ContentItem.array(of: feed.videos)
guard let selectedChannel = selectedChannel else {
return ContentItem.array(of: feed.videos)
}
return ContentItem.array(of: feed.videos.filter {
$0.channel.id == selectedChannel.id
})
}
var channels: [Channel] {
feed.videos.map {
$0.channel
}.unique()
}
@State private var selectedChannel: Channel?
#if os(tvOS)
@FocusState private var focusedChannel: String?
#endif
@State private var feedChannelsViewVisible = false
private var navigation = NavigationModel.shared
private let dismiss_channel_list_id = "dismiss_channel_list_id"
var body: some View {
#if os(tvOS)
GeometryReader { geometry in
ZStack {
// selected channel feed view
HStack(spacing: 0) {
// sidebar - show channels
if feedChannelsViewVisible {
Spacer()
.frame(width: geometry.size.width * 0.3)
}
selectedFeedView
}
.disabled(feedChannelsViewVisible)
.frame(width: geometry.size.width, height: geometry.size.height)
if feedChannelsViewVisible {
HStack(spacing: 0) {
// sidebar - show channels
feedChannelsView
.padding(.all)
.frame(width: geometry.size.width * 0.3)
.background()
.clipShape(RoundedRectangle(cornerRadius: 16))
.contentShape(RoundedRectangle(cornerRadius: 16))
Rectangle()
.fill(.clear)
.id(dismiss_channel_list_id)
.focusable()
.focused(self.$focusedChannel, equals: dismiss_channel_list_id)
}
.frame(width: geometry.size.width, height: geometry.size.height)
.clipped()
}
}
}
#else
selectedFeedView
#endif
}
#if os(tvOS)
var accountsPicker: some View {
ForEach(accountsModel.sortedAccounts.filter { $0.anonymous == false }) { account in
Button(action: {
AccountsModel.shared.setCurrent(account)
}) {
HStack {
Text("\(account.description) (\(account.instance.app.rawValue))")
if account == accountsModel.currentAccount {
Image(systemName: "checkmark")
}
}
}
.buttonStyle(PlainButtonStyle())
}
}
var feedChannelsView: some View {
ScrollViewReader { proxy in
VStack {
Text("Channels")
.font(.subheadline)
if #available(tvOS 17.0, *) {
List(selection: $selectedChannel) {
Button(action: {
self.selectedChannel = nil
self.feedChannelsViewVisible = false
}) {
HStack(spacing: 16) {
Image(systemName: RecentsModel.symbolSystemImage("A"))
.imageScale(.large)
.foregroundColor(.accentColor)
.frame(width: 35, height: 35)
Text("All")
Spacer()
feedCount.unwatchedText
}
}
.padding(.all)
.background(RoundedRectangle(cornerRadius: 8.0)
.fill(self.selectedChannel == nil ? Color.secondary : Color.clear))
.font(.caption)
.buttonStyle(PlainButtonStyle())
.focused(self.$focusedChannel, equals: "all")
ForEach(channels, id: \.self) { channel in
Button(action: {
self.selectedChannel = channel
self.feedChannelsViewVisible = false
}) {
HStack(spacing: 16) {
ChannelAvatarView(channel: channel, subscribedBadge: false)
.frame(width: 50, height: 50)
Text(channel.name)
.lineLimit(1)
Spacer()
if let unwatchedCount = feedCount.unwatchedByChannelText(channel) {
unwatchedCount
}
}
}
.padding(.all)
.background(RoundedRectangle(cornerRadius: 8.0)
.fill(self.selectedChannel == channel ? Color.secondary : Color.clear))
.font(.caption)
.buttonStyle(PlainButtonStyle())
.focused(self.$focusedChannel, equals: channel.id)
}
}
.onChange(of: self.focusedChannel) {
if self.focusedChannel == "all" {
withAnimation {
self.selectedChannel = nil
}
} else if self.focusedChannel == dismiss_channel_list_id {
self.feedChannelsViewVisible = false
} else {
withAnimation {
self.selectedChannel = channels.first {
$0.id == self.focusedChannel
}
}
}
}
.onAppear {
guard let selectedChannel = self.selectedChannel else {
return
}
proxy.scrollTo(selectedChannel, anchor: .top)
}
.onExitCommand {
withAnimation {
self.feedChannelsViewVisible = false
}
}
}
}
}
}
#endif
var selectedFeedView: some View {
VerticalCells(items: videos) { if shouldDisplayHeader { header } }
.environment(\.loadMoreContentHandler) { feed.loadNextPage() }
.onAppear {
@@ -49,33 +214,55 @@ struct FeedView: View {
}
var header: some View {
HStack {
HStack(spacing: 16) {
#if os(tvOS)
SubscriptionsPageButton()
ListingStyleButtons(listingStyle: $subscriptionsListingStyle)
HideWatchedButtons()
HideShortsButtons()
#endif
if showCacheStatus {
Spacer()
CacheStatusHeader(
refreshTime: feed.formattedFeedTime,
isLoading: feed.isLoading
)
}
#if os(tvOS)
Button {
feed.loadResources(force: true)
} label: {
Label("Refresh", systemImage: "arrow.clockwise")
.labelStyle(.iconOnly)
.imageScale(.small)
.font(.caption)
if #available(tvOS 17.0, *) {
Menu {
accountsPicker
} label: {
Label("Channels", systemImage: "filemenu.and.selection")
.labelStyle(.iconOnly)
.imageScale(.small)
.font(.caption)
} primaryAction: {
withAnimation {
self.feedChannelsViewVisible = true
self.focusedChannel = selectedChannel?.id ?? "all"
}
}
.opacity(feedChannelsViewVisible ? 0 : 1)
.frame(minWidth: feedChannelsViewVisible ? 0 : nil, maxWidth: feedChannelsViewVisible ? 0 : nil)
}
channelHeaderView
if selectedChannel == nil {
Spacer()
}
if feedChannelsViewVisible == false {
ListingStyleButtons(listingStyle: $subscriptionsListingStyle)
HideWatchedButtons()
HideShortsButtons()
}
#endif
if feedChannelsViewVisible == false {
if showCacheStatus {
CacheStatusHeader(
refreshTime: feed.formattedFeedTime,
isLoading: feed.isLoading
)
}
#if os(tvOS)
Button {
feed.loadResources(force: true)
} label: {
Label("Refresh", systemImage: "arrow.clockwise")
.labelStyle(.iconOnly)
.imageScale(.small)
.font(.caption)
}
#endif
}
}
.padding(.leading, 30)
#if os(tvOS)
@@ -84,6 +271,46 @@ struct FeedView: View {
#endif
}
var channelHeaderView: some View {
guard let selectedChannel = selectedChannel else {
return AnyView(
Text("All Channels")
.font(.caption)
.frame(alignment: .leading)
.lineLimit(1)
.padding(0)
.padding(.leading, 16)
)
}
return AnyView(
HStack(spacing: 16) {
ChannelAvatarView(channel: selectedChannel, subscribedBadge: false)
.id("channel-avatar-\(selectedChannel.id)")
.frame(width: 80, height: 80)
Text("\(selectedChannel.name)")
.font(.caption)
.frame(alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: true, vertical: false)
Spacer()
if feedChannelsViewVisible == false {
Button(action: {
navigation.openChannel(selectedChannel, navigationStyle: .tab)
}) {
Text("Visit Channel")
.font(.caption)
.frame(alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: true, vertical: false)
}
}
}
.padding(0)
.padding(.leading, 16)
)
}
var shouldDisplayHeader: Bool {
#if os(tvOS)
true

110
Shared/URLTester.swift Normal file
View File

@@ -0,0 +1,110 @@
import Foundation
import Logging
enum URLTester {
private static let hlsMediaPrefix = "#EXT-X-MEDIA:"
private static let hlsInfPrefix = "#EXTINF:"
private static let uriRegex = "(?<=URI=\")(.*?)(?=\")"
static func testURLResponse(url: URL, range: String, isHLS: Bool, completion: @escaping (Int) -> Void) {
if isHLS {
parseAndTestHLSManifest(manifestUrl: url, range: range, completion: completion)
} else {
httpRequest(url: url, range: range) { statusCode, _ in
completion(statusCode)
}
}
}
private static func httpRequest(url: URL, range: String, completion: @escaping (Int, URLSessionDataTask?) -> Void) {
var request = URLRequest(url: url)
request.httpMethod = "HEAD"
request.setValue("bytes=\(range)", forHTTPHeaderField: "Range")
request.setValue(UserAgentManager.shared.userAgent, forHTTPHeaderField: "User-Agent")
var dataTask: URLSessionDataTask?
dataTask = URLSession.shared.dataTask(with: request) { _, response, _ in
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? HTTPStatus.Forbidden
Logger(label: "stream.yattee.httpRequest").info("URL: \(url) | Status Code: \(statusCode)")
completion(statusCode, dataTask)
}
dataTask?.resume()
}
static func parseAndTestHLSManifest(manifestUrl: URL, range: String, completion: @escaping (Int) -> Void) {
recursivelyParseManifest(manifestUrl: manifestUrl) { allURLs in
if let url = allURLs.randomElement() {
httpRequest(url: url, range: range) { statusCode, _ in
completion(statusCode)
}
} else {
completion(HTTPStatus.NotFound)
}
}
}
private static func recursivelyParseManifest(manifestUrl: URL, fullyParsed: @escaping ([URL]) -> Void) {
parseHLSManifest(manifestUrl: manifestUrl) { urls in
var allURLs = [URL]()
let group = DispatchGroup()
for url in urls {
if url.pathExtension == "m3u8" {
group.enter()
recursivelyParseManifest(manifestUrl: url) { subUrls in
allURLs += subUrls
group.leave()
}
} else {
allURLs.append(url)
}
}
group.notify(queue: .main) {
fullyParsed(allURLs)
}
}
}
private static func parseHLSManifest(manifestUrl: URL, completion: @escaping ([URL]) -> Void) {
URLSession.shared.dataTask(with: manifestUrl) { data, _, _ in
// swiftlint:disable:next shorthand_optional_binding
guard let data = data else {
Logger(label: "stream.yattee.httpRequest").error("Data is nil")
completion([])
return
}
// swiftlint:disable:next non_optional_string_data_conversion
guard let manifest = String(data: data, encoding: .utf8), !manifest.isEmpty else {
Logger(label: "stream.yattee.httpRequest").error("Cannot read or empty HLS manifest")
completion([])
return
}
let lines = manifest.split(separator: "\n")
var mediaURLs: [URL] = []
for index in 0 ..< lines.count {
let lineString = String(lines[index])
if lineString.hasPrefix(hlsMediaPrefix),
let uriRange = lineString.range(of: uriRegex, options: .regularExpression)
{
let uri = lineString[uriRange]
if let url = URL(string: String(uri)) {
mediaURLs.append(url)
}
} else if lineString.hasPrefix(hlsInfPrefix), index < lines.count - 1 {
let possibleURL = String(lines[index + 1])
let baseURL = manifestUrl.deletingLastPathComponent()
if let relativeURL = URL(string: possibleURL, relativeTo: baseURL),
relativeURL.scheme != nil
{
mediaURLs.append(relativeURL)
}
}
}
completion(mediaURLs)
}
.resume()
}
}

View File

@@ -0,0 +1,37 @@
import Logging
#if !os(tvOS)
import WebKit
#endif
final class UserAgentManager {
static let shared = UserAgentManager()
private(set) var userAgent: String
#if !os(tvOS)
private var webView: WKWebView?
#endif
private init() {
/*
In case an error occurs while retrieving the actual User-Agent, and on tvOS,
we set a default User-Agent value that represents a commonly used User-Agent.
*/
userAgent = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)"
#if !os(tvOS)
webView = WKWebView()
webView?.evaluateJavaScript("navigator.userAgent") { [weak self] result, _ in
if let userAgent = result as? String {
DispatchQueue.main.async {
self?.userAgent = userAgent
Logger(label: "stream.yattee.userAgentManager").info("User-Agent: \(userAgent)")
}
} else {
Logger(label: "stream.yattee.userAgentManager").warning("Failed to update User-Agent.")
}
}
#else
Logger(label: "stream.yattee.userAgentManager.tvOS").info("User-Agent: \(userAgent)")
#endif
}
}

View File

@@ -2,7 +2,7 @@ import SwiftUI
struct ListView: View {
var items: [ContentItem]
var limit: Int? = 10
var limit: Int?
var body: some View {
LazyVStack(alignment: .leading) {
@@ -16,16 +16,12 @@ struct ListView: View {
}
var limitedItems: [ContentItem] {
if let limit, limit >= 0 {
return Array(items.prefix(limit))
}
return items
Array(items.prefix(limit ?? items.count))
}
}
struct ListView_Previews: PreviewProvider {
static var previews: some View {
ListView(items: [.init(video: .fixture)])
ListView(items: [.init(video: .fixture)], limit: 10)
}
}

View File

@@ -150,61 +150,82 @@ struct YatteeApp: App {
}
configured = true
#if DEBUG
SiestaLog.Category.enabled = .common
#endif
SDImageCodersManager.shared.addCoder(SDImageAWebPCoder.shared)
SDWebImageManager.defaultImageCache = PINCache(name: "stream.yattee.app")
DispatchQueue.main.async {
#if DEBUG
SiestaLog.Category.enabled = .common
#endif
#if os(tvOS)
SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared)
#else
SDImageCodersManager.shared.addCoder(SDImageAWebPCoder.shared)
#endif
if !Defaults[.lastAccountIsPublic] {
AccountsModel.shared.configureAccount()
}
SDWebImageManager.defaultImageCache = PINCache(name: "stream.yattee.app")
if let countryOfPublicInstances = Defaults[.countryOfPublicInstances] {
InstancesManifest.shared.setPublicAccount(countryOfPublicInstances, asCurrent: AccountsModel.shared.current.isNil)
}
if !AccountsModel.shared.current.isNil {
player.restoreQueue()
}
if !Defaults[.saveRecents] {
recents.clear()
}
let startupSection = Defaults[.startupSection]
var section: TabSelection? = startupSection.tabSelection
#if os(macOS)
if section == .playlists {
section = .search
if !Defaults[.lastAccountIsPublic] {
AccountsModel.shared.configureAccount()
}
#endif
NavigationModel.shared.tabSelection = section ?? .search
if let countryOfPublicInstances = Defaults[.countryOfPublicInstances] {
InstancesManifest.shared.setPublicAccount(countryOfPublicInstances, asCurrent: AccountsModel.shared.current.isNil)
}
playlists.load()
if !AccountsModel.shared.current.isNil {
player.restoreQueue()
}
#if !os(macOS)
player.updateRemoteCommandCenter()
#endif
if player.presentingPlayer {
player.presentingPlayer = false
}
#if os(iOS)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if Defaults[.lockPortraitWhenBrowsing] {
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
DispatchQueue.global(qos: .userInitiated).async {
if !Defaults[.saveRecents] {
recents.clear()
}
}
#endif
URLBookmarkModel.shared.refreshAll()
let startupSection = Defaults[.startupSection]
var section: TabSelection? = startupSection.tabSelection
migrateHomeHistoryItems()
migrateQualityProfiles()
#if os(macOS)
if section == .playlists {
section = .search
}
#endif
NavigationModel.shared.tabSelection = section ?? .search
DispatchQueue.global(qos: .userInitiated).async {
playlists.load()
}
#if !os(macOS)
player.updateRemoteCommandCenter()
#endif
if player.presentingPlayer {
player.presentingPlayer = false
}
#if os(iOS)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if Defaults[.lockPortraitWhenBrowsing] {
Orientation.lockOrientation(.all, andRotateTo: .portrait)
}
}
#endif
// Initialize UserAgentManager
_ = UserAgentManager.shared
DispatchQueue.global(qos: .userInitiated).async {
URLBookmarkModel.shared.refreshAll()
}
DispatchQueue.global(qos: .userInitiated).async {
self.migrateHomeHistoryItems()
}
DispatchQueue.global(qos: .userInitiated).async {
self.migrateQualityProfiles()
}
}
}
func migrateHomeHistoryItems() {

View File

@@ -129,7 +129,7 @@
"LIVE" = "مباشر";
/* Loading stream OSD */
"Loading streams..." = "تحميل بثوث...";
"Loading streams" = "تحميل بثوث";
"Loading..." = "تحميل...";
/* Video duration filter in search */
@@ -163,8 +163,8 @@
"Open Settings" = "فتح الإعدادات";
/* Loading stream OSD */
"Opening %@ stream..." = "فتح بث %@ ...";
"Opening audio stream..." = "فتح بث صوتي...";
"Opening %@ stream" = "فتح بث %@ ";
"Opening audio stream" = "فتح بث صوتي";
"Orientation" = "اتجاه";
"Play in PiP" = "تشغيل في الفيديو المصغر";
"Play Last" = "تشغيل الأخير";
@@ -387,7 +387,7 @@
"Backend" = "الواجهة الخلفية";
"Badge" = "الشارة";
"Close PiP when starting playing other video" = "غلق الفيديو المصغر عند بدء تشغيل فيديو آخر";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "تذكيرات صريحة لإبداء الإعجاب بها أو الإشتراك فيها أو التفاعل معها على أي منصة (منصات) مدفوعة أو مجانية (مثل النقر فوق مقطع الفيديو).\n";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "تذكيرات صريحة لإبداء الإعجاب بها أو الإشتراك فيها أو التفاعل معها على أي منصة (منصات) مدفوعة أو مجانية (مثل النقر فوق مقطع الفيديو).";
"Filter" = " عامل التصفية";
"Frontend URL" = "عنوان URL للواجهة الأمامية";
"Fullscreen size" = "حجم ملء الشاشة";
@@ -558,7 +558,7 @@
"Are you sure you want to clear cache?" = "هل أنت متأكد من أنك تريد مسح ذاكرة التخزين المؤقت؟";
"Gesture settings control skipping interval for remote arrow buttons (for 2nd generation Siri Remote or newer). Changing system controls settings requires restart." = "التحكم في إعدادات إيماءة الفاصل الزمني للتخطي لأزرار الأسهم عن بعد (للجيل الثاني من Siri Remote أو أحدث). تغيير إعدادات نظام عناصر التحكم يتطلب إعادة بدء التشغيل.";
"Opened File" = "ملف مفتوح";
"Opening file..." = "فتح الملف...";
"Opening file" = "فتح الملف";
"Short videos: hidden" = "مقاطع الفيديو القصيرة: مخفية";
"Mark channel feed as watched" = "وضع علامة تمت المشاهدة على محتوى القناة";
"Gesture settings control skipping interval for double click on left/right side of the player. Changing system controls settings requires restart." = "التحكم في إعدادات إيماءة فترة التخطي للنقر المزدوج على الجانب الأيسر/الأيمن من المشغل. تغيير إعدادات نظام عناصر التحكم يتطلب إعادة بدء التشغيل.";
@@ -628,4 +628,4 @@
"Password required to import" = "كلمة المرور مطلوبة للإستيراد";
"Password saved in import file" = "كلمة المرور محفوظة في ملف الإستيراد";
"Export in progress..." = "جارِ التصدير...";
"In progress..." = "في تَقَدم…";
"In progress..." = "في طور الأجراء…";

View File

@@ -277,11 +277,11 @@
"Large" = "Böyük";
/* Loading stream OSD */
"Loading streams..." = "Yayımlar yüklənilir...";
"Loading streams" = "Yayımlar yüklənilir";
"Only when signed in" = "Yalnız daxil olduqda";
/* Loading stream OSD */
"Opening %@ stream..." = "%@ yayımıılır...";
"Opening %@ stream" = "%@ yayımıılır";
"Matrix Chat" = "Matrix Söhbət";
/* Player controls layout size */
@@ -296,7 +296,7 @@
"Open Settings" = "Tənzimləmələri Aç";
"Movies" = "Filmlər";
"No description" = "Açıqlama yoxdur";
"Opening audio stream..." = "Səs yayımıılır...";
"Opening audio stream" = "Səs yayımıılır";
"Password" = "Şifrə";
"Preferred Formats" = "Üstünlük Verilən Formatlar";
"Quality Profile" = "Profil Keyfiyyəti";

View File

@@ -113,7 +113,7 @@
"LIVE" = "EN VIU";
/* Loading stream OSD */
"Loading streams..." = "S'estan carregant els fluxos...";
"Loading streams" = "S'estan carregant els fluxos";
"Loading..." = "Carregant...";
"Locations" = "Ubicacions";
"Lock portrait mode" = "Bloqueja el mode vertical";
@@ -147,7 +147,7 @@
"Offtopic in Music Videos" = "Offtopic als vídeos musicals";
"Open \"Playlists\" tab to create new one" = "Obriu la pestanya \"Llistes de reproducció\" per crear-ne una de nova";
"Open Settings" = "Obriu Configuració";
"Opening audio stream..." = "Obrint la reproducció d'àudio...";
"Opening audio stream" = "Obrint la reproducció d'àudio";
"Orientation" = "Orientació";
/* SponsorBlock category name */
@@ -454,7 +454,7 @@
"Low" = "Baix";
/* Loading stream OSD */
"Opening %@ stream..." = "S'està obrint %@...";
"Opening %@ stream" = "S'està obrint %@";
"Only when signed in" = "Només quan s'ha iniciat la sessió";
"Password" = "Contrasenya";
"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 d'un vídeo que promociona un producte o servei no relacionat directament amb el creador. El creador rebrà un pagament o compensació en forma de diners o productes gratuïts.";

View File

@@ -166,7 +166,7 @@
"Only when signed in" = "Pouze když přihlášený";
"Open \"Playlists\" tab to create new one" = "Otevřete kartu \"Playlisty\", aby jste vytvořili nový";
"Open Settings" = "Otevřete Nastavení";
"Opening audio stream..." = "Otevírám audio stream...";
"Opening audio stream" = "Otevírám audio stream";
"Orientation" = "Orientace";
"Password" = "Heslo";
"Pause" = "Pauza";
@@ -401,11 +401,11 @@
"Promoting a product or service that is directly related to the creator themselves. This usually includes merchandise or promotion of monetized platforms." = "Propagace produktu nebo služby, která přímo souvisí s tvůrcem samotným. Obvykle se jedná o zboží nebo zpeněžené platformy.";
/* Loading stream OSD */
"Loading streams..." = "Načítaní streamu...";
"Loading streams" = "Načítaní streamu";
"Matrix Chat" = "Matrix Chat";
/* Loading stream OSD */
"Opening %@ stream..." = "Otevírám %@ stream...";
"Opening %@ stream" = "Otevírám %@ stream";
/* SponsorBlock category name */
"Outro" = "Zakončení";
@@ -543,7 +543,7 @@
"Available" = "Dostupné";
"Are you sure you want to remove %@ from Favorites?" = "Opravdu chcete odstranit %@ z oblíbených položek?";
"Use system controls with AVPlayer" = "Použití systémových ovládacích prvků s AVPlayerem";
"Opening file..." = "Otvírání souboru...";
"Opening file" = "Otvírání souboru";
"No videos to show" = "Žádná videa k zobrazení";
"Autoplay next" = "Automaticky přehrát další";
"Inspector" = "Inspektor";

View File

@@ -182,7 +182,7 @@
"Large" = "Groß";
/* Loading stream OSD */
"Loading streams..." = "Lädt Streams …";
"Loading streams" = "Lädt Streams …";
/* Video duration filter in search */
"Long" = "Lang";
@@ -210,7 +210,7 @@
"Only when signed in" = "Nur wenn Sie eingeloggt sind";
/* Loading stream OSD */
"Opening %@ stream..." = "Öffne %@-stream …";
"Opening %@ stream" = "Öffne %@-Stream …";
"Connection failed" = "Verbindung fehlgeschlagen";
"Continue from %@" = "Ab %@ fortsetzen";
"Contributing" = "Beitragen";
@@ -227,7 +227,7 @@
"I want to ask a question" = "Ich möchte eine Frage stellen";
"If you are interested what's coming in future updates, you can track project Milestones." = "Wenn Sie sich für künftige Updates interessieren, können Sie die Meilensteine des Projekts verfolgen.";
"Large layout is not suitable for all devices and using it may cause controls not to fit on the screen." = "Das große Layout ist nicht für alle Geräte geeignet und kann dazu führen, dass die Bedienelemente nicht auf den Bildschirm passen.";
"Opening audio stream..." = "Audiostream wird geöffnet …";
"Opening audio stream" = "Audiostream wird geöffnet …";
"Orientation" = "Ausrichtung";
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "Die Wiedergabeliste \"%@\" wird gelöscht.\nDies kann nicht rückgängig gemacht werden.";
"Preferred Formats" = "Bevorzugte Formate";
@@ -569,7 +569,7 @@
"Enter location address to connect..." = "Geben Sie die Internetadresse ein, um eine Verbindung herzustellen …";
"Opened File" = "Geöffnete Datei";
"File Extension" = "Dateierweiterung";
"Opening file..." = "Datei öffnen …";
"Opening file" = "Datei öffnen …";
"Close video and player on end" = "Video und Player am Ende beenden";
"Use system controls with AVPlayer" = "Systemsteuerung mit AVPlayer verwenden";
"Public account" = "Öffentliches Konto";

View File

@@ -157,7 +157,7 @@
"LIVE" = "LIVE";
/* Loading stream OSD */
"Loading streams..." = "Loading streams...";
"Loading streams" = "Loading streams";
"Loading..." = "Loading...";
"Locations" = "Locations";
"Lock portrait mode" = "Lock portrait mode";
@@ -202,8 +202,8 @@
"Open Settings" = "Open Settings";
/* Loading stream OSD */
"Opening %@ stream..." = "Opening %@ stream...";
"Opening audio stream..." = "Opening audio stream...";
"Opening %@ stream" = "Opening %@ stream";
"Opening audio stream" = "Opening audio stream";
"Orientation" = "Orientation";
/* SponsorBlock category name */
@@ -567,7 +567,7 @@
"Seek" = "Seek";
"Opened File" = "Opened File";
"File Extension" = "File Extension";
"Opening file..." = "Opening file...";
"Opening file" = "Opening file";
"Public account" = "Public account";
"Your Accounts" = "Your Accounts";
"Browse without account" = "Browse without account";

View File

@@ -117,7 +117,7 @@
"Decreased opacity" = "Opacidad disminuida";
"High" = "Alto";
"%lld videos" = "%lld videos";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Recordatorios explícitos para que indiquen les guste, se suscriban o interactúen con ellos en cualquier plataforma de pago o gratuita (por ejemplo, haciendo clic en un vídeo).\n";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Solicitudes explícitas para dar me gusta, suscribirse o interactuar con ellos en una o más plataformas gratuitas o de pago (por ejemplo, hacer clic en un vídeo).";
"Formats will be selected in order as listed.\nHLS is an adaptive format (resolution setting does not apply)." = "Los formatos se seleccionarán en orden como se indica.\nHLS es un formato adaptable (no aplica la configuración de resolución).";
"Fullscreen size" = "Tamaño de pantalla completa";
"Badge & Decreased opacity" = "Insignia y opacidad dosminuída";
@@ -138,7 +138,7 @@
/* Video date filter in search */
"Today" = "Hoy";
"Opening audio stream..." = "Abriendo transmisión de audio...";
"Opening audio stream" = "Abriendo transmisión de audio";
"Open Video" = "Abrir Video";
"I want to ask a question" = "Quiero hacer una pregunta";
"Save history of played videos" = "Guardar historial de videos reproducidos";
@@ -208,7 +208,7 @@
"No documents" = "Sin documentos";
/* Loading stream OSD */
"Opening %@ stream..." = "Abriendo %@ emisión...";
"Opening %@ stream" = "Abriendo %@ emisión";
"Documents" = "Documentos";
"Thumbnails" = "Miniaturas";
"Password" = "Contraseña";
@@ -265,7 +265,7 @@
"Shuffle" = "Mezclar";
/* Loading stream OSD */
"Loading streams..." = "Cargando secuencias...";
"Loading streams" = "Cargando secuencias";
"Public Locations" = "Ubicaciones públicas";
"Yattee" = "Yattee";
"No results" = "No hay resultados";
@@ -558,7 +558,7 @@
"Available" = "Disponible";
"Loop one" = "Bucle uno";
"Use system controls with AVPlayer" = "Utilizar los controles del sistema con AVPlayer";
"Opening file..." = "Abriendo el archivo...";
"Opening file" = "Abriendo el archivo";
"No videos to show" = "No hay vídeos que mostrar";
"Autoplay next" = "Reproducir automáticamente la siguiente";
"Home Settings" = "Ajustes iniciales";

View File

@@ -83,7 +83,7 @@
"Profiles" = "نمایه‌ها";
"New Playlist" = "فهرست پخش جدید";
"Automatic" = "خودکار";
"Opening file..." = "در حال باز کردن فایل…";
"Opening file" = "در حال باز کردن فایل…";
"Add Quality Profile" = "افزودن نمایهٔ کیفیت";
"Close video after playing last in the queue" = "ویدیو را پس از پخش آخرین مورد فهرست ببند";
@@ -279,14 +279,14 @@
"Controls" = "کنترل‌ها";
"This URL could not be opened" = "این نشانی باز نمی‌شود";
"Trending" = "پرطرفدار";
"Opening audio stream..." = "باز کردن استریم صوتی…";
"Opening audio stream" = "باز کردن استریم صوتی…";
"Statistics" = "آمار";
"Pause when player is closed" = "پس از بسته شدن پخش‌کننده مکث کن";
"Play All" = "همه را پخش کن";
"Sort: %@" = "ترتیب: %@";
/* Loading stream OSD */
"Opening %@ stream..." = "باز کردن استریم %@…";
"Opening %@ stream" = "باز کردن استریم %@…";
"Next in Queue" = "مورد بعد در صف";
"Honor orientation lock" = "قفل چرخش صفحه را در نظر بگیر";
"Rate" = "امتیاز";
@@ -405,7 +405,7 @@
"Info" = "اطلاعات";
/* Loading stream OSD */
"Loading streams..." = "درحال دریافت استریم…";
"Loading streams" = "درحال دریافت استریم…";
"No rotation" = "بدون چرخش";
"Codec" = "کدک (Codec)";
"Startup section" = "بخش آغازین";

View File

@@ -264,7 +264,7 @@
"Don't use public locations" = "Ne pas utiliser d'instances publiques";
"Enable Return YouTube Dislike" = "Activer Return YouTube Dislike";
"Enter fullscreen in landscape" = "Entrer en plein écran en mode paysage";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Rappels d'aimer la vidéo, de s'abonner ou d'interagir avec le créateur sur une plateforme gratuite ou payante.\n";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Rappels d'aimer la vidéo, de s'abonner ou d'interagir avec le créateur sur une plateforme gratuite ou payante.";
"Frontend URL" = "URL frontale";
"Public Locations" = "Instances publiques";
"Public Manifest" = "Manifeste publique";
@@ -282,11 +282,11 @@
"Large layout is not suitable for all devices and using it may cause controls not to fit on the screen." = "Le grand format n'est pas adapté à tous les appareils et son utilisation peut empêcher certains contrôles de s'afficher à l'écran.";
/* Loading stream OSD */
"Loading streams..." = "Chargement des flux...";
"Loading streams" = "Chargement des flux";
"Lock portrait mode" = "Verrouille l'orientation en mode portrait";
"Matrix Channel" = "Salon Matrix";
"Only when signed in" = "Uniquement lorsque vous êtes connecté";
"Opening audio stream..." = "Ouverture du flux audio…";
"Opening audio stream" = "Ouverture du flux audio…";
"Orientation" = "Orientation";
/* SponsorBlock category name */
@@ -424,7 +424,7 @@
"Milestones" = "Étapes";
/* Loading stream OSD */
"Opening %@ stream..." = "Ouverture du flux %@…";
"Opening %@ stream" = "Ouverture du flux %@…";
"Regular size" = "Taille normale";
"Regular Size" = "Taille normale";
"Related" = "En relation";
@@ -567,7 +567,7 @@
"Seek" = "Recherche";
"Show scroll to top button in comments" = "Afficher le bouton de retour en haut de la page dans les commentaires";
"Opened File" = "Fichier ouvert";
"Opening file..." = "Ouverture du fichier...";
"Opening file" = "Ouverture du fichier";
"Enter location address to connect..." = "Entrez l'adresse de l'instance pour se connecter...";
"File Extension" = "Extension de fichier";
"Public account" = "Compte publique";

View File

@@ -84,8 +84,8 @@
"Open Settings" = "सेटिंग खोलें";
/* Loading stream OSD */
"Opening %@ stream..." = "%@ स्ट्रीम खुल रहा…";
"Opening audio stream..." = "ऑडियो स्ट्रीम खुल रहा…";
"Opening %@ stream" = "%@ स्ट्रीम खुल रहा…";
"Opening audio stream" = "ऑडियो स्ट्रीम खुल रहा…";
"If you are reporting a bug, include all relevant details (especially: app version, used device and system version, steps to reproduce)." = "यदि आप किसी बग की रिपोर्ट कर रहे हैं, तो सभी प्रासंगिक विवरण शामिल करें (विशेषकर: ऐप संस्करण, प्रयुक्त डिवाइस और सिस्टम संस्करण, पुन: पेश करने के चरण)।";
"Increase rate" = "दर बढ़ाएँ";
"Info" = "जानकारी";
@@ -104,7 +104,7 @@
"LIVE" = "लाइव";
/* Loading stream OSD */
"Loading streams..." = "स्ट्रीम लोड हो रहें…";
"Loading streams" = "स्ट्रीम लोड हो रहें…";
"Loading..." = "लोड हो रहा…";
"Locations" = "स्थान";
"Lock portrait mode" = "पोर्ट्रेट मोड लॉक करें";

View File

@@ -158,8 +158,8 @@
"Open Settings" = "Apri Impostazioni";
/* Loading stream OSD */
"Opening %@ stream..." = "Apertura stream %@...";
"Opening audio stream..." = "Apertura stream audio...";
"Opening %@ stream" = "Apertura stream %@";
"Opening audio stream" = "Apertura stream audio";
"Orientation" = "Orientamento";
/* SponsorBlock category name */
@@ -170,7 +170,7 @@
"LIVE" = "DIRETTA";
/* Loading stream OSD */
"Loading streams..." = "Caricamento stream...";
"Loading streams" = "Caricamento stream";
"Loading..." = "Caricamento...";
"Locations" = "Posizioni";
"Low quality" = "Qualità bassa";
@@ -569,7 +569,7 @@
"Enter location address to connect..." = "Inserisci posizione per connetterti...";
"Opened File" = "File aperto";
"File Extension" = "Estensione file";
"Opening file..." = "Apro file...";
"Opening file" = "Apro file";
"Close video and player on end" = "Chiudi video e riproduttore alla fine";
"Use system controls with AVPlayer" = "Usa controlli di sistema con AVPlayer";
"Public account" = "Account pubblico";

View File

@@ -87,7 +87,7 @@
"Large" = "大";
/* Loading stream OSD */
"Loading streams..." = "ストリーム読込中...";
"Loading streams" = "ストリーム読込中";
"Lock portrait mode" = "縦モードをロック";
"LIVE" = "ライブ";
"Locations" = "場所";
@@ -108,10 +108,10 @@
"Offtopic in Music Videos" = "音楽動画の非音楽部分";
"Only when signed in" = "ログイン時のみ";
"Orientation" = "向き";
"Opening audio stream..." = "音声ストリーム 開始中...";
"Opening audio stream" = "音声ストリーム 開始中";
/* Loading stream OSD */
"Opening %@ stream..." = "%@ ストリーム 開始中...";
"Opening %@ stream" = "%@ ストリーム 開始中";
/* SponsorBlock category name */
"Outro" = "終了シーン";
@@ -566,7 +566,7 @@
"Show scroll to top button in comments" = "コメント欄に「上に戻る」表示";
"Opened File" = "開いたファイル";
"File Extension" = "ファイル拡張子";
"Opening file..." = "ファイルを読み込み中...";
"Opening file" = "ファイルを読み込み中";
"Your Accounts" = "アカウントを使用";
"Public account" = "公開アカウント";
"Browse without account" = "アカウントなしで閲覧";

View File

@@ -0,0 +1,70 @@
"%@ Playlist" = "%@ 플레이리스트";
"%@ subscribers" = "%@ 구독자";
"10 seconds forwards/backwards" = "10초 건너뛰기/뒤로 가기";
"Accounts" = "계정";
"Accounts are not supported for the application of this instance" = "해당 애플리케이션 인스턴스는 계정을 지원하지 않습니다";
"Add Location" = "주소 추가";
"Add Location..." = "주소 추가...";
"Add profile..." = "프로필 추가...";
"Add Quality Profile" = "품질 프로필 추가";
"Add to %@" = "%@에 추가";
"Add to Favorites" = "즐겨찾기에 추가";
"Add to Playlist" = "플레이리스트에 추가";
"Add to Playlist..." = "플레이리스트에 추가...";
"Advanced" = "고급";
/* Trending category, section containing all kinds of videos */
"All" = "전체";
"Always use AVPlayer for live videos" = "라이브 동영상에 항상 AVPlayer 사용";
"Anonymous" = "익명";
/* Video date filter in search
Video duration filter in search */
"Any" = "모두";
"Apply to all" = "모두 적용";
"Are you sure you want to clear search history?" = "검색 기록을 삭제하시겠습니까?";
"Are you sure you want to delete playlist?" = "플레이리스트를 삭제하시겠습니까?";
"Are you sure you want to restore default quality profiles?" = "기본 품질 프로필로 복구하시겠습니까?";
"Are you sure you want to unsubscribe from %@?" = "%@을/를 구독 해제하시겠습니까?";
"Autoplaying Next" = "다음으로 자동재생";
"Backend" = "백엔드";
"Badge" = "배지";
"Battery" = "배터리";
"Cellular" = "셀룰러";
" subscribers" = " 구독자";
"%@ Channel" = "%@ 채널";
"%lld videos" = "%lld 동영상";
"Are you sure you want to clear history of watched videos?" = "동영상 시청 기록을 삭제하시겠습니까?";
"Add Account" = "계정 추가";
"Add Account..." = "계정 추가...";
"Automatic" = "자동";
"Close" = "닫기";
"Badge color" = "배지 색상";
"Bugs and great feature ideas can be sent to the GitHub issues tracker. " = "깃허브 이슈 트래커를 통해 버그 제보 또는 개발 아이디어를 제공해주실 수 있습니다. ";
"Category" = "카테고리";
"Captions" = "자막";
"Close Video" = "동영상 닫기";
"Close player when closing video" = "동영상 종료 시 플레이어 닫기";
"Close player when starting PiP" = "PiP 시작 시 플레이어 닫기";
"Button" = "버튼";
"Cancel" = "취소";
"Based on system color scheme" = "시스템 색 구성표 기반";
"Blue" = "파란색";
"Categories to Skip" = "건너뛸 카테고리";
"Chapters" = "챕터";
"Charging" = "충전중";
"Badge & Decreased opacity" = "배지와 불투명도 감소";
"Browsing" = "탐색";
"Buffering stream..." = "스트림 버퍼링...";
"Clear" = "지우기";
"Clear All" = "모두 지우기";
"Clear All Recents" = "최근내역 모두 지우기";
"Clear History" = "기록 지우기";
"Clear Search History" = "검색기록 지우기";
"Clear Search History..." = "검색기록 지우기...";
"Clear the queue" = "대기열 지우기";
"Close PiP and open player when application enters foreground" = "애플리케이션이 포그라운드에 진입하면 PiP를 닫고 플레이어를 열기";
"Close PiP when player is opened" = "플레이어가 열리면 PiP 닫기";
"Close PiP when starting playing other video" = "다른 동영상 재생을 시작하면 PiP 닫기";

View File

@@ -82,7 +82,7 @@
"LIVE" = "Direkte";
/* Loading stream OSD */
"Loading streams..." = "Laster inn strømmer …";
"Loading streams" = "Laster inn strømmer …";
/* Video duration filter in search */
"Long" = "Lang";
@@ -223,7 +223,7 @@
"Rate" = "Takt";
"Not Playing" = "Spiller ikke";
"Open \"Playlists\" tab to create new one" = "Åpne «Spillelister»-fanen for å opprette ny";
"Opening audio stream..." = "Åpner lydstrøm …";
"Opening audio stream" = "Åpner lydstrøm …";
"Password" = "Passord";
"Nothing" = "Ingenting";
"Picture in Picture" = "Bilde-i-bilde";
@@ -303,7 +303,7 @@
"Offtopic in Music Videos" = "Urelaterte ting i musikkvideoer";
/* Loading stream OSD */
"Opening %@ stream..." = "Åpner %@-strøm …";
"Opening %@ stream" = "Åpner %@-strøm …";
"Play in PiP" = "Bilde-i-bilde";
"Pause when entering background" = "Pause ved forsendelse til bakgrunnen";
"Promoting a product or service that is directly related to the creator themselves. This usually includes merchandise or promotion of monetized platforms." = "Promoterer noe som har å gjøre med skaperen direkte. Vanligvis effekter eller betalte plattformer.";

View File

@@ -294,3 +294,240 @@
"Show anonymous accounts" = "Anonieme accounts laten zien";
"Show channel name" = "Naam van kanaal laten zien";
"Show history" = "Geschiedenis laten zien";
"Show keywords" = "Toon sleutelwoorden";
"Show progress of watching on thumbnails" = "Toon vooruitgang bovenop voorvertoning";
"Show playback statistics" = "Toon afspeelstatistieken";
"Show sidebar when space permits" = "Toon zijbalk wanneer de ruimte dit toelaat";
"Show video length" = "Toon videoduur";
"Shuffle" = "Shuffle";
"Shuffle All" = "Shuffle alles";
"Sidebar" = "Zijbalk";
"Sign In Required" = "Aanmelden vereist";
/* Player controls layout size */
"Small" = "Klein";
/* Player controls layout size */
"Smaller" = "Kleiner";
"Subscribe" = "Abonneer";
/* Subscriptions title */
"Subscriptions" = "Abonnementen";
"Switch to other public location" = "Schakel naar andere publieke locatie";
"System controls show buttons for %@" = "Knoppen systeemcontrole voor %@";
"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." = "Dat is fijn om te horen. Het is leuk om apps aan te bieden die mensen willen gebruiken. Je kan een donatie overwegen of een bijdrage leveren in de ontwikkeling van nieuwe functionaliteiten.";
"This cannot be reverted" = "Dit kan niet ongedaan worden gemaakt";
"Switch to public locations" = "Schakel naar publieke locatie";
"System controls buttons" = "Knoppen voor systeemcontrole";
"This cannot be reverted. You might need to switch between views or restart the app to see changes." = "Dit kan niet ongedaan worden gemaakt. Je zou mogelijks tussen de weergave moeten schakelen of de app opnieuw te openen om de wijzigingen te zien.";
"This information will be processed only on your device and used to connect you to the server in the specified country." = "Deze informatie zal enkel verwerkt worden binnen het toestel zelf en gebruikt worden om te verbinden met de server in het gekozen land.";
"This will remove all your custom profiles and return their default values. This cannot be reverted." = "Dit zal alle profielen verwijderen en terug naar de initiële waarden brengen. Dit kan niet ongedaan worden gemaakt.";
"Thumbnails" = "Voorvertoningen";
/* Video date filter in search */
"Today" = "Vandaag";
"Trending" = "Trending";
/* Player controls layout size for TV */
"TV" = "TV";
"unknown" = "onbekend";
"Unsubscribe" = "Afmelden";
"Upload date" = "Datum van opladen";
"URL" = "URL";
"Videos" = "Videos";
/* Video sort order in search */
"Views" = "Views";
/* Selected video is being played */
"Watching now" = "Aan het bekijken";
/* Video date filter in search */
"Week" = "Week";
"Welcome" = "Welkom";
"When partially watched video is played" = "Wanneer een gedeeltelijk bekeken video wordt afgespeeld";
"Wi-Fi" = "Wi-Fi";
"Wiki" = "Wiki";
"Yattee" = "Yattee";
"Yattee %@ (build %@)" = "Yattee %@ (build %@)";
"You can use automatic profile selection based on current device status or switch it in video playback settings controls." = "Je kan gebruik maken van een automatische profielselectie gebaseerd op de status van het toestel of maak een keuze in de aspeelinstellingen.";
"You have no Playlists" = "Je hebt geen afspeellijsten";
"You have no playlists\n\nTap on \"New Playlist\" to create one" = "Je hebt geen afspeellijsten\n\nTik op \"Nieuwe afspeellijst\" om er een aan te maken";
"You need to create an instance and accounts\nto access %@ section" = "Je moet een instance en een account aanmaken\nom toegang te krijgen tot het onderdeel %@";
"You need to select an account\nto access %@ section" = "Je moet een account selecteren\nom toegang te krijgen tot het onderdeel %@";
"Public" = "Publiek";
"Unlisted" = "Onopgelijst";
"Now Playing" = "Wordt nu afgespeeld";
"Current Location" = "Huidige Locatie";
"Private" = "Privé";
"Playback queue is empty" = "Afspeellijst is leeg";
"Playing Next" = "Speelt Hierna";
"You can switch between profiles in playback settings controls." = "Je kan schakelen tussen profielen via de afspeelinstellingen.";
"Add Channels, Playlists and Searches to Favorites using" = "Voeg Kanalen, Afspeellijsten en Zoekopdrachten toe aan Favorieten gebruik makend van";
"Make default" = "Stel in als standaard";
"Visibility" = "Zichtbaarheid";
"Current Playlist" = "Huidige Afspeellijst";
"Stream & Player" = "Stream & Speler";
"Statistics" = "Statistieken";
"Hardware decoder" = "Hardware decoder";
"Stream FPS" = "Stream FPS";
"Cached time" = "Cache duurtijd";
"Rate & Captions" = "Beoordelen & Ondertiteling";
"Dropped frames" = "Verloren frames";
"Any format" = "Elk formaat";
"%@ formats" = "%@ formaten";
"Keep last played video in the queue after restart" = "Behoud de laatst gespeelde video in de afspeellijst na herstart";
"Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "Afspeellijst is leeg\n\nTik en houd de video vast en\nkies dan \"Voeg toe aan afspeellijst\"";
"It can be changed later in settings. You can use your own locations too." = "Dit kan later aangepast worden in de instellingen. Je kan tevens je eigen locatie gebruiken.";
"Press and hold remote button to open captions and quality menus" = "Tik en houd de knop van de afstandsbediening ingedrukt om het menu van de ondertitelingen en streamkwaliteit te openen";
"Comments are disabled" = "Commentaar is uitgeschakeld";
"No comments" = "Geen commentaren";
"No chapters information available" = "Geen hoofdstukinformatie beschikbaar";
"Share Logs..." = "Deel logboek…";
"Open logs in Finder" = "Open logboek in Finder";
"Channel could not be found" = "Kanaal kan niet gevonden worden";
"Could not extract channel information" = "Kan geen informatie van het kanaal vinden";
"Could not extract SID from received cookies: %@" = "Kon de SID niet bepalen van de ontvangen cookies: %@";
"Could not update your token." = "Kan jouw token niet updaten.";
"Could not refresh Trending" = "Kon Trending niet verversen";
"Could not create share link" = "Kon geen link aanmaken om te delen";
"Could not open playlist" = "Kon de afspeellijst niet openen";
"Could not extract video ID" = "Kon de video ID niet bepalen";
"This video could not be opened" = "Deze video kon niet worden geopend";
"Could not extract playlist ID" = "Kon de playlist ID niet bepalen";
"No locations available at the moment" = "Geen locaties beschikbaar op dit moment";
"Could not refresh Playlists" = "Kon de Afspeellijsten niet verversen";
"If you want this app to be available in your language, join translation project." = "Als je deze app in jouw taal wenst te gebruiken, neem deel aan het vertalingsproject.";
"Translations" = "Vertalingen";
"No documents" = "Geen documenten";
"Recent Documents" = "Recente Documenten";
"Home" = "Home";
"Pages buttons" = "Paginas knoppen";
"Only for local files and URLs" = "Enkel voor lokale bestanden en URLs";
"Right" = "Rechts";
"Playback Mode" = "Afspeelmodus";
"Add" = "Voeg toe";
"Hide" = "Verberg";
"Always" = "Altijd";
"Channels" = "Kanalen";
"Open Files" = "Open Bestanden";
"Share" = "Deel";
"Show icons and text when space permits" = "Toon iconen en tekst wanneer de ruimte dit toelaat";
"Left" = "Links";
"Format" = "Formaat";
"Driver" = "Driver";
"Show only icons" = "Toon enkel iconen";
"Audio" = "Audio";
"File" = "Bestand";
"Documents" = "Documenten";
"Video" = "Video";
"Codec" = "Codec";
"Size" = "Grootte";
"FPS" = "FPS";
"Sample Rate" = "Sample Rate";
"Could not find any links to open in your clipboard" = "Kon geen links vinden om te openen in het klembord";
"Address" = "Adres";
"Show sidebar" = "Toon zijbalk";
"Locations Manifest" = "Locatie Manifest";
"Remove Location" = "Verwijder Locatie";
"Could not delete document" = "Kon het document niet verwijderen";
"Verified" = "Geverifiëerd";
"Channel" = "Kanaal";
"Open expanded" = "Open uitgeklapt";
"Mark channel feed as unwatched" = "Markeer het kanaal als onbekeken";
"Mark channel feed as watched" = "Markeer het kanaal als bekeken";
"Short videos: visible" = "Korte videos: zichtbaar";
"Player Bar" = "Afspeelbalk";
"Short videos: hidden" = "Korte videos: verborgen";
"Play all unwatched" = "Speel alle onbekeken af";
"Seeking" = "Scrollen";
"System controls" = "Systeembediening";
"Controls button: backwards" = "Bedieningsknop: terug";
/* Loading stream OSD */
"Loading streams…" = "Streams laden…";
/* Loading stream OSD */
"Opening %@ stream…" = "Stream %@ openen…";
"Opening audio stream…" = "Audio stream openen…";
"Sort" = "Sorteer";
"Sort: %@" = "Sorteer: %@";
/* SponsorBlock category name */
"Sponsor" = "Sponsor";
"SponsorBlock" = "SponsorBlock";
"Source" = "Bron";
"SponsorBlock API Instance" = "SponsorBlock API Instance";
"Username" = "Gebruikersnaam";
/* Player controls layout size */
"Very Large" = "Heel Groot";
"Watched" = "Bekeken";
/* Selected video was played on given date */
"Watched %@" = "%@ Bekeken";
/* Video date filter in search */
"Year" = "Jaar";
"Typically near or at the end of the video when the credits pop up and/or endcards are shown." = "Typisch dicht bij of op het einde van een video wanneer de aftiteling getoond wordt.";
"Used to create links from videos, channels and playlists" = "Gebruikt om links te maken van videos, kanalen en afspeellijsten";
"You can find information about using Yattee in the Wiki pages." = "Je vindt informatie over het gebruik van Yattee in de Wiki.";
"Share files from Finder on a Mac\nor iTunes on Windows" = "Deel bestanden van Finder op Mac\nof iTunes op Windows";
"Show Favorites" = "Toon Favorieten";
"Inspector visibility" = "Inspector zichtbaarheid";
"Edit Favorites…" = "Pas Favorieten aan…";
"Show Open Videos toolbar button" = "Toon Open Videos werkbalkknop";
"Show Inspector" = "Toon Inspector";
"Open" = "Open";
"Open Videos" = "Open Videos";
"Enter links to open, one per line" = "Voeg links toe om te openen, één per lijn";
"Could not refresh Subscriptions" = "Kan Abonnementen niet verversen";
"Show Home" = "Toon Home";
"Recent History" = "Recente Geschiedenis";
"Enter link to open" = "Voeg de link in om te openen";
"Show Open Videos quick actions" = "Toon Open Videos snelle acties";
"Files" = "Bestanden";
"Could not open Files" = "Kon Bestanden niet openen";
"Could not load streams" = "Kan de streams niet laden";
"Could not open video" = "Kan de video niet openen";
"Buttons labels" = "Knoppen labels";
"Show Documents" = "Toon Documenten";
"Video Details" = "Video Eigenschappen";
"Clear Queue before opening" = "Maak de wachtrij leeg voor het openen";
"For custom locations you can configure Frontend URL in Locations settings" = "Voor aangepaste locaties kan je de URL instellen in de locatie instellingen";
"This URL could not be opened" = "Deze URL kon niet geopend worden";
"Could not load video" = "Kon de video niet laden";
"Pages toolbar position" = "Paginas werkbalk positie";
"Reload manifest" = "Ververs het manifest";
"Video actions buttons" = "Video actie knoppen";
"URL to Open" = "URL om te Openen";
"Paste" = "Plakken";
"Share%@link" = "Deel %@link";
"Could not open channel" = "Kan het Kanaal niet openen";
"Could not refresh Popular" = "Kon Populair niet verversen";
"Center" = "Midden";
"Open Video" = "Open Video";
"Default Profile" = "Standaard Profiel";
"Playback history is empty" = "Afspeelgeschiedenis is leeg";
"Shorts" = "Shorts";
"Double tap gesture" = "Dubbele tik gebaar";
"Tap and hold channel thumbnail to open context menu with more actions" = "Tik en houd voorvertoningsafbeelding vast om het contextmenu te openen met meer acties";
"Always show controls buttons" = "Toon altijd bedieningsknoppen";
"Single tap gesture" = "Enkel tikgebaar";
"Maximum width expanded" = "Maximale breedte uitgeklapt";
"Clear all" = "Verwijder alle";
"Show unwatched feed badges" = "Toon onbekeken feed badges";
"Controls button: forwards" = "Bedieningsknop: voorwaarts";
"Remove…" = "Verwijder…";
"Actions buttons" = "Actieknoppen";
"\"%@\" will be irreversibly removed from this device." = "\"%@\" zal onherroepelijk verwijderd worden van dit toestel.";
"Right click channel thumbnail to open context menu with more actions" = "Rechts klikken op voorvertoningsafbeelding om de contextmenu te openen met meer acties";
"Gesture: backwards" = "Gebaar: achteruit";
"Copy%@link" = "Kopieer %@link";
"Live Streams" = "Livestreams";
"Gesture: fowards" = "Gebaar: voorwaarts";
"Controls Buttons" = "Bedieningsknoppen";
"Are you sure you want to remove this document?" = "Ben je zeker om dit document te verwijderen?";
"Are you sure you want to remove %@ location?" = "Ben je zeker om locatie %a te verwijderen?";

View File

@@ -157,7 +157,7 @@
"LIVE" = "LIVE";
/* Loading stream OSD */
"Loading streams..." = "Ładowanie strumieni...";
"Loading streams" = "Ładowanie strumieni";
"Loading..." = "Ładowanie…";
"Locations" = "Lokalizacje";
"Lock portrait mode" = "Zablokuj tryb portretowy";
@@ -202,8 +202,8 @@
"Open Settings" = "Otwórz Ustawienia";
/* Loading stream OSD */
"Opening %@ stream..." = "Otwieranie strumienia %@…";
"Opening audio stream..." = "Otwieranie strumienia audio...";
"Opening %@ stream" = "Otwieranie strumienia %@…";
"Opening audio stream" = "Otwieranie strumienia audio";
"Orientation" = "Orientacja";
/* SponsorBlock category name */
@@ -570,7 +570,7 @@
"Enter location address to connect..." = "Wprowadź adres lokalizacji, aby połączyć...";
"Opened File" = "Otwarty plik";
"File Extension" = "Rozszerzenie pliku";
"Opening file..." = "Otwieranie pliku...";
"Opening file" = "Otwieranie pliku";
"Public account" = "Konto publiczne";
"Your Accounts" = "Twoje konta";
"Browse without account" = "Przeglądanie bez konta";

View File

@@ -102,7 +102,7 @@
"Just watched" = "Acabou de assistir";
/* Loading stream OSD */
"Loading streams..." = "Carregando streams…";
"Loading streams" = "Carregando streams…";
"Medium quality" = "Qualidade média";
"No description" = "Sem descrição";
"No Playlists" = "Sem playlists";
@@ -114,8 +114,8 @@
"Open Settings" = "Abrir Ajustes";
/* Loading stream OSD */
"Opening %@ stream..." = "Abrindo stream %@…";
"Opening audio stream..." = "Abrindo stream de áudio…";
"Opening %@ stream" = "Abrindo stream %@…";
"Opening audio stream" = "Abrindo stream de áudio…";
"Orientation" = "Orientação";
/* SponsorBlock category name */
@@ -406,7 +406,7 @@
"Country" = "País";
"Clear All" = "Limpar Tudo";
"Clear All Recents" = "Limpar Todos os Recentes";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Lembretes explícitos de dar like, se inscrever ou interagir com eles em qualquer plataforma, paga ou grátis (p.ex. clicar em um vídeo).\n";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Lembretes explícitos de dar like, se inscrever ou interagir com eles em qualquer plataforma, paga ou grátis (p.ex. clicar em um vídeo).";
"Duration" = "Duração";
"Edit Quality Profile" = "Editar Perfil de Qualidade";
"Discussions take place in Discord and Matrix. It's a good spot for general questions." = "Discussões acontecem no Discord e no Matrix. É um bom lugar para perguntas gerais.";
@@ -569,7 +569,7 @@
"Enter account credentials to connect..." = "Insira as credenciais da conta para conectar…";
"Opened File" = "Arquivo Aberto";
"File Extension" = "Extensão do Arquivo";
"Opening file..." = "Abrindo arquivo…";
"Opening file" = "Abrindo arquivo…";
"Browse without account" = "Navegar sem uma conta";
"Rotate when entering fullscreen on landscape video" = "Girar quando entrar no modo tela cheia em vídeo em paisagem";
"Landscape left" = "Paisagem à esquerda";

View File

@@ -196,7 +196,7 @@
"LIVE" = "AO VIVO";
/* Loading stream OSD */
"Loading streams..." = "Carregando streams…";
"Loading streams" = "Carregando streams…";
"Loading..." = "Carregando…";
"Locations" = "Localizações";
"Lock portrait mode" = "Travar modo retrato";
@@ -237,8 +237,8 @@
"Open Settings" = "Abrir Ajustes";
/* Loading stream OSD */
"Opening %@ stream..." = "Abrindo stream %@…";
"Opening audio stream..." = "Abrindo stream de áudio…";
"Opening %@ stream" = "Abrindo stream %@…";
"Opening audio stream" = "Abrindo stream de áudio…";
"Orientation" = "Orientação";
/* SponsorBlock category name */
@@ -557,7 +557,7 @@
"Keep channels with unwatched videos on top of subscriptions list" = "Manter canais com vídeos não vistos no topo da lista de inscrições";
"Opened File" = "Ficheiro Aberto";
"File Extension" = "Extensão do Ficheiro";
"Opening file..." = "A abrir ficheiro…";
"Opening file" = "A abrir ficheiro…";
"Close video and player on end" = "Fechar vídeo e player ao final";
"Use system controls with AVPlayer" = "Usar controles do sistema com o AVPlayer";
"Public account" = "Conta pública";

View File

@@ -62,7 +62,7 @@
"Edit" = "Editați";
"Edit Playlist" = "Editați Playlist";
"Enter fullscreen in landscape" = "Introduceți ecranul complet în peisaj";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Mementouri explicite de a aprecia, de a vă abona sau de a interacționa cu ele pe orice platformă (platforme) plătite sau gratuite (de exemplu, faceți clic pe un videoclip).\n";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Mementouri explicite de a aprecia, de a vă abona sau de a interacționa cu ele pe orice platformă (platforme) plătite sau gratuite (de exemplu, faceți clic pe un videoclip).";
"Find Other" = "Găsiți alte";
"Finding something to play..." = "Să găsești ceva de jucat...";
"For videos which feature music as the primary content." = "Pentru videoclipurile care includ muzica ca conținut principal.";
@@ -89,7 +89,7 @@
"LIVE" = "LIVE";
/* Loading stream OSD */
"Loading streams..." = "Se încarcă fluxurile...";
"Loading streams" = "Se încarcă fluxurile";
"Locations" = "Locații";
"Mark watched videos with" = "Marcați videoclipurile vizionate cu";
"Matrix Channel" = "Canal Matrix";
@@ -100,7 +100,7 @@
"Nothing" = "Nimic";
/* Loading stream OSD */
"Opening %@ stream..." = "Se deschide %@ flux...";
"Opening %@ stream" = "Se deschide %@ flux";
"Play Last" = "Reda ultimul";
"Player" = "Player";
"Playlist" = "Playlist";
@@ -243,7 +243,7 @@
/* SponsorBlock category name */
"Outro" = "Outro";
"Orientation" = "Orientare";
"Opening audio stream..." = "Se deschide fluxul audio...";
"Opening audio stream" = "Se deschide fluxul audio";
"Password" = "Parolă";
"Pause" = "Pauză";
"Pause when entering background" = "Pauză când intrați în fundal";
@@ -568,7 +568,7 @@
"Enter account credentials to connect..." = "Introduceți acreditările contului pentru a vă conecta...";
"Enter location address to connect..." = "Introdu adresa locației pentru a te conecta...";
"Opened File" = "Fișier deschis";
"Opening file..." = "Deschiderea fișierului...";
"Opening file" = "Deschiderea fișierului";
"File Extension" = "Extensie fișier";
"Use system controls with AVPlayer" = "Utilizați controalele de sistem cu AVPlayer";
"Rotate when entering fullscreen on landscape video" = "Rotiți când intrați pe ecran complet în videoclipul peisaj";

View File

@@ -370,7 +370,7 @@
"LIVE" = "ПРЯМАЯ ТРАНСЛЯЦИЯ";
/* Loading stream OSD */
"Loading streams..." = "Загрузка прямой трансляции...";
"Loading streams" = "Загрузка прямой трансляции";
"Loading..." = "Загрузка...";
"Lock portrait mode" = "Блокировка портретного режима";
"Low" = "Низкое";
@@ -405,8 +405,8 @@
"Open Settings" = "Отрыть настройки";
/* Loading stream OSD */
"Opening %@ stream..." = "Открытие %@ прямой трансляции...";
"Opening audio stream..." = "Открытие прямой трансляции аудио...";
"Opening %@ stream" = "Открытие %@ прямой трансляции";
"Opening audio stream" = "Открытие прямой трансляции аудио";
"Orientation" = "Ориентация";
/* SponsorBlock category name */
@@ -591,7 +591,7 @@
"Actions buttons" = "Кнопки действия";
"Show sidebar" = "Показать боковую панель";
"Browse without account" = "Искать без аккаунта";
"Opening file..." = "Отрытие файла...";
"Opening file" = "Отрытие файла";
"Public account" = "Публичный аккаунт";
"Your Accounts" = "Ваши аккаунты";
"Close video and player on end" = "Закрыть видео и плеер в конце";

View File

@@ -110,7 +110,7 @@
"I want to ask a question" = "Bir soru sormak istiyorum";
/* Loading stream OSD */
"Loading streams..." = "Akışlar yükleniyor...";
"Loading streams" = "Akışlar yükleniyor";
"Edit Quality Profile" = "Kalite Profilini Düzenle";
"Frontend URL" = "Ön uç URL'si";
"Close player when starting PiP" = "Resim içinde Resim modu başlatılırken oynatıcıyı kapat";
@@ -227,7 +227,7 @@
"No description" = "Açıklama yok";
"Normal" = "Normal";
"Open \"Playlists\" tab to create new one" = "Yeni bir tane oluşturmak için \"Oynatma Listeleri\" sekmesini açın";
"Opening audio stream..." = "Ses akışıılıyor...";
"Opening audio stream" = "Ses akışıılıyor";
"Rate" = "Derecelendir";
"Orientation" = "Yönlendirme";
"No results" = "Sonuç yok";
@@ -396,7 +396,7 @@
"Open" = "Aç";
/* Loading stream OSD */
"Opening %@ stream..." = "%@ akışıılıyor...";
"Opening %@ stream" = "%@ akışıılıyor";
"Clear Queue before opening" = "Açmadan önce Bekleme Listesini temizle";
"Copy %@ link with time" = "Bağlantıyı %@ zaman ile kopyala";
"Show Inspector" = "Denetleyiciyi Göster";

View File

@@ -259,7 +259,7 @@
"LIVE" = "В ЕФІРІ";
/* Loading stream OSD */
"Loading streams..." = "Завантаження трансляції...";
"Loading streams" = "Завантаження трансляції";
"Loading..." = "Завантаження...";
"Locations" = "Локації";
"Lock portrait mode" = "Заблокувати портретний режим";
@@ -298,8 +298,8 @@
"Open Settings" = "Відрити налаштування";
/* Loading stream OSD */
"Opening %@ stream..." = "Запуск трансляції %@...";
"Opening audio stream..." = "Запуск аудіо трансляції...";
"Opening %@ stream" = "Запуск трансляції %@";
"Opening audio stream" = "Запуск аудіо трансляції";
"Orientation" = "Орієнтація";
/* SponsorBlock category name */

View File

@@ -152,7 +152,7 @@
"Loading..." = "加载中...";
/* Loading stream OSD */
"Loading streams..." = "加载流中...";
"Loading streams" = "加载流中";
"Locations" = "地址";
"Lock portrait mode" = "锁定竖屏模式";
@@ -191,8 +191,8 @@
"Open Settings" = "打开设置";
/* Loading stream OSD */
"Opening %@ stream..." = "正在打开 %@ 的流...";
"Opening audio stream..." = "正在打开音频流...";
"Opening %@ stream" = "正在打开 %@ 的流";
"Opening audio stream" = "正在打开音频流";
"Orientation" = "方向";
/* SponsorBlock category name */
@@ -530,7 +530,7 @@
"Show scroll to top button in comments" = "在评论中显示“滚动到顶部”按钮";
"Opened File" = "打开的文件";
"File Extension" = "文件扩展";
"Opening file..." = "打开文件中...";
"Opening file" = "打开文件中";
"Single tap gesture" = "单击手势";
"Right click channel thumbnail to open context menu with more actions" = "右键单击频道缩略图以打开具有更多操作的上下文菜单";
"Show unwatched feed badges" = "显示未观看的 Feed 标志";

View File

@@ -252,7 +252,7 @@
"Interface" = "介面";
/* Loading stream OSD */
"Loading streams..." = "加載中...";
"Loading streams" = "加載中";
"Loading..." = "加載中...";
"Locations" = "地址";
"Lock portrait mode" = "鎖定直屏";
@@ -292,8 +292,8 @@
"Open Settings" = "打開設置";
/* Loading stream OSD */
"Opening %@ stream..." = "正在打開 %@ ...";
"Opening audio stream..." = "正在打開音訊...";
"Opening %@ stream" = "正在打開 %@ ";
"Opening audio stream" = "正在打開音訊";
/* SponsorBlock category name */
"Outro" = "結尾";
@@ -554,7 +554,7 @@
"Queue - shuffled" = "隊列 - 隨機";
"Loop one" = "單個循環";
"File Extension" = "副檔名";
"Opening file..." = "正在打開文件...";
"Opening file" = "正在打開文件";
"Public account" = "公共帳戶";
"Enter account credentials to connect..." = "輸入帳號密碼來連接...";
"Enter location address to connect..." = "輸入站台地址來連接...";

View File

@@ -1070,6 +1070,18 @@
37FFC440272734C3009FFD26 /* Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FFC43F272734C3009FFD26 /* Throttle.swift */; };
37FFC441272734C3009FFD26 /* Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FFC43F272734C3009FFD26 /* Throttle.swift */; };
37FFC442272734C3009FFD26 /* Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FFC43F272734C3009FFD26 /* Throttle.swift */; };
E24DC6582BFA124100BF6187 /* UserAgentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24DC6572BFA124100BF6187 /* UserAgentManager.swift */; };
E24DC6592BFA124100BF6187 /* UserAgentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24DC6572BFA124100BF6187 /* UserAgentManager.swift */; };
E24DC65A2BFA124100BF6187 /* UserAgentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24DC6572BFA124100BF6187 /* UserAgentManager.swift */; };
E25028B02BF790F5002CB9FC /* HTTPStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */; };
E25028B12BF790F5002CB9FC /* HTTPStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */; };
E25028B22BF790F5002CB9FC /* HTTPStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */; };
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 */; };
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 */
@@ -1539,6 +1551,10 @@
3DA101AD287C30F50027D920 /* DEVELOPMENT_TEAM.template.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DEVELOPMENT_TEAM.template.xcconfig; sourceTree = "<group>"; };
3DA101AE287C30F50027D920 /* DEVELOPMENT_TEAM.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DEVELOPMENT_TEAM.xcconfig; sourceTree = "<group>"; };
3DA101AF287C30F50027D920 /* Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Shared.xcconfig; sourceTree = "<group>"; };
E24DC6572BFA124100BF6187 /* UserAgentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgentManager.swift; sourceTree = "<group>"; };
E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPStatus.swift; sourceTree = "<group>"; };
E258F3892BF61BD2005B8C28 /* URLTester.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLTester.swift; sourceTree = "<group>"; };
E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageCodes.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -2286,6 +2302,8 @@
372915E52687E3B900F5A35B /* Defaults.swift */,
37D2E0D328B67EFC00F64D52 /* Delay.swift */,
3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */,
E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */,
E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */,
375B537828DF6CBB004C1D19 /* Localizable.strings */,
3729037D2739E47400EA99F6 /* MenuCommands.swift */,
37B7958F2771DAE0001CF27B /* OpenURLHandler.swift */,
@@ -2293,6 +2311,8 @@
3700155E271B12DD0049C794 /* SiestaConfiguration.swift */,
37FFC43F272734C3009FFD26 /* Throttle.swift */,
378FFBC328660172009E3FBE /* URLParser.swift */,
E258F3892BF61BD2005B8C28 /* URLTester.swift */,
E24DC6572BFA124100BF6187 /* UserAgentManager.swift */,
37D4B0C22671614700C925CA /* YatteeApp.swift */,
37D4B0C42671614800C925CA /* Assets.xcassets */,
37BD07C42698ADEE003EBB87 /* Yattee.entitlements */,
@@ -3115,6 +3135,7 @@
37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */,
37F7D82C289EB05F00E2B3D0 /* SettingsPickerModifier.swift in Sources */,
375EC95D289EEEE000751258 /* QualityProfile.swift in Sources */,
E25028B02BF790F5002CB9FC /* HTTPStatus.swift in Sources */,
3773B8102ADC076800B5FEF3 /* ScrollViewMatcher.swift in Sources */,
371B7E662759786B00D21217 /* Comment+Fixtures.swift in Sources */,
37BE0BD326A1D4780092E2DB /* AppleAVPlayerView.swift in Sources */,
@@ -3138,6 +3159,8 @@
377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */,
3752069D285E910600CA655F /* ChapterView.swift in Sources */,
375EC96A289F232600751258 /* QualityProfilesModel.swift in Sources */,
E24DC6582BFA124100BF6187 /* UserAgentManager.swift in Sources */,
E258F38A2BF61BD2005B8C28 /* URLTester.swift in Sources */,
3751B4B227836902000B7DF4 /* SearchPage.swift in Sources */,
37CC3F4C270CFE1700608308 /* PlayerQueueView.swift in Sources */,
37FFC440272734C3009FFD26 /* Throttle.swift in Sources */,
@@ -3213,6 +3236,7 @@
37C3A24D272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */,
37484C2D26FC844700287258 /* InstanceSettings.swift in Sources */,
37DD9DA32785BBC900539416 /* NoCommentsView.swift in Sources */,
E27568B92BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */,
377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */,
377ABC48286E5887009C986F /* Sequence+Unique.swift in Sources */,
@@ -3393,6 +3417,7 @@
371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */,
3756C2AB2861151C00E4B059 /* NetworkStateModel.swift in Sources */,
375EC95A289EEB8200751258 /* QualityProfileForm.swift in Sources */,
E27568BA2BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */,
37001564271B1F250049C794 /* AccountsModel.swift in Sources */,
378FFBC528660172009E3FBE /* URLParser.swift in Sources */,
3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
@@ -3401,6 +3426,7 @@
3743CA4F270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
374C053C2724614F009BDDBE /* PlayerTVMenu.swift in Sources */,
377FC7DD267A081A00A6BBAF /* PopularView.swift in Sources */,
E24DC6592BFA124100BF6187 /* UserAgentManager.swift in Sources */,
374924DB2921050B0017D862 /* LocationsSettings.swift in Sources */,
371AC0A0294D13AA0085989E /* UnwatchedFeedCountModel.swift in Sources */,
379E7C342A20FE3900AF8118 /* FocusableSearchTextField.swift in Sources */,
@@ -3587,6 +3613,7 @@
37D4B19826717E1500C925CA /* Video.swift in Sources */,
371B7E5D27596B8400D21217 /* Comment.swift in Sources */,
37EF5C232739D37B00B03725 /* MenuModel.swift in Sources */,
E25028B12BF790F5002CB9FC /* HTTPStatus.swift in Sources */,
37A7D71C2B680E66009CB1ED /* LocationsSettingsGroupExporter.swift in Sources */,
37BC50A92778A84700510953 /* HistorySettings.swift in Sources */,
374DE88128BB896C0062BBF2 /* PlayerDragGesture.swift in Sources */,
@@ -3621,6 +3648,7 @@
378E9C39294552A700B2D696 /* ThumbnailView.swift in Sources */,
370F4FAB27CC164D001B35DC /* PlayerControlsModel.swift in Sources */,
37E8B0ED27B326C00024006F /* TimelineView.swift in Sources */,
E258F38B2BF61BD2005B8C28 /* URLTester.swift in Sources */,
370E990B2A1EA8C500D144E9 /* WatchModel.swift in Sources */,
3717407E2949D40800FDDBC7 /* ChannelLinkView.swift in Sources */,
37FB28422721B22200A57617 /* ContentItem.swift in Sources */,
@@ -3864,8 +3892,10 @@
37BA221329526A19000DAD1F /* ControlsGradientView.swift in Sources */,
374C0541272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */,
37130A61277657300033018A /* PersistenceController.swift in Sources */,
E24DC65A2BFA124100BF6187 /* UserAgentManager.swift in Sources */,
37E70929271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */,
3717407F2949D40800FDDBC7 /* ChannelLinkView.swift in Sources */,
E25028B22BF790F5002CB9FC /* HTTPStatus.swift in Sources */,
370F4FAA27CC163B001B35DC /* PlayerBackend.swift in Sources */,
376A33E62720CB35000C1D6B /* Account.swift in Sources */,
3763C98B290C7A50004D3B5F /* OpenVideosView.swift in Sources */,
@@ -3908,6 +3938,7 @@
37B2631C2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */,
37484C2B26FC83FF00287258 /* AccountForm.swift in Sources */,
37FB2860272225E800A57617 /* ContentItemView.swift in Sources */,
E258F38C2BF61BD2005B8C28 /* URLTester.swift in Sources */,
37F7D82E289EB05F00E2B3D0 /* SettingsPickerModifier.swift in Sources */,
374AB3DD28BCAF7E00DF56FB /* SeekType.swift in Sources */,
374C053727242D9F009BDDBE /* SponsorBlockSettings.swift in Sources */,
@@ -3984,6 +4015,7 @@
37FB28432721B22200A57617 /* ContentItem.swift in Sources */,
37A7D6EF2B67E3BF009CB1ED /* BrowsingSettingsGroupImporter.swift in Sources */,
37D2E0D228B67DBC00F64D52 /* AnimationCompletionObserverModifier.swift in Sources */,
E27568BB2BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */,
37A7D6EB2B67E334009CB1ED /* BrowsingSettingsGroupExporter.swift in Sources */,
37AAF2A226741C97007FC770 /* FeedView.swift in Sources */,
37484C1B26FC837400287258 /* PlayerSettings.swift in Sources */,
@@ -4071,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 = 182;
CURRENT_PROJECT_VERSION = 187;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
@@ -4102,7 +4134,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 182;
CURRENT_PROJECT_VERSION = 187;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist";
@@ -4133,7 +4165,7 @@
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 182;
CURRENT_PROJECT_VERSION = 187;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 11.0;
@@ -4153,7 +4185,7 @@
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 182;
CURRENT_PROJECT_VERSION = 187;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 11.0;
@@ -4317,7 +4349,7 @@
CODE_SIGN_ENTITLEMENTS = "iOS/Yattee (iOS).entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 182;
CURRENT_PROJECT_VERSION = 187;
ENABLE_PREVIEWS = YES;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
@@ -4370,7 +4402,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 182;
CURRENT_PROJECT_VERSION = 187;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
ENABLE_PREVIEWS = YES;
GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1";
@@ -4422,7 +4454,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 182;
CURRENT_PROJECT_VERSION = 187;
DEAD_CODE_STRIPPING = YES;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
@@ -4461,7 +4493,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 182;
CURRENT_PROJECT_VERSION = 187;
DEAD_CODE_STRIPPING = YES;
"DEVELOPMENT_TEAM[sdk=macosx*]" = 78Z5H3M6RJ;
ENABLE_APP_SANDBOX = YES;
@@ -4496,7 +4528,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 182;
CURRENT_PROJECT_VERSION = 187;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4520,7 +4552,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 182;
CURRENT_PROJECT_VERSION = 187;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4546,7 +4578,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 182;
CURRENT_PROJECT_VERSION = 187;
DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4571,7 +4603,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 182;
CURRENT_PROJECT_VERSION = 187;
DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4597,7 +4629,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 182;
CURRENT_PROJECT_VERSION = 187;
DEVELOPMENT_ASSET_PATHS = "";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -4637,7 +4669,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 182;
CURRENT_PROJECT_VERSION = 187;
DEVELOPMENT_ASSET_PATHS = "";
"DEVELOPMENT_TEAM[sdk=appletvos*]" = 78Z5H3M6RJ;
ENABLE_PREVIEWS = YES;
@@ -4678,7 +4710,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 182;
CURRENT_PROJECT_VERSION = 187;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -4702,7 +4734,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 182;
CURRENT_PROJECT_VERSION = 187;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",

View File

@@ -25,7 +25,7 @@
"location" : "https://github.com/hyperoslo/Cache.git",
"state" : {
"branch" : "master",
"revision" : "f44a8f6b5ec27730198725ccc542fef0d1cc6b3d"
"revision" : "dff9930c559aa2d1f7ed818d490d30c8852f57a6"
}
},
{
@@ -87,8 +87,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/ashleymills/Reachability.swift",
"state" : {
"revision" : "57da4b1270cab7c2228919eabc0e4e1bf93e48ea",
"version" : "5.2.2"
"revision" : "7cbd73f46a7dfaeca079e18df7324c6de6d1834a",
"version" : "5.2.3"
}
},
{
@@ -105,8 +105,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImage",
"state" : {
"revision" : "5642d1ffe3dbe628592443bd14154e31929727b4",
"version" : "5.19.2"
"revision" : "be0bcd7823ce56629948491f2eaeaa19979514f7",
"version" : "5.19.4"
}
},
{