Compare commits

...

113 Commits

Author SHA1 Message Date
Arkadiusz Fal
8b0c9d3d0a Fix syntax 2024-02-02 11:48:03 +01:00
Arkadiusz Fal
371471ad81 Fix syntax 2024-02-02 11:38:01 +01:00
Arkadiusz Fal
d5464186af Use old Previews syntax 2024-02-02 11:31:34 +01:00
Arkadiusz Fal
f4c310846a Update CHANGELOG 2024-02-02 11:16:27 +01:00
Arkadiusz Fal
2413526d70 Style fixes 2024-02-02 11:16:27 +01:00
Arkadiusz Fal
55f4a4a2a1 Fix possible crash 2024-02-02 11:16:27 +01:00
Arkadiusz Fal
5b35c03bc5 Add MPV deinterlace filter setting
Fix #601
2024-02-02 11:16:27 +01:00
Arkadiusz Fal
93ea943c54 Add action buttons label setting 2024-02-02 11:16:27 +01:00
Arkadiusz Fal
5ae6f321cd Update dependencies 2024-02-02 11:16:27 +01:00
Arkadiusz Fal
2be6f04e37 Import export settings 2024-02-02 11:16:27 +01:00
Arkadiusz Fal
9826ee4d36 Remove old references 2024-02-02 11:06:32 +01:00
Arkadiusz Fal
39a109216b Style fix 2024-02-02 11:06:32 +01:00
Arkadiusz Fal
05b25b65bc Use mpvkit 0.37.0 2024-02-02 11:06:32 +01:00
Mohammed Al Otaibi
195db01602 Translated using Weblate (Arabic)
Currently translated at 100.0% (537 of 537 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ar/
2024-02-02 11:06:32 +01:00
maboroshin
292af65ea5 Translated using Weblate (Japanese)
Currently translated at 99.6% (535 of 537 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ja/
2024-02-02 11:06:32 +01:00
Mohammed Al Otaibi
5ee46fe87a Translated using Weblate (Arabic)
Currently translated at 100.0% (537 of 537 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ar/
2024-02-02 11:06:32 +01:00
Arkadiusz Fal
179b4358ae Bump build number to 177 2024-02-02 11:06:32 +01:00
Arkadiusz Fal
5be8a663e0 Update CHANGELOG 2024-02-02 11:06:32 +01:00
Arkadiusz Fal
1d81f710a9 Bump build number to 176 2024-02-02 11:06:32 +01:00
Arkadiusz Fal
49e051c70d Use mpvkit 0.36.0 2024-02-02 11:06:31 +01:00
Arkadiusz Fal
1efaed4541 Use fastlane fork with tvos certs fix 2024-02-02 11:06:31 +01:00
Arkadiusz Fal
3e96001511 Bump build number to 175 2024-02-02 11:06:31 +01:00
Arkadiusz Fal
6e8fb4a6db Update CHANGELOG 2024-02-02 11:06:31 +01:00
Arkadiusz Fal
446ee0ac8e Update dependencies 2024-02-02 11:06:31 +01:00
Arkadiusz Fal
f6a89c7daf Bump version number to 1.5.2 2024-02-02 11:06:31 +01:00
Karel van Klink
ad5dc8a871 Translated using Weblate (Dutch)
Currently translated at 41.8% (225 of 537 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/nl/
2024-01-20 14:02:05 +01:00
Mohammed Al Otaibi
afaeb754ca Translated using Weblate (Arabic)
Currently translated at 100.0% (537 of 537 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ar/
2024-01-19 07:00:27 +01:00
OneiMoment
21fd92aea4 Translated using Weblate (Arabic)
Currently translated at 100.0% (537 of 537 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ar/
2024-01-18 06:13:06 +01:00
jonnysemon
02e5749fc9 Translated using Weblate (Arabic)
Currently translated at 100.0% (537 of 537 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ar/
2024-01-18 06:13:06 +01:00
OneiMoment
d44f80bd53 Translated using Weblate (Arabic)
Currently translated at 100.0% (537 of 537 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ar/
2024-01-15 13:06:27 +00:00
cdguy
5c87916785 Translated using Weblate (Turkish)
Currently translated at 62.3% (335 of 537 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/tr/
2024-01-15 13:06:26 +00:00
Arkadiusz Fal
043443fb89 Merge pull request #600 from yattee/chore/update-packages
Chore/update packages
2024-01-14 13:14:10 +01:00
Arkadiusz Fal
1bc44afde6 Update dependencies 2024-01-14 12:56:06 +01:00
Arkadiusz Fal
d80101d779 Update CHANGELOG 2024-01-14 12:53:52 +01:00
Arkadiusz Fal
39925c390a Bump build number to 174 2024-01-14 12:53:52 +01:00
Arkadiusz Fal
9c51f24d3f Bump build number to 173 2024-01-13 10:29:48 +01:00
Arkadiusz Fal
c231546b5c Revert "Update packages"
This reverts commit 4d953b0871.

Dependecies issue:

[09:16:19]: ▸ local binary target 'Libshaderc_combined' does not contain expected binary artifact named 'Libshaderc_combined'local binary target 'Libplacebo' does not contain expected binary artifact named 'Libplacebo'2024-01-13 09:16:19.223 xcodebuild[5250:23030] Writing error result bundle to /var/folders/mm/pltwc2yj1jx192t6dzy9zsrr0000gn/T/ResultBundle_2024-13-01_09-16-0019.xcresult
[09:16:20]: ▸ fatalErrorxcodebuild: error: Could not resolve package dependencies:
[09:16:20]: ▸   local binary target 'Libshaderc_combined' does not contain expected binary artifact named 'Libshaderc_combined'
[09:16:20]: ▸   local binary target 'Libplacebo' does not contain expected binary artifact named 'Libplacebo'
[09:16:20]: ▸   fatalError
2024-01-13 10:17:45 +01:00
Arkadiusz Fal
58c43acb2b Update CHANGELOG 2024-01-13 10:12:14 +01:00
Arkadiusz Fal
4d953b0871 Update packages 2024-01-13 10:12:14 +01:00
Arkadiusz Fal
101fee9a37 Revert "Enable external distribution"
This reverts commit 51e5aeec13.
2024-01-13 10:12:14 +01:00
Arkadiusz Fal
a44bbe4cec Merge pull request #599 from weblate/weblate-yattee-localizable-strings
Translations update from Hosted Weblate
2024-01-13 10:11:58 +01:00
OneiMoment
3f0eec3c54 Translated using Weblate (Arabic)
Currently translated at 100.0% (537 of 537 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ar/
2024-01-13 00:06:25 +01:00
mrtag-oss
ea0f52ebe0 Translated using Weblate (Russian)
Currently translated at 99.6% (535 of 537 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ru/
2024-01-11 19:06:37 +01:00
OneiMoment
a82bdd2a00 Translated using Weblate (Arabic)
Currently translated at 100.0% (537 of 537 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ar/
2024-01-11 19:06:37 +01:00
Arkadiusz Fal
b2b8565635 Update fastlane only 2024-01-09 18:14:58 +01:00
Arkadiusz Fal
a061c1c040 Revert "Update packages"
This reverts commit 06aafc1719.
2024-01-09 18:14:24 +01:00
Arkadiusz Fal
0182faceae Bump build number to 172 2024-01-09 17:03:07 +01:00
Arkadiusz Fal
3b4e594fcf Update CHANGELOG 2024-01-09 17:02:55 +01:00
Arkadiusz Fal
51e5aeec13 Enable external distribution 2024-01-09 17:02:55 +01:00
Arkadiusz Fal
06aafc1719 Update packages 2024-01-09 17:02:54 +01:00
Arkadiusz Fal
f468fa6340 Merge pull request #587 from weblate/weblate-yattee-localizable-strings
Translations update from Hosted Weblate
2024-01-09 16:55:41 +01:00
M K
eb85e3b731 Translated using Weblate (Dutch)
Currently translated at 10.2% (55 of 537 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/nl/
2024-01-09 16:55:22 +01:00
M K
935f5cc75e Added translation using Weblate (Dutch) 2024-01-09 16:55:22 +01:00
ssantos
bdbd23d66b Translated using Weblate (Portuguese)
Currently translated at 100.0% (537 of 537 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/pt/
2024-01-09 16:55:22 +01:00
maboroshin
e4796b08b6 Translated using Weblate (Japanese)
Currently translated at 99.2% (533 of 537 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ja/
2024-01-09 16:55:22 +01:00
Ophiushi
19bc9197ec Translated using Weblate (French)
Currently translated at 100.0% (537 of 537 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/fr/
2024-01-09 16:55:22 +01:00
mere
c513753f59 Translated using Weblate (Romanian)
Currently translated at 100.0% (537 of 537 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ro/
2024-01-09 16:55:22 +01:00
joaooliva
81b5ae1fa9 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (537 of 537 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/pt_BR/
2024-01-09 16:55:22 +01:00
gallegonovato
eddab1980a Translated using Weblate (Spanish)
Currently translated at 100.0% (537 of 537 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/es/
2024-01-09 16:55:22 +01:00
Arkadiusz Fal
7ff52294a8 Merge pull request #597 from stonerl/regex-for-chapters
more robust regex for chapters from description
2024-01-09 16:55:16 +01:00
Arkadiusz Fal
5ae18a5170 Merge pull request #596 from stonerl/update-defaults
Update defaults to 7.3.1
2024-01-09 16:55:07 +01:00
Toni Förster
ecba91f35d more robust regex for chapters from description
This should reduce the number of falsely matched chapters, e.g., when time-code-like numbers appear in the middle of the text, like 16:9 or sports results.

It also checks for chapters that have an end time and omits the end time code from the title.

Track lists in music videos are now also properly displayed as chapters.
2024-01-01 16:04:40 +01:00
Toni Förster
36ecf63b6c defaults move from .observe() to .updates()
https://github.com/sindresorhus/Defaults/releases/tag/v7.0.0
2023-12-27 17:27:24 +01:00
Toni Förster
0671b6ef9f update Defaults package to 7.3.1 2023-12-27 17:00:18 +01:00
Arkadiusz Fal
39f6319043 Bump build number to 171 2023-12-09 22:08:21 +01:00
Arkadiusz Fal
6a2dc9164e Update CHANGELOG 2023-12-09 22:08:20 +01:00
Arkadiusz Fal
37d3f43596 Add channel view help labels 2023-12-09 22:08:20 +01:00
Arkadiusz Fal
aef0ba6ffd Fix displaying account username 2023-12-09 22:08:20 +01:00
Arkadiusz Fal
1ae12cfa21 Add localizations for fa, es, tr, ru 2023-12-09 22:08:20 +01:00
Arkadiusz Fal
8475669aab Update packages 2023-12-09 22:08:20 +01:00
Arkadiusz Fal
6fec76fcb3 Merge pull request #584 from weblate/weblate-yattee-localizable-strings
Translations update from Hosted Weblate
2023-12-09 22:08:13 +01:00
Arkadiusz Fal
08b2e7ceac Translated using Weblate (Polish)
Currently translated at 100.0% (537 of 537 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/pl/
2023-12-09 22:08:00 +01:00
Anonymous
60972f0c7b Translated using Weblate (English)
Currently translated at 100.0% (537 of 537 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/en/
2023-12-09 22:08:00 +01:00
Arkadiusz Fal
a49db76588 Merge pull request #564 from stonerl/collapsible-chapters
make chapters collapsible and highlight current chapter
2023-12-09 21:51:24 +01:00
Arkadiusz Fal
bd0c86060b Merge pull request #583 from weblate/weblate-yattee-localizable-strings
Translations update from Hosted Weblate
2023-12-09 21:34:14 +01:00
Arkadiusz Fal
9849f31e1f Translated using Weblate (Polish)
Currently translated at 100.0% (535 of 535 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/pl/
2023-12-09 21:33:58 +01:00
Arkadiusz Fal
00ac222af6 Merge pull request #568 from weblate/weblate-yattee-localizable-strings
Translations update from Hosted Weblate
2023-12-09 21:25:54 +01:00
Vijay
732a8d7385 Translated using Weblate (Hindi)
Currently translated at 65.7% (352 of 535 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/hi/
2023-12-09 21:03:19 +01:00
Vijay
a009ad7d53 Translated using Weblate (Hindi)
Currently translated at 63.5% (340 of 535 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/hi/
2023-12-09 21:03:19 +01:00
Kaambiz
91c7c9fc8e Translated using Weblate (Persian)
Currently translated at 78.1% (418 of 535 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/fa/
2023-12-09 21:03:19 +01:00
Alper Zengintaş
e836f87c88 Translated using Weblate (Turkish)
Currently translated at 49.9% (267 of 535 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/tr/
2023-12-09 21:03:19 +01:00
Kaambiz
1e1a23acd0 Added translation using Weblate (Persian) 2023-12-09 21:03:19 +01:00
Sloom
d8d728a48f Translated using Weblate (Arabic (Najdi))
Currently translated at 0.3% (2 of 533 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ars/
2023-12-09 21:03:19 +01:00
maboroshin
067e8a79bf Translated using Weblate (Japanese)
Currently translated at 99.6% (533 of 535 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ja/
2023-12-09 21:03:19 +01:00
joaooliva
576f40360d Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (535 of 535 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/pt_BR/
2023-12-09 21:03:19 +01:00
gallegonovato
c679a52903 Translated using Weblate (Spanish)
Currently translated at 100.0% (535 of 535 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/es/
2023-12-09 21:03:19 +01:00
Alper Zengintaş
82f109290d Translated using Weblate (Turkish)
Currently translated at 46.7% (250 of 535 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/tr/
2023-12-09 21:03:19 +01:00
Ophiushi
765006b185 Translated using Weblate (French)
Currently translated at 100.0% (535 of 535 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/fr/
2023-12-09 21:03:19 +01:00
Anonymous
86cddb06e4 Translated using Weblate (English)
Currently translated at 100.0% (535 of 535 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/en/
2023-12-09 21:03:19 +01:00
Sloom
012b5156b7 Added translation using Weblate (Arabic (Najdi)) 2023-12-09 21:03:19 +01:00
Arkadiusz Fal
d6c04540e9 Merge pull request #569 from stonerl/no-180-rotation
disable portraitUpsideDown on iPhones
2023-12-09 21:03:14 +01:00
Arkadiusz Fal
3144b52b55 Merge pull request #578 from stonerl/description-fix
macOS: disable hit testing when not expanded
2023-12-09 21:01:30 +01:00
Toni Förster
b04385ceae macOS: disable hit testing when not expanded
fixes overspilling of text when clicking on collapsed descriptions
2023-12-05 00:36:12 +01:00
Toni Förster
721a97dc41 no need for NotificationCenter 2023-12-05 00:07:36 +01:00
Toni Förster
4ca8adc4dd move onReceive to the chapter header in VideoDetails 2023-12-04 23:00:20 +01:00
Toni Förster
d361ef01d4 move updating the time to PlayerModel
this makes the chapter view much much smoother
2023-12-04 21:58:49 +01:00
Toni Förster
0d9c27319d make time updates work for .mpv and .avp
- time update notifications work for both backends
- only init mpv timers when mpv is the active backend
- move notification extension to playerbackend
2023-12-04 14:47:26 +01:00
Toni Förster
600b8d198b add option to expand vertical chapters by default 2023-12-04 01:04:39 +01:00
Toni Förster
d65224320e chapters can only be clicked if expanded or horizontal 2023-12-04 00:39:29 +01:00
Toni Förster
586cea7d44 subscribe chapters to currentTime notification 2023-12-04 00:07:39 +01:00
Toni Förster
aa5d6733b2 remove superfluous vars and chaptermodel 2023-12-03 20:04:57 +01:00
Toni Förster
982dca1846 highlight current chapter when clicked on it 2023-11-29 00:31:53 +01:00
Toni Förster
7de702ad23 change color to appRed for chapter 2023-11-28 23:06:37 +01:00
Toni Förster
13ef96cd02 separate tvOS View 2023-11-28 20:05:04 +01:00
Toni Förster
4ec6f35c5d highlight current chapter 2023-11-28 16:45:36 +01:00
Toni Förster
7f19f7aa47 disable portraitUpsideDown on iPhones 2023-11-27 18:05:17 +01:00
Toni Förster
fb5cd0f681 vertical chapters are full width and max 3 lines 2023-11-27 14:18:43 +01:00
Toni Förster
3feafc153c remove toggle for vertical chapters 2023-11-27 13:34:18 +01:00
Toni Förster
8f08674527 vertical chapters with images are always collapsed 2023-11-27 12:13:51 +01:00
Toni Förster
a7baaeb485 iOS click on collapsed chapters expands the view 2023-11-26 21:24:52 +01:00
Toni Förster
b0d81cdefd iOS - chapters header is a button now 2023-11-26 21:24:52 +01:00
Toni Förster
a33a1d7658 formatting 2023-11-26 21:24:51 +01:00
Toni Förster
e436dec4ba make chapters collapsible
Chapters are now collapsible by default only the first two chapters are shown. The second will be shown opaque to indicate more chapters.
2023-11-26 21:24:50 +01:00
88 changed files with 4539 additions and 808 deletions

View File

@@ -1,14 +1,13 @@
## Build 170
* Description is collapsible with a button
* Links in description are clickable on macOS
* Aspect ratio is honored on resize on macOS
* Added support for private Invidious instances
## Build 177
* Added Settings Import/Export (iOS, macOS)
* Export all settings, instances and accounts
* Import selected elements from the file
* Include unencrypted passwords in the export or provide them during the import
* Added Controls setting "Action button labels" icon or icon and text
* Added Advanced setting for MPV: "deinterlace"
* Updated dependencies
**Big thanks to [@stonerl](https://github.com/stonerl) for these contributions!**
**And to all other project contributors!**
## Previous builds
* Fixed issue where Piped login token would not refresh
* Fixed issue with MPV subtitles not working
* Updated localizations
* Fixed reported crash
* Other minor changes and improvements
**Big thanks to the past, current and future project contributors!**

View File

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

View File

@@ -1,27 +1,74 @@
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:
CFPropertyList (3.0.6)
rexml
addressable (2.8.5)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.856.0)
aws-sdk-core (3.188.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (1.886.0)
aws-sdk-core (3.191.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.73.0)
aws-sdk-core (~> 3, >= 3.188.0)
aws-sdk-kms (1.77.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.139.0)
aws-sdk-core (~> 3, >= 3.188.0)
aws-sdk-s3 (1.143.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.6)
aws-sigv4 (1.7.0)
aws-sigv4 (~> 1.8)
aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
claide (1.1.0)
@@ -32,10 +79,10 @@ GEM
declarative (0.0.20)
digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20231109)
domain_name (0.6.20240107)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.104.0)
excon (0.109.0)
faraday (1.10.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
@@ -64,51 +111,11 @@ GEM
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.7)
fastlane (2.217.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
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-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)
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)
fastimage (2.3.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.53.0)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.2)
google-apis-core (0.11.3)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
@@ -116,24 +123,23 @@ GEM
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
webrick
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.29.0)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-core (1.6.1)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.3.1)
google-cloud-storage (1.45.0)
google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.29.0)
google-apis-storage_v1 (~> 0.31.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
@@ -148,7 +154,7 @@ GEM
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.2)
json (2.6.3)
json (2.7.1)
jwt (2.7.1)
mini_magick (4.12.0)
mini_mime (1.1.5)
@@ -156,9 +162,9 @@ GEM
multipart-post (2.3.0)
nanaimo (0.3.0)
naturally (2.2.1)
optparse (0.1.1)
optparse (0.4.0)
os (1.1.4)
plist (3.7.0)
plist (3.7.1)
public_suffix (5.0.4)
rake (13.1.0)
representable (3.2.0)
@@ -184,14 +190,13 @@ GEM
unicode-display_width (>= 1.1.1, < 3)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.1)
tty-screen (0.8.2)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unicode-display_width (2.5.0)
webrick (1.8.1)
word_wrap (1.0.0)
xcodeproj (1.23.0)
xcodeproj (1.24.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
@@ -210,7 +215,7 @@ PLATFORMS
x86_64-linux
DEPENDENCIES
fastlane
fastlane!
BUNDLED WITH
2.3.6

View File

@@ -64,6 +64,10 @@ final class AccountsModel: ObservableObject {
)
}
func find(_ id: Account.ID) -> Account? {
all.first { $0.id == id }
}
func configureAccount() {
if let account = lastUsed ??
InstancesModel.shared.lastUsed?.anonymousAccount ??
@@ -108,8 +112,8 @@ final class AccountsModel: ObservableObject {
Defaults[.accounts].first { $0.id == id }
}
static func add(instance: Instance, name: String, username: String, password: String) -> Account {
let account = Account(instanceID: instance.id, name: name, urlString: instance.apiURLString)
static func add(instance: Instance, id: String? = UUID().uuidString, name: String, username: String, password: String) -> Account {
let account = Account(id: id, instanceID: instance.id, name: name, urlString: instance.apiURLString)
Defaults[.accounts].append(account)
setCredentials(account, username: username, password: password)

View File

@@ -68,4 +68,8 @@ struct Instance: Defaults.Serializable, Hashable, Identifiable {
func hash(into hasher: inout Hasher) {
hasher.combine(apiURL)
}
var accounts: [Account] {
AccountsModel.shared.all.filter { $0.instanceID == id }
}
}

View File

@@ -42,15 +42,23 @@ final class InstancesModel: ObservableObject {
Defaults[.accounts].filter { $0.instanceID == id }
}
func add(app: VideosApp, name: String, url: String) -> Instance {
func add(id: String? = UUID().uuidString, app: VideosApp, name: String, url: String) -> Instance {
let instance = Instance(
app: app, id: UUID().uuidString, name: name, apiURLString: standardizedURL(url)
app: app, id: id, name: name, apiURLString: standardizedURL(url)
)
Defaults[.instances].append(instance)
return instance
}
func insert(id: String? = UUID().uuidString, app: VideosApp, name: String, url: String) -> Instance {
if let instance = Defaults[.instances].first(where: { $0.apiURL.absoluteString == standardizedURL(url) }) {
return instance
}
return add(id: id, app: app, name: name, url: url)
}
func setFrontendURL(_ instance: Instance, _ url: String) {
if let index = Defaults[.instances].firstIndex(where: { $0.id == instance.id }) {
var instance = Defaults[.instances][index]

View File

@@ -154,7 +154,8 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
method: .post,
parameters: ["username": username, "password": password],
encoding: JSONEncoding.default
).responseDecodable(of: JSON.self) { [weak self] response in
)
.responseDecodable(of: JSON.self) { [weak self] response in
guard let self else {
return
}

View File

@@ -144,52 +144,63 @@ extension VideosAPI {
}
func extractChapters(from description: String) -> [Chapter] {
guard let chaptersRegularExpression = try? NSRegularExpression(
pattern: "(?<start>(?:[0-9]+:){1,}(?:[0-9]+))(?:\\s)+(?:- ?)?(?<title>.*)",
options: .caseInsensitive
) else { return [] }
/*
The following chapter patterns are covered:
let chapterLines = chaptersRegularExpression.matches(
in: description,
range: NSRange(description.startIndex..., in: description)
)
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)
return chapterLines.compactMap { line in
let titleRange = line.range(withName: "title")
let startRange = line.range(withName: "start")
The order is important!
*/
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|$)"
]
guard let titleSubstringRange = Range(titleRange, in: description),
let startSubstringRange = Range(startRange, in: description) else { return nil }
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 titleCapture = String(description[titleSubstringRange])
let startCapture = String(description[startSubstringRange])
let startComponents = startCapture.components(separatedBy: ":")
guard startComponents.count <= 3 else { return nil }
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 }
var hours: Double?
var minutes: Double?
var seconds: Double?
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])
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 .init(title: titleCapture, start: startSeconds)
}
}
guard var startSeconds = seconds else { return nil }
if let minutes {
startSeconds += 60 * minutes
}
if let hours {
startSeconds += 60 * 60 * hours
}
return .init(title: titleCapture, start: startSeconds)
}
return []
}
}

View File

@@ -25,6 +25,7 @@ struct FavoritesModel {
}
func add(_ item: FavoriteItem) {
if contains(item) { return }
all.append(item)
}
@@ -122,4 +123,12 @@ struct FavoritesModel {
func widgetSettings(_ item: FavoriteItem) -> WidgetSettings {
widgetsSettings.first { $0.id == item.widgetSettingsKey } ?? WidgetSettings(id: item.widgetSettingsKey)
}
func updateWidgetSettings(_ settings: WidgetSettings) {
if let index = widgetsSettings.firstIndex(where: { $0.id == settings.id }) {
widgetsSettings[index] = settings
} else {
widgetsSettings.append(settings)
}
}
}

View File

@@ -0,0 +1,17 @@
import Defaults
import SwiftyJSON
final class AdvancedSettingsGroupExporter: SettingsGroupExporter {
override var globalJSON: JSON {
[
"showPlayNowInBackendContextMenu": Defaults[.showPlayNowInBackendContextMenu],
"showMPVPlaybackStats": Defaults[.showMPVPlaybackStats],
"mpvEnableLogging": Defaults[.mpvEnableLogging],
"mpvCacheSecs": Defaults[.mpvCacheSecs],
"mpvCachePauseWait": Defaults[.mpvCachePauseWait],
"mpvDeinterlace": Defaults[.mpvDeinterlace],
"showCacheStatus": Defaults[.showCacheStatus],
"feedCacheSize": Defaults[.feedCacheSize]
]
}
}

View File

@@ -0,0 +1,54 @@
import Defaults
import SwiftyJSON
final class BrowsingSettingsGroupExporter: SettingsGroupExporter {
override var globalJSON: JSON {
[
"showHome": Defaults[.showHome],
"showOpenActionsInHome": Defaults[.showOpenActionsInHome],
"showQueueInHome": Defaults[.showQueueInHome],
"showFavoritesInHome": Defaults[.showFavoritesInHome],
"favorites": Defaults[.favorites].compactMap { jsonFromString(FavoriteItem.bridge.serialize($0)) },
"widgetsSettings": Defaults[.widgetsSettings].compactMap { widgetSettingsJSON($0) },
"startupSection": Defaults[.startupSection].rawValue,
"visibleSections": Defaults[.visibleSections].compactMap { $0.rawValue },
"showOpenActionsToolbarItem": Defaults[.showOpenActionsToolbarItem],
"accountPickerDisplaysAnonymousAccounts": Defaults[.accountPickerDisplaysAnonymousAccounts],
"showUnwatchedFeedBadges": Defaults[.showUnwatchedFeedBadges],
"expandChannelDescription": Defaults[.expandChannelDescription],
"keepChannelsWithUnwatchedFeedOnTop": Defaults[.keepChannelsWithUnwatchedFeedOnTop],
"showChannelAvatarInChannelsLists": Defaults[.showChannelAvatarInChannelsLists],
"showChannelAvatarInVideosListing": Defaults[.showChannelAvatarInVideosListing],
"playerButtonSingleTapGesture": Defaults[.playerButtonSingleTapGesture].rawValue,
"playerButtonDoubleTapGesture": Defaults[.playerButtonDoubleTapGesture].rawValue,
"playerButtonShowsControlButtonsWhenMinimized": Defaults[.playerButtonShowsControlButtonsWhenMinimized],
"playerButtonIsExpanded": Defaults[.playerButtonIsExpanded],
"playerBarMaxWidth": Defaults[.playerBarMaxWidth],
"channelOnThumbnail": Defaults[.channelOnThumbnail],
"timeOnThumbnail": Defaults[.timeOnThumbnail],
"roundedThumbnails": Defaults[.roundedThumbnails],
"thumbnailsQuality": Defaults[.thumbnailsQuality].rawValue
]
}
override var platformJSON: JSON {
var export = JSON()
#if os(iOS)
export["showDocuments"].bool = Defaults[.showDocuments]
export["lockPortraitWhenBrowsing"].bool = Defaults[.lockPortraitWhenBrowsing]
#endif
#if !os(tvOS)
export["accountPickerDisplaysUsername"].bool = Defaults[.accountPickerDisplaysUsername]
#endif
return export
}
private func widgetSettingsJSON(_ settings: WidgetSettings) -> JSON {
var json = JSON()
json.dictionaryObject = WidgetSettingsBridge().serialize(settings)
return json
}
}

View File

@@ -0,0 +1,40 @@
import Defaults
import SwiftyJSON
final class ConstrolsSettingsGroupExporter: SettingsGroupExporter {
override var globalJSON: JSON {
[
"avPlayerUsesSystemControls": Defaults[.avPlayerUsesSystemControls],
"horizontalPlayerGestureEnabled": Defaults[.horizontalPlayerGestureEnabled],
"seekGestureSensitivity": Defaults[.seekGestureSensitivity],
"seekGestureSpeed": Defaults[.seekGestureSpeed],
"playerControlsLayout": Defaults[.playerControlsLayout].rawValue,
"fullScreenPlayerControlsLayout": Defaults[.fullScreenPlayerControlsLayout].rawValue,
"systemControlsCommands": Defaults[.systemControlsCommands].rawValue,
"buttonBackwardSeekDuration": Defaults[.buttonBackwardSeekDuration],
"buttonForwardSeekDuration": Defaults[.buttonForwardSeekDuration],
"gestureBackwardSeekDuration": Defaults[.gestureBackwardSeekDuration],
"gestureForwardSeekDuration": Defaults[.gestureForwardSeekDuration],
"systemControlsSeekDuration": Defaults[.systemControlsSeekDuration],
"playerControlsSettingsEnabled": Defaults[.playerControlsSettingsEnabled],
"playerControlsCloseEnabled": Defaults[.playerControlsCloseEnabled],
"playerControlsRestartEnabled": Defaults[.playerControlsRestartEnabled],
"playerControlsAdvanceToNextEnabled": Defaults[.playerControlsAdvanceToNextEnabled],
"playerControlsPlaybackModeEnabled": Defaults[.playerControlsPlaybackModeEnabled],
"playerControlsMusicModeEnabled": Defaults[.playerControlsMusicModeEnabled],
"playerActionsButtonLabelStyle": Defaults[.playerActionsButtonLabelStyle].rawValue,
"actionButtonShareEnabled": Defaults[.actionButtonShareEnabled],
"actionButtonAddToPlaylistEnabled": Defaults[.actionButtonAddToPlaylistEnabled],
"actionButtonSubscribeEnabled": Defaults[.actionButtonSubscribeEnabled],
"actionButtonSettingsEnabled": Defaults[.actionButtonSettingsEnabled],
"actionButtonHideEnabled": Defaults[.actionButtonHideEnabled],
"actionButtonCloseEnabled": Defaults[.actionButtonCloseEnabled],
"actionButtonFullScreenEnabled": Defaults[.actionButtonFullScreenEnabled],
"actionButtonPipEnabled": Defaults[.actionButtonPipEnabled],
"actionButtonLockOrientationEnabled": Defaults[.actionButtonLockOrientationEnabled],
"actionButtonRestartEnabled": Defaults[.actionButtonRestartEnabled],
"actionButtonAdvanceToNextItemEnabled": Defaults[.actionButtonAdvanceToNextItemEnabled],
"actionButtonMusicModeEnabled": Defaults[.actionButtonMusicModeEnabled]
]
}
}

View File

@@ -0,0 +1,21 @@
import Defaults
import SwiftyJSON
final class HistorySettingsGroupExporter: SettingsGroupExporter {
override var globalJSON: JSON {
[
"saveRecents": Defaults[.saveRecents],
"saveHistory": Defaults[.saveHistory],
"showWatchingProgress": Defaults[.showWatchingProgress],
"saveLastPlayed": Defaults[.saveLastPlayed],
"watchedVideoPlayNowBehavior": Defaults[.watchedVideoPlayNowBehavior].rawValue,
"watchedThreshold": Defaults[.watchedThreshold],
"resetWatchedStatusOnPlaying": Defaults[.resetWatchedStatusOnPlaying],
"watchedVideoStyle": Defaults[.watchedVideoStyle].rawValue,
"watchedVideoBadgeColor": Defaults[.watchedVideoBadgeColor].rawValue,
"showToggleWatchedStatusButton": Defaults[.showToggleWatchedStatusButton]
]
}
}

View File

@@ -0,0 +1,56 @@
import Defaults
import SwiftyJSON
final class LocationsSettingsGroupExporter: SettingsGroupExporter {
var includePublicInstances = true
var includeInstances = true
var includeAccounts = true
var includeAccountsUnencryptedPasswords = false
init(includePublicInstances: Bool = true, includeInstances: Bool = true, includeAccounts: Bool = true, includeAccountsUnencryptedPasswords: Bool = false) {
self.includePublicInstances = includePublicInstances
self.includeInstances = includeInstances
self.includeAccounts = includeAccounts
self.includeAccountsUnencryptedPasswords = includeAccountsUnencryptedPasswords
}
override var globalJSON: JSON {
var json = JSON()
if includePublicInstances {
json["instancesManifest"].string = Defaults[.instancesManifest]
json["countryOfPublicInstances"].string = Defaults[.countryOfPublicInstances] ?? ""
}
if includeInstances {
json["instances"].arrayObject = Defaults[.instances].compactMap { instanceJSON($0) }
}
if includeAccounts {
json["accounts"].arrayObject = Defaults[.accounts].compactMap { account in
var account = account
let (username, password) = AccountsModel.getCredentials(account)
account.username = username ?? ""
if includeAccountsUnencryptedPasswords {
account.password = password ?? ""
}
return accountJSON(account).dictionaryObject
}
}
return json
}
private func instanceJSON(_ instance: Instance) -> JSON {
var json = JSON()
json.dictionaryObject = InstancesBridge().serialize(instance)
return json
}
private func accountJSON(_ account: Account) -> JSON {
var json = JSON()
json.dictionaryObject = AccountsBridge().serialize(account)
return json
}
}

View File

@@ -0,0 +1,27 @@
import Defaults
import SwiftyJSON
final class OtherDataSettingsGroupExporter: SettingsGroupExporter {
override var globalJSON: JSON {
[
"lastAccountID": Defaults[.lastAccountID] ?? "",
"lastInstanceID": Defaults[.lastInstanceID] ?? "",
"playerRate": Defaults[.playerRate],
"trendingCategory": Defaults[.trendingCategory].rawValue,
"trendingCountry": Defaults[.trendingCountry].rawValue,
"subscriptionsViewPage": Defaults[.subscriptionsViewPage].rawValue,
"subscriptionsListingStyle": Defaults[.subscriptionsListingStyle].rawValue,
"popularListingStyle": Defaults[.popularListingStyle].rawValue,
"trendingListingStyle": Defaults[.trendingListingStyle].rawValue,
"playlistListingStyle": Defaults[.playlistListingStyle].rawValue,
"channelPlaylistListingStyle": Defaults[.channelPlaylistListingStyle].rawValue,
"searchListingStyle": Defaults[.searchListingStyle].rawValue,
"hideShorts": Defaults[.hideShorts],
"hideWatched": Defaults[.hideWatched]
]
}
}

View File

@@ -0,0 +1,44 @@
import Defaults
import SwiftyJSON
final class PlayerSettingsGroupExporter: SettingsGroupExporter {
override var globalJSON: JSON {
[
"playerInstanceID": Defaults[.playerInstanceID] ?? "",
"pauseOnHidingPlayer": Defaults[.pauseOnHidingPlayer],
"closeVideoOnEOF": Defaults[.closeVideoOnEOF],
"expandVideoDescription": Defaults[.expandVideoDescription],
"collapsedLinesDescription": Defaults[.collapsedLinesDescription],
"showChapters": Defaults[.showChapters],
"expandChapters": Defaults[.expandChapters],
"showRelated": Defaults[.showRelated],
"showInspector": Defaults[.showInspector].rawValue,
"playerSidebar": Defaults[.playerSidebar].rawValue,
"showKeywords": Defaults[.showKeywords],
"enableReturnYouTubeDislike": Defaults[.enableReturnYouTubeDislike],
"closePiPOnNavigation": Defaults[.closePiPOnNavigation],
"closePiPOnOpeningPlayer": Defaults[.closePiPOnOpeningPlayer],
"closePlayerOnOpeningPiP": Defaults[.closePlayerOnOpeningPiP]
]
}
override var platformJSON: JSON {
var export = JSON()
#if !os(macOS)
export["pauseOnEnteringBackground"].bool = Defaults[.pauseOnEnteringBackground]
#endif
#if !os(tvOS)
export["showScrollToTopInComments"].bool = Defaults[.showScrollToTopInComments]
#endif
#if os(iOS)
export["honorSystemOrientationLock"].bool = Defaults[.honorSystemOrientationLock]
export["enterFullscreenInLandscape"].bool = Defaults[.enterFullscreenInLandscape]
export["rotateToLandscapeOnEnterFullScreen"].string = Defaults[.rotateToLandscapeOnEnterFullScreen].rawValue
#endif
return export
}
}

View File

@@ -0,0 +1,21 @@
import Defaults
import SwiftyJSON
final class QualitySettingsGroupExporter: SettingsGroupExporter {
override var globalJSON: JSON {
[
"batteryCellularProfile": Defaults[.batteryCellularProfile],
"batteryNonCellularProfile": Defaults[.batteryNonCellularProfile],
"chargingCellularProfile": Defaults[.chargingCellularProfile],
"chargingNonCellularProfile": Defaults[.chargingNonCellularProfile],
"forceAVPlayerForLiveStreams": Defaults[.forceAVPlayerForLiveStreams],
"qualityProfiles": Defaults[.qualityProfiles].compactMap { qualityProfileJSON($0) }
]
}
func qualityProfileJSON(_ profile: QualityProfile) -> JSON {
var json = JSON()
json.dictionaryObject = QualityProfileBridge().serialize(profile)
return json
}
}

View File

@@ -0,0 +1,16 @@
import Defaults
import SwiftyJSON
final class RecentlyOpenedExporter: SettingsGroupExporter {
override var globalJSON: JSON {
[
"recentlyOpened": Defaults[.recentlyOpened].compactMap { recentItemJSON($0) }
]
}
private func recentItemJSON(_ recentItem: RecentItem) -> JSON {
var json = JSON()
json.dictionaryObject = RecentItemBridge().serialize(recentItem)
return json
}
}

View File

@@ -0,0 +1,32 @@
import Foundation
import SwiftyJSON
class SettingsGroupExporter { // swiftlint:disable:this final_class
var globalJSON: JSON {
[]
}
var platformJSON: JSON {
[]
}
var exportJSON: JSON {
var json = globalJSON
if !platformJSON.isEmpty {
try? json.merge(with: platformJSON)
}
return json
}
func jsonFromString(_ string: String?) -> JSON? {
if let data = string?.data(using: .utf8, allowLossyConversion: false),
let json = try? JSON(data: data)
{
return json
}
return nil
}
}

View File

@@ -0,0 +1,11 @@
import Defaults
import SwiftyJSON
final class SponsorBlockSettingsGroupExporter: SettingsGroupExporter {
override var globalJSON: JSON {
[
"sponsorBlockInstance": Defaults[.sponsorBlockInstance],
"sponsorBlockCategories": Array(Defaults[.sponsorBlockCategories])
]
}
}

View File

@@ -0,0 +1,193 @@
import Defaults
import Foundation
import SwiftUI
import SwiftyJSON
final class ImportExportSettingsModel: ObservableObject {
static let shared = ImportExportSettingsModel()
static var exportFile: URL {
YatteeApp.settingsExportDirectory
.appendingPathComponent("Yattee Settings from \(Constants.deviceName).\(settingsExtension)")
}
static var settingsExtension: String {
"yatteesettings"
}
enum ExportGroup: String, Identifiable, CaseIterable {
case browsingSettings
case playerSettings
case controlsSettings
case qualitySettings
case historySettings
case sponsorBlockSettings
case advancedSettings
case locationsSettings
case instances
case accounts
case accountsUnencryptedPasswords
case recentlyOpened
case otherData
static var settingsGroups: [Self] {
[.browsingSettings, .playerSettings, .controlsSettings, .qualitySettings, .historySettings, .sponsorBlockSettings, .advancedSettings]
}
static var locationsGroups: [Self] {
[.locationsSettings, .instances, .accounts, .accountsUnencryptedPasswords]
}
static var otherGroups: [Self] {
[.recentlyOpened, .otherData]
}
var id: RawValue {
rawValue
}
var label: String {
switch self {
case .browsingSettings:
return "Browsing"
case .playerSettings:
return "Player"
case .controlsSettings:
return "Controls"
case .qualitySettings:
return "Quality"
case .historySettings:
return "History"
case .sponsorBlockSettings:
return "SponsorBlock"
case .locationsSettings:
return "Public Locations"
case .instances:
return "Custom Locations"
case .accounts:
return "Accounts"
case .accountsUnencryptedPasswords:
return "Accounts passwords (unencrypted)"
case .advancedSettings:
return "Advanced"
case .recentlyOpened:
return "Recents"
case .otherData:
return "Other data"
}
}
}
@Published var selectedExportGroups = Set<ExportGroup>()
static var defaultExportGroups = Set<ExportGroup>([
.browsingSettings,
.playerSettings,
.controlsSettings,
.qualitySettings,
.historySettings,
.sponsorBlockSettings,
.locationsSettings,
.instances,
.accounts,
.advancedSettings
])
@Published var isExportInProgress = false
private var navigation = NavigationModel.shared
private var settings = SettingsModel.shared
func toggleExportGroupSelection(_ group: ExportGroup) {
if isGroupSelected(group) {
selectedExportGroups.remove(group)
} else {
selectedExportGroups.insert(group)
}
removeNotEnabledSelectedGroups()
}
func reset() {
isExportInProgress = false
selectedExportGroups = Self.defaultExportGroups
}
func reset(_ model: ImportSettingsFileModel? = nil) {
reset()
guard let model else { return }
selectedExportGroups = selectedExportGroups.filter { model.isGroupIncludedInFile($0) }
}
func exportAction() {
DispatchQueue.global(qos: .background).async { [weak self] in
var writingOptions: JSONSerialization.WritingOptions = []
#if DEBUG
writingOptions.insert(.prettyPrinted)
writingOptions.insert(.sortedKeys)
#endif
try? self?.jsonForExport?.rawString(options: writingOptions)?.write(to: Self.exportFile, atomically: true, encoding: String.Encoding.utf8)
#if os(macOS)
DispatchQueue.main.async { [weak self] in
self?.isExportInProgress = false
}
NSWorkspace.shared.selectFile(Self.exportFile.path, inFileViewerRootedAtPath: YatteeApp.settingsExportDirectory.path)
#endif
}
}
private var jsonForExport: JSON? {
[
"metadata": metadataJSON,
"browsingSettings": selectedExportGroups.contains(.browsingSettings) ? BrowsingSettingsGroupExporter().exportJSON : JSON(),
"playerSettings": selectedExportGroups.contains(.playerSettings) ? PlayerSettingsGroupExporter().exportJSON : JSON(),
"controlsSettings": selectedExportGroups.contains(.controlsSettings) ? ConstrolsSettingsGroupExporter().exportJSON : JSON(),
"qualitySettings": selectedExportGroups.contains(.qualitySettings) ? QualitySettingsGroupExporter().exportJSON : JSON(),
"historySettings": selectedExportGroups.contains(.historySettings) ? HistorySettingsGroupExporter().exportJSON : JSON(),
"sponsorBlockSettings": selectedExportGroups.contains(.sponsorBlockSettings) ? SponsorBlockSettingsGroupExporter().exportJSON : JSON(),
"locationsSettings": LocationsSettingsGroupExporter(
includePublicInstances: isGroupSelected(.locationsSettings),
includeInstances: isGroupSelected(.instances),
includeAccounts: isGroupSelected(.accounts),
includeAccountsUnencryptedPasswords: isGroupSelected(.accountsUnencryptedPasswords)
).exportJSON,
"advancedSettings": selectedExportGroups.contains(.advancedSettings) ? AdvancedSettingsGroupExporter().exportJSON : JSON(),
"recentlyOpened": selectedExportGroups.contains(.recentlyOpened) ? RecentlyOpenedExporter().exportJSON : JSON(),
"otherData": selectedExportGroups.contains(.otherData) ? OtherDataSettingsGroupExporter().exportJSON : JSON()
]
}
private var metadataJSON: JSON {
[
"build": YatteeApp.build,
"timestamp": "\(Date().timeIntervalSince1970)",
"platform": Constants.platform
]
}
func isGroupSelected(_ group: ExportGroup) -> Bool {
selectedExportGroups.contains(group)
}
func isGroupEnabled(_ group: ExportGroup) -> Bool {
switch group {
case .accounts:
return selectedExportGroups.contains(.instances)
case .accountsUnencryptedPasswords:
return selectedExportGroups.contains(.instances) && selectedExportGroups.contains(.accounts)
default:
return true
}
}
func removeNotEnabledSelectedGroups() {
selectedExportGroups = selectedExportGroups.filter { isGroupEnabled($0) }
}
var isExportAvailable: Bool {
!selectedExportGroups.isEmpty && !isExportInProgress
}
}

View File

@@ -0,0 +1,135 @@
import Defaults
import Foundation
import SwiftyJSON
struct ImportSettingsFileModel {
let url: URL
var filename: String {
String(url.lastPathComponent.dropLast(ImportExportSettingsModel.settingsExtension.count + 1))
}
var locationsSettingsGroupImporter: LocationsSettingsGroupImporter? {
if let locationsSettings = json?.dictionaryValue["locationsSettings"] {
return LocationsSettingsGroupImporter(
json: locationsSettings,
includePublicLocations: importExportModel.isGroupEnabled(.locationsSettings),
includedInstancesIDs: sheetViewModel.selectedInstances,
includedAccountsIDs: sheetViewModel.selectedAccounts,
includedAccountsPasswords: sheetViewModel.importableAccountsPasswords
)
}
return nil
}
var importExportModel = ImportExportSettingsModel.shared
var sheetViewModel = ImportSettingsSheetViewModel.shared
func isGroupIncludedInFile(_ group: ImportExportSettingsModel.ExportGroup) -> Bool {
switch group {
case .locationsSettings:
return isPublicInstancesSettingsGroupInFile || instancesOrAccountsInFile
default:
return !groupJSON(group).isEmpty
}
}
var isPublicInstancesSettingsGroupInFile: Bool {
guard let dict = groupJSON(.locationsSettings).dictionary else { return false }
return dict.keys.contains("instancesManifest") || dict.keys.contains("countryOfPublicInstances")
}
var instancesOrAccountsInFile: Bool {
guard let dict = groupJSON(.locationsSettings).dictionary else { return false }
return (dict.keys.contains("instances") && !(dict["instances"]?.arrayValue.isEmpty ?? true)) ||
(dict.keys.contains("accounts") && !(dict["accounts"]?.arrayValue.isEmpty ?? true))
}
func groupJSON(_ group: ImportExportSettingsModel.ExportGroup) -> JSON {
json?.dictionaryValue[group.rawValue] ?? .init()
}
func performImport() {
if importExportModel.isGroupSelected(.browsingSettings), isGroupIncludedInFile(.browsingSettings) {
BrowsingSettingsGroupImporter(json: groupJSON(.browsingSettings)).performImport()
}
if importExportModel.isGroupSelected(.playerSettings), isGroupIncludedInFile(.playerSettings) {
PlayerSettingsGroupImporter(json: groupJSON(.playerSettings)).performImport()
}
if importExportModel.isGroupSelected(.controlsSettings), isGroupIncludedInFile(.controlsSettings) {
ConstrolsSettingsGroupImporter(json: groupJSON(.controlsSettings)).performImport()
}
if importExportModel.isGroupSelected(.qualitySettings), isGroupIncludedInFile(.qualitySettings) {
QualitySettingsGroupImporter(json: groupJSON(.qualitySettings)).performImport()
}
if importExportModel.isGroupSelected(.historySettings), isGroupIncludedInFile(.historySettings) {
HistorySettingsGroupImporter(json: groupJSON(.historySettings)).performImport()
}
if importExportModel.isGroupSelected(.sponsorBlockSettings), isGroupIncludedInFile(.sponsorBlockSettings) {
SponsorBlockSettingsGroupImporter(json: groupJSON(.sponsorBlockSettings)).performImport()
}
locationsSettingsGroupImporter?.performImport()
if importExportModel.isGroupSelected(.advancedSettings), isGroupIncludedInFile(.advancedSettings) {
AdvancedSettingsGroupImporter(json: groupJSON(.advancedSettings)).performImport()
}
if importExportModel.isGroupSelected(.recentlyOpened), isGroupIncludedInFile(.recentlyOpened) {
RecentlyOpenedImporter(json: groupJSON(.recentlyOpened)).performImport()
}
if importExportModel.isGroupSelected(.otherData), isGroupIncludedInFile(.otherData) {
OtherDataSettingsGroupImporter(json: groupJSON(.otherData)).performImport()
}
}
var json: JSON? {
if let fileContents = try? Data(contentsOf: url),
let json = try? JSON(data: fileContents)
{
return json
}
return nil
}
var metadataBuild: String? {
if let build = json?.dictionaryValue["metadata"]?.dictionaryValue["build"]?.string {
return build
}
return nil
}
var metadataPlatform: String? {
if let platform = json?.dictionaryValue["metadata"]?.dictionaryValue["platform"]?.string {
return platform
}
return nil
}
var metadataDate: String? {
if let timestamp = json?.dictionaryValue["metadata"]?.dictionaryValue["timestamp"]?.doubleValue {
let date = Date(timeIntervalSince1970: timestamp)
return dateFormatter.string(from: date)
}
return nil
}
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .long
formatter.timeStyle = .medium
return formatter
}
}

View File

@@ -0,0 +1,40 @@
import Defaults
import SwiftyJSON
struct AdvancedSettingsGroupImporter {
var json: JSON
func performImport() {
if let showPlayNowInBackendContextMenu = json["showPlayNowInBackendContextMenu"].bool {
Defaults[.showPlayNowInBackendContextMenu] = showPlayNowInBackendContextMenu
}
if let showMPVPlaybackStats = json["showMPVPlaybackStats"].bool {
Defaults[.showMPVPlaybackStats] = showMPVPlaybackStats
}
if let mpvEnableLogging = json["mpvEnableLogging"].bool {
Defaults[.mpvEnableLogging] = mpvEnableLogging
}
if let mpvCacheSecs = json["mpvCacheSecs"].string {
Defaults[.mpvCacheSecs] = mpvCacheSecs
}
if let mpvCachePauseWait = json["mpvCachePauseWait"].string {
Defaults[.mpvCachePauseWait] = mpvCachePauseWait
}
if let mpvDeinterlace = json["mpvDeinterlace"].bool {
Defaults[.mpvDeinterlace] = mpvDeinterlace
}
if let showCacheStatus = json["showCacheStatus"].bool {
Defaults[.showCacheStatus] = showCacheStatus
}
if let feedCacheSize = json["feedCacheSize"].string {
Defaults[.feedCacheSize] = feedCacheSize
}
}
}

View File

@@ -0,0 +1,144 @@
import Defaults
import SwiftyJSON
struct BrowsingSettingsGroupImporter {
var json: JSON
func performImport() {
if let showHome = json["showHome"].bool {
Defaults[.showHome] = showHome
}
if let showOpenActionsInHome = json["showOpenActionsInHome"].bool {
Defaults[.showOpenActionsInHome] = showOpenActionsInHome
}
if let showQueueInHome = json["showQueueInHome"].bool {
Defaults[.showQueueInHome] = showQueueInHome
}
if let showFavoritesInHome = json["showFavoritesInHome"].bool {
Defaults[.showFavoritesInHome] = showFavoritesInHome
}
if let favorites = json["favorites"].array {
favorites.forEach { favoriteJSON in
if let jsonString = favoriteJSON.rawString(options: []),
let item = FavoriteItem.bridge.deserialize(jsonString)
{
FavoritesModel.shared.add(item)
}
}
}
if let widgetsFavorites = json["widgetsSettings"].array {
widgetsFavorites.forEach { widgetJSON in
let dict = widgetJSON.dictionaryValue.mapValues { json in json.stringValue }
if let item = WidgetSettingsBridge().deserialize(dict) {
FavoritesModel.shared.updateWidgetSettings(item)
}
}
}
if let startupSectionString = json["startupSection"].string,
let startupSection = StartupSection(rawValue: startupSectionString)
{
Defaults[.startupSection] = startupSection
}
if let visibleSections = json["visibleSections"].array {
let sections = visibleSections.compactMap { visibleSectionJSON in
if let visibleSectionString = visibleSectionJSON.rawString(options: []),
let section = VisibleSection(rawValue: visibleSectionString)
{
return section
}
return nil
}
Defaults[.visibleSections] = Set(sections)
}
#if os(iOS)
if let showOpenActionsToolbarItem = json["showOpenActionsToolbarItem"].bool {
Defaults[.showOpenActionsToolbarItem] = showOpenActionsToolbarItem
}
if let lockPortraitWhenBrowsing = json["lockPortraitWhenBrowsing"].bool {
Defaults[.lockPortraitWhenBrowsing] = lockPortraitWhenBrowsing
}
#endif
#if !os(tvOS)
if let accountPickerDisplaysUsername = json["accountPickerDisplaysUsername"].bool {
Defaults[.accountPickerDisplaysUsername] = accountPickerDisplaysUsername
}
#endif
if let accountPickerDisplaysAnonymousAccounts = json["accountPickerDisplaysAnonymousAccounts"].bool {
Defaults[.accountPickerDisplaysAnonymousAccounts] = accountPickerDisplaysAnonymousAccounts
}
if let showUnwatchedFeedBadges = json["showUnwatchedFeedBadges"].bool {
Defaults[.showUnwatchedFeedBadges] = showUnwatchedFeedBadges
}
if let expandChannelDescription = json["expandChannelDescription"].bool {
Defaults[.expandChannelDescription] = expandChannelDescription
}
if let keepChannelsWithUnwatchedFeedOnTop = json["keepChannelsWithUnwatchedFeedOnTop"].bool {
Defaults[.keepChannelsWithUnwatchedFeedOnTop] = keepChannelsWithUnwatchedFeedOnTop
}
if let showChannelAvatarInChannelsLists = json["showChannelAvatarInChannelsLists"].bool {
Defaults[.showChannelAvatarInChannelsLists] = showChannelAvatarInChannelsLists
}
if let showChannelAvatarInVideosListing = json["showChannelAvatarInVideosListing"].bool {
Defaults[.showChannelAvatarInVideosListing] = showChannelAvatarInVideosListing
}
if let playerButtonSingleTapGestureString = json["playerButtonSingleTapGesture"].string,
let playerButtonSingleTapGesture = PlayerTapGestureAction(rawValue: playerButtonSingleTapGestureString)
{
Defaults[.playerButtonSingleTapGesture] = playerButtonSingleTapGesture
}
if let playerButtonDoubleTapGestureString = json["playerButtonDoubleTapGesture"].string,
let playerButtonDoubleTapGesture = PlayerTapGestureAction(rawValue: playerButtonDoubleTapGestureString)
{
Defaults[.playerButtonDoubleTapGesture] = playerButtonDoubleTapGesture
}
if let playerButtonShowsControlButtonsWhenMinimized = json["playerButtonShowsControlButtonsWhenMinimized"].bool {
Defaults[.playerButtonShowsControlButtonsWhenMinimized] = playerButtonShowsControlButtonsWhenMinimized
}
if let playerButtonIsExpanded = json["playerButtonIsExpanded"].bool {
Defaults[.playerButtonIsExpanded] = playerButtonIsExpanded
}
if let playerBarMaxWidth = json["playerBarMaxWidth"].string {
Defaults[.playerBarMaxWidth] = playerBarMaxWidth
}
if let channelOnThumbnail = json["channelOnThumbnail"].bool {
Defaults[.channelOnThumbnail] = channelOnThumbnail
}
if let timeOnThumbnail = json["timeOnThumbnail"].bool {
Defaults[.timeOnThumbnail] = timeOnThumbnail
}
if let roundedThumbnails = json["roundedThumbnails"].bool {
Defaults[.roundedThumbnails] = roundedThumbnails
}
if let thumbnailsQualityString = json["thumbnailsQuality"].string,
let thumbnailsQuality = ThumbnailsQuality(rawValue: thumbnailsQualityString)
{
Defaults[.thumbnailsQuality] = thumbnailsQuality
}
}
}

View File

@@ -0,0 +1,140 @@
import Defaults
import SwiftyJSON
struct ConstrolsSettingsGroupImporter {
var json: JSON
func performImport() {
if let avPlayerUsesSystemControls = json["avPlayerUsesSystemControls"].bool {
Defaults[.avPlayerUsesSystemControls] = avPlayerUsesSystemControls
}
if let horizontalPlayerGestureEnabled = json["horizontalPlayerGestureEnabled"].bool {
Defaults[.horizontalPlayerGestureEnabled] = horizontalPlayerGestureEnabled
}
if let seekGestureSensitivity = json["seekGestureSensitivity"].double {
Defaults[.seekGestureSensitivity] = seekGestureSensitivity
}
if let seekGestureSpeed = json["seekGestureSpeed"].double {
Defaults[.seekGestureSpeed] = seekGestureSpeed
}
if let playerControlsLayoutString = json["playerControlsLayout"].string,
let playerControlsLayout = PlayerControlsLayout(rawValue: playerControlsLayoutString)
{
Defaults[.playerControlsLayout] = playerControlsLayout
}
if let fullScreenPlayerControlsLayoutString = json["fullScreenPlayerControlsLayout"].string,
let fullScreenPlayerControlsLayout = PlayerControlsLayout(rawValue: fullScreenPlayerControlsLayoutString)
{
Defaults[.fullScreenPlayerControlsLayout] = fullScreenPlayerControlsLayout
}
if let systemControlsCommandsString = json["systemControlsCommands"].string,
let systemControlsCommands = SystemControlsCommands(rawValue: systemControlsCommandsString)
{
Defaults[.systemControlsCommands] = systemControlsCommands
}
if let buttonBackwardSeekDuration = json["buttonBackwardSeekDuration"].string {
Defaults[.buttonBackwardSeekDuration] = buttonBackwardSeekDuration
}
if let buttonForwardSeekDuration = json["buttonForwardSeekDuration"].string {
Defaults[.buttonForwardSeekDuration] = buttonForwardSeekDuration
}
if let gestureBackwardSeekDuration = json["gestureBackwardSeekDuration"].string {
Defaults[.gestureBackwardSeekDuration] = gestureBackwardSeekDuration
}
if let gestureForwardSeekDuration = json["gestureForwardSeekDuration"].string {
Defaults[.gestureForwardSeekDuration] = gestureForwardSeekDuration
}
if let systemControlsSeekDuration = json["systemControlsSeekDuration"].string {
Defaults[.systemControlsSeekDuration] = systemControlsSeekDuration
}
if let playerControlsSettingsEnabled = json["playerControlsSettingsEnabled"].bool {
Defaults[.playerControlsSettingsEnabled] = playerControlsSettingsEnabled
}
if let playerControlsCloseEnabled = json["playerControlsCloseEnabled"].bool {
Defaults[.playerControlsCloseEnabled] = playerControlsCloseEnabled
}
if let playerControlsRestartEnabled = json["playerControlsRestartEnabled"].bool {
Defaults[.playerControlsRestartEnabled] = playerControlsRestartEnabled
}
if let playerControlsAdvanceToNextEnabled = json["playerControlsAdvanceToNextEnabled"].bool {
Defaults[.playerControlsAdvanceToNextEnabled] = playerControlsAdvanceToNextEnabled
}
if let playerControlsPlaybackModeEnabled = json["playerControlsPlaybackModeEnabled"].bool {
Defaults[.playerControlsPlaybackModeEnabled] = playerControlsPlaybackModeEnabled
}
if let playerControlsMusicModeEnabled = json["playerControlsMusicModeEnabled"].bool {
Defaults[.playerControlsMusicModeEnabled] = playerControlsMusicModeEnabled
}
if let playerActionsButtonLabelStyleString = json["playerActionsButtonLabelStyle"].string,
let playerActionsButtonLabelStyle = ButtonLabelStyle(rawValue: playerActionsButtonLabelStyleString)
{
Defaults[.playerActionsButtonLabelStyle] = playerActionsButtonLabelStyle
}
if let actionButtonShareEnabled = json["actionButtonShareEnabled"].bool {
Defaults[.actionButtonShareEnabled] = actionButtonShareEnabled
}
if let actionButtonAddToPlaylistEnabled = json["actionButtonAddToPlaylistEnabled"].bool {
Defaults[.actionButtonAddToPlaylistEnabled] = actionButtonAddToPlaylistEnabled
}
if let actionButtonSubscribeEnabled = json["actionButtonSubscribeEnabled"].bool {
Defaults[.actionButtonSubscribeEnabled] = actionButtonSubscribeEnabled
}
if let actionButtonSettingsEnabled = json["actionButtonSettingsEnabled"].bool {
Defaults[.actionButtonSettingsEnabled] = actionButtonSettingsEnabled
}
if let actionButtonHideEnabled = json["actionButtonHideEnabled"].bool {
Defaults[.actionButtonHideEnabled] = actionButtonHideEnabled
}
if let actionButtonCloseEnabled = json["actionButtonCloseEnabled"].bool {
Defaults[.actionButtonCloseEnabled] = actionButtonCloseEnabled
}
if let actionButtonFullScreenEnabled = json["actionButtonFullScreenEnabled"].bool {
Defaults[.actionButtonFullScreenEnabled] = actionButtonFullScreenEnabled
}
if let actionButtonPipEnabled = json["actionButtonPipEnabled"].bool {
Defaults[.actionButtonPipEnabled] = actionButtonPipEnabled
}
if let actionButtonLockOrientationEnabled = json["actionButtonLockOrientationEnabled"].bool {
Defaults[.actionButtonLockOrientationEnabled] = actionButtonLockOrientationEnabled
}
if let actionButtonRestartEnabled = json["actionButtonRestartEnabled"].bool {
Defaults[.actionButtonRestartEnabled] = actionButtonRestartEnabled
}
if let actionButtonAdvanceToNextItemEnabled = json["actionButtonAdvanceToNextItemEnabled"].bool {
Defaults[.actionButtonAdvanceToNextItemEnabled] = actionButtonAdvanceToNextItemEnabled
}
if let actionButtonMusicModeEnabled = json["actionButtonMusicModeEnabled"].bool {
Defaults[.actionButtonMusicModeEnabled] = actionButtonMusicModeEnabled
}
}
}

View File

@@ -0,0 +1,54 @@
import Defaults
import SwiftyJSON
struct HistorySettingsGroupImporter {
var json: JSON
func performImport() {
if let saveRecents = json["saveRecents"].bool {
Defaults[.saveRecents] = saveRecents
}
if let saveHistory = json["saveHistory"].bool {
Defaults[.saveHistory] = saveHistory
}
if let showWatchingProgress = json["showWatchingProgress"].bool {
Defaults[.showWatchingProgress] = showWatchingProgress
}
if let saveLastPlayed = json["saveLastPlayed"].bool {
Defaults[.saveLastPlayed] = saveLastPlayed
}
if let watchedVideoPlayNowBehaviorString = json["watchedVideoPlayNowBehavior"].string,
let watchedVideoPlayNowBehavior = WatchedVideoPlayNowBehavior(rawValue: watchedVideoPlayNowBehaviorString)
{
Defaults[.watchedVideoPlayNowBehavior] = watchedVideoPlayNowBehavior
}
if let watchedThreshold = json["watchedThreshold"].int {
Defaults[.watchedThreshold] = watchedThreshold
}
if let resetWatchedStatusOnPlaying = json["resetWatchedStatusOnPlaying"].bool {
Defaults[.resetWatchedStatusOnPlaying] = resetWatchedStatusOnPlaying
}
if let watchedVideoStyleString = json["watchedVideoStyle"].string,
let watchedVideoStyle = WatchedVideoStyle(rawValue: watchedVideoStyleString)
{
Defaults[.watchedVideoStyle] = watchedVideoStyle
}
if let watchedVideoBadgeColorString = json["watchedVideoBadgeColor"].string,
let watchedVideoBadgeColor = WatchedVideoBadgeColor(rawValue: watchedVideoBadgeColorString)
{
Defaults[.watchedVideoBadgeColor] = watchedVideoBadgeColor
}
if let showToggleWatchedStatusButton = json["showToggleWatchedStatusButton"].bool {
Defaults[.showToggleWatchedStatusButton] = showToggleWatchedStatusButton
}
}
}

View File

@@ -0,0 +1,84 @@
import Defaults
import SwiftyJSON
struct LocationsSettingsGroupImporter {
var json: JSON
var includePublicLocations = true
var includedInstancesIDs = Set<Instance.ID>()
var includedAccountsIDs = Set<Account.ID>()
var includedAccountsPasswords = [Account.ID: String]()
init(
json: JSON,
includePublicLocations: Bool = true,
includedInstancesIDs: Set<Instance.ID> = [],
includedAccountsIDs: Set<Account.ID> = [],
includedAccountsPasswords: [Account.ID: String] = [:]
) {
self.json = json
self.includePublicLocations = includePublicLocations
self.includedInstancesIDs = includedInstancesIDs
self.includedAccountsIDs = includedAccountsIDs
self.includedAccountsPasswords = includedAccountsPasswords
}
var instances: [Instance] {
if let instances = json["instances"].array {
return instances.compactMap { instanceJSON in
let dict = instanceJSON.dictionaryValue.mapValues { json in json.stringValue }
return InstancesBridge().deserialize(dict)
}
}
return []
}
var accounts: [Account] {
if let accounts = json["accounts"].array {
return accounts.compactMap { accountJSON in
let dict = accountJSON.dictionaryValue.mapValues { json in json.stringValue }
return AccountsBridge().deserialize(dict)
}
}
return []
}
func performImport() {
if includePublicLocations {
Defaults[.instancesManifest] = json["instancesManifest"].string ?? ""
Defaults[.countryOfPublicInstances] = json["countryOfPublicInstances"].string ?? ""
}
instances.filter { includedInstancesIDs.contains($0.id) }.forEach { instance in
_ = InstancesModel.shared.insert(id: instance.id, app: instance.app, name: instance.name, url: instance.apiURLString)
}
if let accounts = json["accounts"].array {
accounts.forEach { accountJSON in
let dict = accountJSON.dictionaryValue.mapValues { json in json.stringValue }
if let account = AccountsBridge().deserialize(dict),
includedAccountsIDs.contains(account.id)
{
var password = account.password
if password?.isEmpty ?? true {
password = includedAccountsPasswords[account.id]
}
if let password,
!password.isEmpty,
let instanceID = account.instanceID,
let instance = InstancesModel.shared.find(instanceID)
{
if !instance.accounts.contains(where: { instanceAccount in
let (username, _) = instanceAccount.credentials
return username == account.username
}) {
_ = AccountsModel.add(instance: instance, id: account.id, name: account.name, username: account.username, password: password)
}
}
}
}
}
}
}

View File

@@ -0,0 +1,70 @@
import Defaults
import SwiftyJSON
struct OtherDataSettingsGroupImporter {
var json: JSON
func performImport() {
if let lastAccountID = json["lastAccountID"].string {
Defaults[.lastAccountID] = lastAccountID
}
if let lastInstanceID = json["lastInstanceID"].string {
Defaults[.lastInstanceID] = lastInstanceID
}
if let playerRate = json["playerRate"].double {
Defaults[.playerRate] = playerRate
}
if let trendingCategoryString = json["trendingCategory"].string,
let trendingCategory = TrendingCategory(rawValue: trendingCategoryString)
{
Defaults[.trendingCategory] = trendingCategory
}
if let trendingCountryString = json["trendingCountry"].string,
let trendingCountry = Country(rawValue: trendingCountryString)
{
Defaults[.trendingCountry] = trendingCountry
}
if let subscriptionsViewPageString = json["subscriptionsViewPage"].string,
let subscriptionsViewPage = SubscriptionsView.Page(rawValue: subscriptionsViewPageString)
{
Defaults[.subscriptionsViewPage] = subscriptionsViewPage
}
if let subscriptionsListingStyle = json["subscriptionsListingStyle"].string {
Defaults[.subscriptionsListingStyle] = ListingStyle(rawValue: subscriptionsListingStyle) ?? .list
}
if let popularListingStyle = json["popularListingStyle"].string {
Defaults[.popularListingStyle] = ListingStyle(rawValue: popularListingStyle) ?? .list
}
if let trendingListingStyle = json["trendingListingStyle"].string {
Defaults[.trendingListingStyle] = ListingStyle(rawValue: trendingListingStyle) ?? .list
}
if let playlistListingStyle = json["playlistListingStyle"].string {
Defaults[.playlistListingStyle] = ListingStyle(rawValue: playlistListingStyle) ?? .list
}
if let channelPlaylistListingStyle = json["channelPlaylistListingStyle"].string {
Defaults[.channelPlaylistListingStyle] = ListingStyle(rawValue: channelPlaylistListingStyle) ?? .list
}
if let searchListingStyle = json["searchListingStyle"].string {
Defaults[.searchListingStyle] = ListingStyle(rawValue: searchListingStyle) ?? .list
}
if let hideShorts = json["hideShorts"].bool {
Defaults[.hideShorts] = hideShorts
}
if let hideWatched = json["hideWatched"].bool {
Defaults[.hideWatched] = hideWatched
}
}
}

View File

@@ -0,0 +1,100 @@
import Defaults
import SwiftyJSON
struct PlayerSettingsGroupImporter {
var json: JSON
func performImport() {
if let playerInstanceID = json["playerInstanceID"].string {
Defaults[.playerInstanceID] = playerInstanceID
}
if let pauseOnHidingPlayer = json["pauseOnHidingPlayer"].bool {
Defaults[.pauseOnHidingPlayer] = pauseOnHidingPlayer
}
if let closeVideoOnEOF = json["closeVideoOnEOF"].bool {
Defaults[.closeVideoOnEOF] = closeVideoOnEOF
}
if let expandVideoDescription = json["expandVideoDescription"].bool {
Defaults[.expandVideoDescription] = expandVideoDescription
}
if let collapsedLinesDescription = json["collapsedLinesDescription"].int {
Defaults[.collapsedLinesDescription] = collapsedLinesDescription
}
if let showChapters = json["showChapters"].bool {
Defaults[.showChapters] = showChapters
}
if let expandChapters = json["expandChapters"].bool {
Defaults[.expandChapters] = expandChapters
}
if let showRelated = json["showRelated"].bool {
Defaults[.showRelated] = showRelated
}
if let showInspectorString = json["showInspector"].string,
let showInspector = ShowInspectorSetting(rawValue: showInspectorString)
{
Defaults[.showInspector] = showInspector
}
if let playerSidebarString = json["playerSidebar"].string,
let playerSidebar = PlayerSidebarSetting(rawValue: playerSidebarString)
{
Defaults[.playerSidebar] = playerSidebar
}
if let showKeywords = json["showKeywords"].bool {
Defaults[.showKeywords] = showKeywords
}
if let enableReturnYouTubeDislike = json["enableReturnYouTubeDislike"].bool {
Defaults[.enableReturnYouTubeDislike] = enableReturnYouTubeDislike
}
if let closePiPOnNavigation = json["closePiPOnNavigation"].bool {
Defaults[.closePiPOnNavigation] = closePiPOnNavigation
}
if let closePiPOnOpeningPlayer = json["closePiPOnOpeningPlayer"].bool {
Defaults[.closePiPOnOpeningPlayer] = closePiPOnOpeningPlayer
}
if let closePlayerOnOpeningPiP = json["closePlayerOnOpeningPiP"].bool {
Defaults[.closePlayerOnOpeningPiP] = closePlayerOnOpeningPiP
}
#if !os(macOS)
if let pauseOnEnteringBackground = json["pauseOnEnteringBackground"].bool {
Defaults[.pauseOnEnteringBackground] = pauseOnEnteringBackground
}
#endif
#if !os(tvOS)
if let showScrollToTopInComments = json["showScrollToTopInComments"].bool {
Defaults[.showScrollToTopInComments] = showScrollToTopInComments
}
#endif
#if os(iOS)
if let honorSystemOrientationLock = json["honorSystemOrientationLock"].bool {
Defaults[.honorSystemOrientationLock] = honorSystemOrientationLock
}
if let enterFullscreenInLandscape = json["enterFullscreenInLandscape"].bool {
Defaults[.enterFullscreenInLandscape] = enterFullscreenInLandscape
}
if let rotateToLandscapeOnEnterFullScreenString = json["rotateToLandscapeOnEnterFullScreen"].string,
let rotateToLandscapeOnEnterFullScreen = FullScreenRotationSetting(rawValue: rotateToLandscapeOnEnterFullScreenString)
{
Defaults[.rotateToLandscapeOnEnterFullScreen] = rotateToLandscapeOnEnterFullScreen
}
#endif
}
}

View File

@@ -0,0 +1,37 @@
import Defaults
import SwiftyJSON
struct QualitySettingsGroupImporter {
var json: JSON
func performImport() {
if let batteryCellularProfileString = json["batteryCellularProfile"].string {
Defaults[.batteryCellularProfile] = batteryCellularProfileString
}
if let batteryNonCellularProfileString = json["batteryNonCellularProfile"].string {
Defaults[.batteryNonCellularProfile] = batteryNonCellularProfileString
}
if let chargingCellularProfileString = json["chargingCellularProfile"].string {
Defaults[.chargingCellularProfile] = chargingCellularProfileString
}
if let chargingNonCellularProfileString = json["chargingNonCellularProfile"].string {
Defaults[.chargingNonCellularProfile] = chargingNonCellularProfileString
}
if let forceAVPlayerForLiveStreams = json["forceAVPlayerForLiveStreams"].bool {
Defaults[.forceAVPlayerForLiveStreams] = forceAVPlayerForLiveStreams
}
if let qualityProfiles = json["qualityProfiles"].array {
qualityProfiles.forEach { qualityProfileJSON in
let dict = qualityProfileJSON.dictionaryValue.mapValues { json in json.stringValue }
if let item = QualityProfileBridge().deserialize(dict) {
QualityProfilesModel.shared.update(item, item)
}
}
}
}
}

View File

@@ -0,0 +1,17 @@
import Defaults
import SwiftyJSON
struct RecentlyOpenedImporter {
var json: JSON
func performImport() {
if let recentlyOpened = json["recentlyOpened"].array {
recentlyOpened.forEach { recentlyOpenedJSON in
let dict = recentlyOpenedJSON.dictionaryValue.mapValues { json in json.stringValue }
if let item = RecentItemBridge().deserialize(dict) {
RecentsModel.shared.add(item)
}
}
}
}
}

View File

@@ -0,0 +1,16 @@
import Defaults
import SwiftyJSON
struct SponsorBlockSettingsGroupImporter {
var json: JSON
func performImport() {
if let sponsorBlockInstance = json["sponsorBlockInstance"].string {
Defaults[.sponsorBlockInstance] = sponsorBlockInstance
}
if let sponsorBlockCategories = json["sponsorBlockCategories"].array {
Defaults[.sponsorBlockCategories] = Set(sponsorBlockCategories.compactMap { $0.string })
}
}
}

View File

@@ -107,6 +107,10 @@ final class NavigationModel: ObservableObject {
@Published var presentingFileImporter = false
@Published var presentingSettingsImportSheet = false
@Published var presentingSettingsFileImporter = false
@Published var settingsImportURL: URL?
func openChannel(_ channel: Channel, navigationStyle: NavigationStyle) {
guard channel.id != Video.fixtureChannelID else {
return
@@ -269,6 +273,8 @@ final class NavigationModel: ObservableObject {
presentingChannel = false
presentingPlaylist = false
presentingOpenVideos = false
presentingFileImporter = false
presentingSettingsImportSheet = false
}
func hideKeyboard() {
@@ -279,8 +285,9 @@ final class NavigationModel: ObservableObject {
func presentAlert(title: String, message: String? = nil) {
let message = message.isNil ? nil : Text(message!)
alert = Alert(title: Text(title), message: message)
presentingAlert = true
let alert = Alert(title: Text(title), message: message)
presentAlert(alert)
}
func presentRequestErrorAlert(_ error: RequestError) {
@@ -289,6 +296,11 @@ final class NavigationModel: ObservableObject {
}
func presentAlert(_ alert: Alert) {
guard !presentingSettings else {
SettingsModel.shared.presentAlert(alert)
return
}
self.alert = alert
presentingAlert = true
}
@@ -311,6 +323,16 @@ final class NavigationModel: ObservableObject {
print("not implemented")
}
}
func presentSettingsImportSheet(_ url: URL, forceSettings: Bool = false) {
guard !presentingSettings, !forceSettings else {
ImportExportSettingsModel.shared.reset()
SettingsModel.shared.presentSettingsImportSheet(url)
return
}
settingsImportURL = url
presentingSettingsImportSheet = true
}
}
typealias TabSelection = NavigationModel.TabSelection

View File

@@ -596,6 +596,8 @@ final class AVPlayerBackend: PlayerBackend {
if self.controlsUpdates {
self.updateControls()
}
self.model.updateTime(self.currentTime!)
}
}

View File

@@ -182,13 +182,21 @@ final class MPVBackend: PlayerBackend {
}
init() {
// swiftlint:disable shorthand_optional_binding
clientTimer = .init(interval: .seconds(Self.timeUpdateInterval), mode: .infinite) { [weak self] _ in
self?.getTimeUpdates()
guard let self = self, self.model.activeBackend == .mpv else {
return
}
self.getTimeUpdates()
}
networkStateTimer = .init(interval: .seconds(Self.networkStateUpdateInterval), mode: .infinite) { [weak self] _ in
self?.updateNetworkState()
guard let self = self, self.model.activeBackend == .mpv else {
return
}
self.updateNetworkState()
}
// swiftlint:enable shorthand_optional_binding
}
typealias AreInIncreasingOrder = (Stream, Stream) -> Bool
@@ -432,6 +440,8 @@ final class MPVBackend: PlayerBackend {
timeObserverThrottle.execute {
self.model.updateWatch(time: self.currentTime)
}
self.model.updateTime(self.currentTime!)
}
private func stopClientUpdates() {

View File

@@ -67,6 +67,7 @@ final class MPVClient: ObservableObject {
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_initialize(mpv))

View File

@@ -131,6 +131,8 @@ final class PlayerModel: ObservableObject {
@Default(.rotateToLandscapeOnEnterFullScreen) private var rotateToLandscapeOnEnterFullScreen
#endif
@Published var currentChapterIndex: Int?
var accounts: AccountsModel { .shared }
var comments: CommentsModel { .shared }
var controls: PlayerControlsModel { .shared }
@@ -1112,4 +1114,36 @@ final class PlayerModel: ObservableObject {
onPlayStream.forEach { $0(stream) }
onPlayStream.removeAll()
}
func updateTime(_ cmTime: CMTime) {
let time = CMTimeGetSeconds(cmTime)
let newChapterIndex = chapterForTime(time)
if currentChapterIndex != newChapterIndex {
DispatchQueue.main.async {
self.currentChapterIndex = newChapterIndex
}
}
}
private func chapterForTime(_ time: Double) -> Int? {
guard let chapters = self.videoForDisplay?.chapters else {
return nil
}
for (index, chapter) in chapters.enumerated() {
let nextChapterStartTime = index < (chapters.count - 1) ? chapters[index + 1].start : nil
if let nextChapterStart = nextChapterStartTime {
if time >= chapter.start, time < nextChapterStart {
return index
}
} else {
if time >= chapter.start {
return index
}
}
}
return nil
}
}

View File

@@ -7,6 +7,9 @@ final class SettingsModel: ObservableObject {
@Published var presentingAlert = false
@Published var alert = Alert(title: Text("Error"))
@Published var presentingSettingsImportSheet = false
@Published var settingsImportURL: URL?
func presentAlert(title: String, message: String? = nil) {
let message = message.isNil ? nil : Text(message!)
alert = Alert(title: Text(title), message: message)
@@ -17,4 +20,9 @@ final class SettingsModel: ObservableObject {
self.alert = alert
presentingAlert = true
}
func presentSettingsImportSheet(_ url: URL) {
settingsImportURL = url
presentingSettingsImportSheet = true
}
}

View File

@@ -364,6 +364,7 @@ struct ChannelVideosView: View {
}
} label: {
Label("Unsubscribe", systemImage: "xmark.circle")
.help("Unsubscribe")
#if os(iOS)
.labelStyle(.automatic)
#else
@@ -380,6 +381,7 @@ struct ChannelVideosView: View {
}
} label: {
Label("Subscribe", systemImage: "circle")
.help("Subscribe")
#if os(iOS)
.labelStyle(.automatic)
#else
@@ -413,6 +415,7 @@ struct ChannelVideosView: View {
feed.markChannelAsWatched(channel.id)
} label: {
Label("Mark channel feed as watched", systemImage: "checkmark.circle.fill")
.help("Mark channel feed as watched")
}
.disabled(!feed.canMarkAllFeedAsWatched)
}
@@ -423,6 +426,7 @@ struct ChannelVideosView: View {
feed.markChannelAsUnwatched(channel.id)
} label: {
Label("Mark channel feed as unwatched", systemImage: "checkmark.circle")
.help("Mark channel feed as unwatched")
}
}

View File

@@ -2,7 +2,7 @@ import Defaults
import Foundation
import SwiftUI
struct Constants {
enum Constants {
static let yatteeProtocol = "yattee://"
static let overlayAnimation = Animation.linear(duration: 0.2)
static var isIPhone: Bool {
@@ -61,6 +61,26 @@ struct Constants {
#endif
}
static var deviceName: String {
#if os(macOS)
Host().localizedName ?? "Mac"
#else
UIDevice.current.name
#endif
}
static var platform: String {
#if os(macOS)
"macOS"
#elseif os(iOS)
"iOS"
#elseif os(tvOS)
"tvOS"
#else
"unknown"
#endif
}
static func seekIcon(_ type: String, _ interval: TimeInterval) -> String {
let interval = Int(interval)
let allVersions = [10, 15, 30, 45, 60, 75, 90]

View File

@@ -6,37 +6,22 @@ import SwiftUI
#endif
extension Defaults.Keys {
static let instancesManifest = Key<String>("instancesManifest", default: "")
static let countryOfPublicInstances = Key<String?>("countryOfPublicInstances")
static let instances = Key<[Instance]>("instances", default: [])
static let accounts = Key<[Account]>("accounts", default: [])
static let lastAccountID = Key<Account.ID?>("lastAccountID")
static let lastInstanceID = Key<Instance.ID?>("lastInstanceID")
static let lastUsedPlaylistID = Key<Playlist.ID?>("lastPlaylistID")
static let lastAccountIsPublic = Key<Bool>("lastAccountIsPublic", default: false)
static let sponsorBlockInstance = Key<String>("sponsorBlockInstance", default: "https://sponsor.ajay.app")
static let sponsorBlockCategories = Key<Set<String>>("sponsorBlockCategories", default: Set(SponsorBlockAPI.categories))
static let enableReturnYouTubeDislike = Key<Bool>("enableReturnYouTubeDislike", default: false)
// MARK: GROUP - Browsing
static let showHome = Key<Bool>("showHome", default: true)
static let showOpenActionsInHome = Key<Bool>("showOpenActionsInHome", default: true)
static let showQueueInHome = Key<Bool>("showQueueInHome", default: true)
static let showOpenActionsToolbarItem = Key<Bool>("showOpenActionsToolbarItem", default: false)
static let showFavoritesInHome = Key<Bool>("showFavoritesInHome", default: true)
static let favorites = Key<[FavoriteItem]>("favorites", default: [])
static let widgetsSettings = Key<[WidgetSettings]>("widgetsSettings", default: [])
static let startupSection = Key<StartupSection>("startupSection", default: .home)
static let visibleSections = Key<Set<VisibleSection>>("visibleSections", default: [.subscriptions, .trending, .playlists])
static let showOpenActionsToolbarItem = Key<Bool>("showOpenActionsToolbarItem", default: false)
#if os(iOS)
static let showDocuments = Key<Bool>("showDocuments", default: false)
static let lockPortraitWhenBrowsing = Key<Bool>("lockPortraitWhenBrowsing", default: UIDevice.current.userInterfaceIdiom == .phone)
#endif
static let homeHistoryItems = Key<Int>("homeHistoryItems", default: 10)
static let favorites = Key<[FavoriteItem]>("favorites", default: [])
static let playerButtonSingleTapGesture = Key<PlayerTapGestureAction>("playerButtonSingleTapGesture", default: .togglePlayer)
static let playerButtonDoubleTapGesture = Key<PlayerTapGestureAction>("playerButtonDoubleTapGesture", default: .nothing)
static let playerButtonShowsControlButtonsWhenMinimized = Key<Bool>("playerButtonShowsControlButtonsWhenMinimized", default: false)
static let playerButtonIsExpanded = Key<Bool>("playerButtonIsExpanded", default: false)
static let playerBarMaxWidth = Key<String>("playerBarMaxWidth", default: "600")
#if !os(tvOS)
#if os(macOS)
@@ -46,21 +31,139 @@ extension Defaults.Keys {
#endif
static let accountPickerDisplaysUsername = Key<Bool>("accountPickerDisplaysUsername", default: accountPickerDisplaysUsernameDefault)
#endif
static let accountPickerDisplaysAnonymousAccounts = Key<Bool>("accountPickerDisplaysAnonymousAccounts", default: true)
#if os(iOS)
static let lockPortraitWhenBrowsing = Key<Bool>("lockPortraitWhenBrowsing", default: UIDevice.current.userInterfaceIdiom == .phone)
#endif
static let showUnwatchedFeedBadges = Key<Bool>("showUnwatchedFeedBadges", default: false)
static let keepChannelsWithUnwatchedFeedOnTop = Key<Bool>("keepChannelsWithUnwatchedFeedOnTop", default: true)
static let showToggleWatchedStatusButton = Key<Bool>("showToggleWatchedStatusButton", default: false)
static let expandChannelDescription = Key<Bool>("expandChannelDescription", default: false)
static let keepChannelsWithUnwatchedFeedOnTop = Key<Bool>("keepChannelsWithUnwatchedFeedOnTop", default: true)
static let showChannelAvatarInChannelsLists = Key<Bool>("showChannelAvatarInChannelsLists", default: true)
static let showChannelAvatarInVideosListing = Key<Bool>("showChannelAvatarInVideosListing", default: true)
static let playerButtonSingleTapGesture = Key<PlayerTapGestureAction>("playerButtonSingleTapGesture", default: .togglePlayer)
static let playerButtonDoubleTapGesture = Key<PlayerTapGestureAction>("playerButtonDoubleTapGesture", default: .nothing)
static let playerButtonShowsControlButtonsWhenMinimized = Key<Bool>("playerButtonShowsControlButtonsWhenMinimized", default: false)
static let playerButtonIsExpanded = Key<Bool>("playerButtonIsExpanded", default: false)
static let playerBarMaxWidth = Key<String>("playerBarMaxWidth", default: "600")
static let channelOnThumbnail = Key<Bool>("channelOnThumbnail", default: false)
static let timeOnThumbnail = Key<Bool>("timeOnThumbnail", default: true)
static let roundedThumbnails = Key<Bool>("roundedThumbnails", default: true)
static let thumbnailsQuality = Key<ThumbnailsQuality>("thumbnailsQuality", default: .highest)
static let captionsLanguageCode = Key<String?>("captionsLanguageCode")
static let activeBackend = Key<PlayerBackendType>("activeBackend", default: .mpv)
// MARK: GROUP - Player
static let playerInstanceID = Key<Instance.ID?>("playerInstance")
#if os(tvOS)
static let pauseOnHidingPlayerDefault = true
#else
static let pauseOnHidingPlayerDefault = false
#endif
static let pauseOnHidingPlayer = Key<Bool>("pauseOnHidingPlayer", default: pauseOnHidingPlayerDefault)
static let closeVideoOnEOF = Key<Bool>("closeVideoOnEOF", default: false)
#if !os(macOS)
static let pauseOnEnteringBackground = Key<Bool>("pauseOnEnteringBackground", default: true)
#endif
#if os(iOS)
static let expandVideoDescriptionDefault = Constants.isIPad
#else
static let expandVideoDescriptionDefault = true
#endif
static let expandVideoDescription = Key<Bool>("expandVideoDescription", default: expandVideoDescriptionDefault)
static let collapsedLinesDescription = Key<Int>("collapsedLinesDescription", default: 5)
static let showChapters = Key<Bool>("showChapters", default: true)
static let expandChapters = Key<Bool>("expandChapters", default: true)
static let showRelated = Key<Bool>("showRelated", default: true)
static let showInspector = Key<ShowInspectorSetting>("showInspector", default: .onlyLocal)
static let playerSidebar = Key<PlayerSidebarSetting>("playerSidebar", default: .defaultValue)
static let showKeywords = Key<Bool>("showKeywords", default: false)
#if !os(tvOS)
static let showScrollToTopInComments = Key<Bool>("showScrollToTopInComments", default: true)
#endif
static let enableReturnYouTubeDislike = Key<Bool>("enableReturnYouTubeDislike", default: false)
#if os(iOS)
static let honorSystemOrientationLock = Key<Bool>("honorSystemOrientationLock", default: true)
static let enterFullscreenInLandscape = Key<Bool>("enterFullscreenInLandscape", default: UIDevice.current.userInterfaceIdiom == .phone)
static let rotateToLandscapeOnEnterFullScreen = Key<FullScreenRotationSetting>(
"rotateToLandscapeOnEnterFullScreen",
default: UIDevice.current.userInterfaceIdiom == .phone ? .landscapeRight : .disabled
)
#endif
static let closePiPOnNavigation = Key<Bool>("closePiPOnNavigation", default: false)
static let closePiPOnOpeningPlayer = Key<Bool>("closePiPOnOpeningPlayer", default: false)
static let closePlayerOnOpeningPiP = Key<Bool>("closePlayerOnOpeningPiP", default: false)
#if !os(macOS)
static let closePiPAndOpenPlayerOnEnteringForeground = Key<Bool>("closePiPAndOpenPlayerOnEnteringForeground", default: false)
#endif
// MARK: GROUP - Controls
static let avPlayerUsesSystemControls = Key<Bool>("avPlayerUsesSystemControls", default: true)
static let horizontalPlayerGestureEnabled = Key<Bool>("horizontalPlayerGestureEnabled", default: true)
static let seekGestureSensitivity = Key<Double>("seekGestureSensitivity", default: 30.0)
static let seekGestureSpeed = Key<Double>("seekGestureSpeed", default: 0.5)
#if os(iOS)
static let playerControlsLayoutDefault = UIDevice.current.userInterfaceIdiom == .pad ? PlayerControlsLayout.medium : .small
static let fullScreenPlayerControlsLayoutDefault = UIDevice.current.userInterfaceIdiom == .pad ? PlayerControlsLayout.medium : .small
#elseif os(tvOS)
static let playerControlsLayoutDefault = PlayerControlsLayout.tvRegular
static let fullScreenPlayerControlsLayoutDefault = PlayerControlsLayout.tvRegular
#else
static let playerControlsLayoutDefault = PlayerControlsLayout.medium
static let fullScreenPlayerControlsLayoutDefault = PlayerControlsLayout.medium
#endif
static let playerControlsLayout = Key<PlayerControlsLayout>("playerControlsLayout", default: playerControlsLayoutDefault)
static let fullScreenPlayerControlsLayout = Key<PlayerControlsLayout>("fullScreenPlayerControlsLayout", default: fullScreenPlayerControlsLayoutDefault)
static let systemControlsCommands = Key<SystemControlsCommands>("systemControlsCommands", default: .restartAndAdvanceToNext)
static let buttonBackwardSeekDuration = Key<String>("buttonBackwardSeekDuration", default: "10")
static let buttonForwardSeekDuration = Key<String>("buttonForwardSeekDuration", default: "10")
static let gestureBackwardSeekDuration = Key<String>("gestureBackwardSeekDuration", default: "10")
static let gestureForwardSeekDuration = Key<String>("gestureForwardSeekDuration", default: "10")
static let systemControlsSeekDuration = Key<String>("systemControlsBackwardSeekDuration", default: "10")
#if os(iOS)
static let playerControlsLockOrientationEnabled = Key<Bool>("playerControlsLockOrientationEnabled", default: true)
#endif
#if os(tvOS)
static let playerControlsSettingsEnabledDefault = true
#else
static let playerControlsSettingsEnabledDefault = false
#endif
static let playerControlsSettingsEnabled = Key<Bool>("playerControlsSettingsEnabled", default: playerControlsSettingsEnabledDefault)
static let playerControlsCloseEnabled = Key<Bool>("playerControlsCloseEnabled", default: true)
static let playerControlsRestartEnabled = Key<Bool>("playerControlsRestartEnabled", default: false)
static let playerControlsAdvanceToNextEnabled = Key<Bool>("playerControlsAdvanceToNextEnabled", default: false)
static let playerControlsPlaybackModeEnabled = Key<Bool>("playerControlsPlaybackModeEnabled", default: false)
static let playerControlsMusicModeEnabled = Key<Bool>("playerControlsMusicModeEnabled", default: false)
static let playerActionsButtonLabelStyle = Key<ButtonLabelStyle>("playerActionsButtonLabelStyle", default: .iconAndText)
static let actionButtonShareEnabled = Key<Bool>("actionButtonShareEnabled", default: true)
static let actionButtonAddToPlaylistEnabled = Key<Bool>("actionButtonAddToPlaylistEnabled", default: true)
static let actionButtonSubscribeEnabled = Key<Bool>("actionButtonSubscribeEnabled", default: false)
static let actionButtonSettingsEnabled = Key<Bool>("actionButtonSettingsEnabled", default: true)
static let actionButtonHideEnabled = Key<Bool>("actionButtonHideEnabled", default: false)
static let actionButtonCloseEnabled = Key<Bool>("actionButtonCloseEnabled", default: true)
static let actionButtonFullScreenEnabled = Key<Bool>("actionButtonFullScreenEnabled", default: false)
static let actionButtonPipEnabled = Key<Bool>("actionButtonPipEnabled", default: false)
static let actionButtonLockOrientationEnabled = Key<Bool>("actionButtonLockOrientationEnabled", default: false)
static let actionButtonRestartEnabled = Key<Bool>("actionButtonRestartEnabled", default: false)
static let actionButtonAdvanceToNextItemEnabled = Key<Bool>("actionButtonAdvanceToNextItemEnabled", default: false)
static let actionButtonMusicModeEnabled = Key<Bool>("actionButtonMusicModeEnabled", default: true)
// MARK: GROUP - Quality
static let hd2160pMPVProfile = QualityProfile(id: "hd2160pMPVProfile", backend: .mpv, resolution: .hd2160p60, formats: QualityProfile.Format.allCases)
static let hd1080pMPVProfile = QualityProfile(id: "hd1080pMPVProfile", backend: .mpv, resolution: .hd1080p60, formats: QualityProfile.Format.allCases)
@@ -109,150 +212,67 @@ extension Defaults.Keys {
static let chargingCellularProfileDefault = hd1080pMPVProfile.id
static let chargingNonCellularProfileDefault = hd1080pMPVProfile.id
#endif
static let playerRate = Key<Double>("playerRate", default: 1.0)
static let qualityProfiles = Key<[QualityProfile]>("qualityProfiles", default: qualityProfilesDefault)
static let batteryCellularProfile = Key<QualityProfile.ID>("batteryCellularProfile", default: batteryCellularProfileDefault)
static let batteryNonCellularProfile = Key<QualityProfile.ID>("batteryNonCellularProfile", default: batteryNonCellularProfileDefault)
static let chargingCellularProfile = Key<QualityProfile.ID>("chargingCellularProfile", default: chargingCellularProfileDefault)
static let chargingNonCellularProfile = Key<QualityProfile.ID>("chargingNonCellularProfile", default: chargingNonCellularProfileDefault)
static let forceAVPlayerForLiveStreams = Key<Bool>("forceAVPlayerForLiveStreams", default: true)
static let playerSidebar = Key<PlayerSidebarSetting>("playerSidebar", default: .defaultValue)
static let playerInstanceID = Key<Instance.ID?>("playerInstance")
#if os(iOS)
static let playerControlsLayoutDefault = UIDevice.current.userInterfaceIdiom == .pad ? PlayerControlsLayout.medium : .small
static let fullScreenPlayerControlsLayoutDefault = UIDevice.current.userInterfaceIdiom == .pad ? PlayerControlsLayout.medium : .small
#elseif os(tvOS)
static let playerControlsLayoutDefault = PlayerControlsLayout.tvRegular
static let fullScreenPlayerControlsLayoutDefault = PlayerControlsLayout.tvRegular
#else
static let playerControlsLayoutDefault = PlayerControlsLayout.medium
static let fullScreenPlayerControlsLayoutDefault = PlayerControlsLayout.medium
#endif
static let qualityProfiles = Key<[QualityProfile]>("qualityProfiles", default: qualityProfilesDefault)
static let playerControlsLayout = Key<PlayerControlsLayout>("playerControlsLayout", default: playerControlsLayoutDefault)
static let fullScreenPlayerControlsLayout = Key<PlayerControlsLayout>("fullScreenPlayerControlsLayout", default: fullScreenPlayerControlsLayoutDefault)
static let avPlayerUsesSystemControls = Key<Bool>("avPlayerUsesSystemControls", default: true)
static let horizontalPlayerGestureEnabled = Key<Bool>("horizontalPlayerGestureEnabled", default: true)
static let seekGestureSpeed = Key<Double>("seekGestureSpeed", default: 0.5)
static let seekGestureSensitivity = Key<Double>("seekGestureSensitivity", default: 30.0)
static let showKeywords = Key<Bool>("showKeywords", default: false)
#if !os(tvOS)
static let showScrollToTopInComments = Key<Bool>("showScrollToTopInComments", default: true)
#endif
#if os(iOS)
static let expandVideoDescriptionDefault = Constants.isIPad
#else
static let expandVideoDescriptionDefault = true
#endif
static let expandVideoDescription = Key<Bool>("expandVideoDescription", default: expandVideoDescriptionDefault)
static let collapsedLinesDescription = Key<Int>("collapsedLinesDescription", default: 5)
static let showChannelAvatarInChannelsLists = Key<Bool>("showChannelAvatarInChannelsLists", default: true)
static let showChannelAvatarInVideosListing = Key<Bool>("showChannelAvatarInVideosListing", default: true)
#if os(tvOS)
static let pauseOnHidingPlayerDefault = true
#else
static let pauseOnHidingPlayerDefault = false
#endif
static let pauseOnHidingPlayer = Key<Bool>("pauseOnHidingPlayer", default: pauseOnHidingPlayerDefault)
#if !os(macOS)
static let pauseOnEnteringBackground = Key<Bool>("pauseOnEnteringBackground", default: true)
#endif
static let closeVideoOnEOF = Key<Bool>("closeVideoOnEOF", default: false)
static let closePiPOnNavigation = Key<Bool>("closePiPOnNavigation", default: false)
static let closePiPOnOpeningPlayer = Key<Bool>("closePiPOnOpeningPlayer", default: false)
#if !os(macOS)
static let closePiPAndOpenPlayerOnEnteringForeground = Key<Bool>("closePiPAndOpenPlayerOnEnteringForeground", default: false)
#endif
static let closePlayerOnOpeningPiP = Key<Bool>("closePlayerOnOpeningPiP", default: false)
static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: [])
static let queue = Key<[PlayerQueueItem]>("queue", default: [])
static let saveLastPlayed = Key<Bool>("saveLastPlayed", default: false)
static let lastPlayed = Key<PlayerQueueItem?>("lastPlayed")
static let playbackMode = Key<PlayerModel.PlaybackMode>("playbackMode", default: .queue)
// MARK: GROUP - History
static let saveRecents = Key<Bool>("saveRecents", default: true)
static let saveHistory = Key<Bool>("saveHistory", default: true)
static let showWatchingProgress = Key<Bool>("showWatchingProgress", default: true)
static let saveLastPlayed = Key<Bool>("saveLastPlayed", default: false)
static let watchedVideoPlayNowBehavior = Key<WatchedVideoPlayNowBehavior>("watchedVideoPlayNowBehavior", default: .continue)
static let watchedThreshold = Key<Int>("watchedThreshold", default: 90)
static let resetWatchedStatusOnPlaying = Key<Bool>("resetWatchedStatusOnPlaying", default: false)
static let watchedVideoStyle = Key<WatchedVideoStyle>("watchedVideoStyle", default: .badge)
static let watchedVideoBadgeColor = Key<WatchedVideoBadgeColor>("WatchedVideoBadgeColor", default: .red)
static let watchedVideoPlayNowBehavior = Key<WatchedVideoPlayNowBehavior>("watchedVideoPlayNowBehavior", default: .continue)
static let resetWatchedStatusOnPlaying = Key<Bool>("resetWatchedStatusOnPlaying", default: false)
static let saveRecents = Key<Bool>("saveRecents", default: true)
static let showToggleWatchedStatusButton = Key<Bool>("showToggleWatchedStatusButton", default: false)
static let trendingCategory = Key<TrendingCategory>("trendingCategory", default: .default)
static let trendingCountry = Key<Country>("trendingCountry", default: .us)
// MARK: GROUP - SponsorBlock
static let visibleSections = Key<Set<VisibleSection>>("visibleSections", default: [.subscriptions, .trending, .playlists])
static let startupSection = Key<StartupSection>("startupSection", default: .home)
static let sponsorBlockInstance = Key<String>("sponsorBlockInstance", default: "https://sponsor.ajay.app")
static let sponsorBlockCategories = Key<Set<String>>("sponsorBlockCategories", default: Set(SponsorBlockAPI.categories))
#if os(iOS)
static let honorSystemOrientationLock = Key<Bool>("honorSystemOrientationLock", default: true)
static let enterFullscreenInLandscape = Key<Bool>("enterFullscreenInLandscape", default: UIDevice.current.userInterfaceIdiom == .phone)
static let rotateToLandscapeOnEnterFullScreen = Key<FullScreenRotationSetting>(
"rotateToLandscapeOnEnterFullScreen",
default: UIDevice.current.userInterfaceIdiom == .phone ? .landscapeRight : .disabled
)
#endif
// MARK: GROUP - Locations
static let instancesManifest = Key<String>("instancesManifest", default: "")
static let countryOfPublicInstances = Key<String?>("countryOfPublicInstances")
static let instances = Key<[Instance]>("instances", default: [])
static let accounts = Key<[Account]>("accounts", default: [])
// MARK: Group - Advanced
static let showMPVPlaybackStats = Key<Bool>("showMPVPlaybackStats", default: false)
static let showPlayNowInBackendContextMenu = Key<Bool>("showPlayNowInBackendContextMenu", default: false)
#if os(macOS)
static let playerDetailsPageButtonLabelStyleDefault = ButtonLabelStyle.iconAndText
#else
static let playerDetailsPageButtonLabelStyleDefault = UIDevice.current.userInterfaceIdiom == .phone ? ButtonLabelStyle.iconOnly : .iconAndText
#endif
static let playerActionsButtonLabelStyle = Key<ButtonLabelStyle>("playerActionsButtonLabelStyle", default: .iconAndText)
static let systemControlsCommands = Key<SystemControlsCommands>("systemControlsCommands", default: .restartAndAdvanceToNext)
static let buttonBackwardSeekDuration = Key<String>("buttonBackwardSeekDuration", default: "10")
static let buttonForwardSeekDuration = Key<String>("buttonForwardSeekDuration", default: "10")
static let gestureBackwardSeekDuration = Key<String>("gestureBackwardSeekDuration", default: "10")
static let gestureForwardSeekDuration = Key<String>("gestureForwardSeekDuration", default: "10")
static let systemControlsSeekDuration = Key<String>("systemControlsBackwardSeekDuration", default: "10")
static let actionButtonShareEnabled = Key<Bool>("actionButtonShareEnabled", default: true)
static let actionButtonAddToPlaylistEnabled = Key<Bool>("actionButtonAddToPlaylistEnabled", default: true)
static let actionButtonSubscribeEnabled = Key<Bool>("actionButtonSubscribeEnabled", default: false)
static let actionButtonSettingsEnabled = Key<Bool>("actionButtonSettingsEnabled", default: true)
static let actionButtonHideEnabled = Key<Bool>("actionButtonHideEnabled", default: false)
static let actionButtonCloseEnabled = Key<Bool>("actionButtonCloseEnabled", default: true)
static let actionButtonFullScreenEnabled = Key<Bool>("actionButtonFullScreenEnabled", default: false)
static let actionButtonPipEnabled = Key<Bool>("actionButtonPipEnabled", default: false)
static let actionButtonLockOrientationEnabled = Key<Bool>("actionButtonLockOrientationEnabled", default: false)
static let actionButtonRestartEnabled = Key<Bool>("actionButtonRestartEnabled", default: false)
static let actionButtonAdvanceToNextItemEnabled = Key<Bool>("actionButtonAdvanceToNextItemEnabled", default: false)
static let actionButtonMusicModeEnabled = Key<Bool>("actionButtonMusicModeEnabled", default: true)
#if os(iOS)
static let playerControlsLockOrientationEnabled = Key<Bool>("playerControlsLockOrientationEnabled", default: true)
#endif
#if os(tvOS)
static let playerControlsSettingsEnabledDefault = true
#else
static let playerControlsSettingsEnabledDefault = false
#endif
static let playerControlsSettingsEnabled = Key<Bool>("playerControlsSettingsEnabled", default: playerControlsSettingsEnabledDefault)
static let playerControlsCloseEnabled = Key<Bool>("playerControlsCloseEnabled", default: true)
static let playerControlsRestartEnabled = Key<Bool>("playerControlsRestartEnabled", default: false)
static let playerControlsAdvanceToNextEnabled = Key<Bool>("playerControlsAdvanceToNextEnabled", default: false)
static let playerControlsPlaybackModeEnabled = Key<Bool>("playerControlsPlaybackModeEnabled", default: false)
static let playerControlsMusicModeEnabled = Key<Bool>("playerControlsMusicModeEnabled", default: false)
static let showMPVPlaybackStats = Key<Bool>("showMPVPlaybackStats", default: false)
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 mpvEnableLogging = Key<Bool>("mpvEnableLogging", default: false)
static let mpvDeinterlace = Key<Bool>("mpvDeinterlace", default: false)
static let showCacheStatus = Key<Bool>("showCacheStatus", default: false)
static let feedCacheSize = Key<String>("feedCacheSize", default: "50")
// MARK: GROUP - Other exportable
static let lastAccountID = Key<Account.ID?>("lastAccountID")
static let lastInstanceID = Key<Instance.ID?>("lastInstanceID")
static let playerRate = Key<Double>("playerRate", default: 1.0)
static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: [])
static let trendingCategory = Key<TrendingCategory>("trendingCategory", default: .default)
static let trendingCountry = Key<Country>("trendingCountry", default: .us)
static let subscriptionsViewPage = Key<SubscriptionsView.Page>("subscriptionsViewPage", default: .feed)
static let subscriptionsListingStyle = Key<ListingStyle>("subscriptionsListingStyle", default: .cells)
@@ -263,10 +283,22 @@ extension Defaults.Keys {
static let searchListingStyle = Key<ListingStyle>("searchListingStyle", default: .cells)
static let hideShorts = Key<Bool>("hideShorts", default: false)
static let hideWatched = Key<Bool>("hideWatched", default: false)
static let showInspector = Key<ShowInspectorSetting>("showInspector", default: .onlyLocal)
static let showChapters = Key<Bool>("showChapters", default: true)
static let showRelated = Key<Bool>("showRelated", default: true)
static let widgetsSettings = Key<[WidgetSettings]>("widgetsSettings", default: [])
// MARK: GROUP - Not exportable
static let queue = Key<[PlayerQueueItem]>("queue", default: [])
static let playbackMode = Key<PlayerModel.PlaybackMode>("playbackMode", default: .queue)
static let lastPlayed = Key<PlayerQueueItem?>("lastPlayed")
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)
// MARK: LEGACY
static let homeHistoryItems = Key<Int>("homeHistoryItems", default: 10)
}
enum ResolutionSetting: String, CaseIterable, Defaults.Serializable {
@@ -400,6 +432,15 @@ enum ButtonLabelStyle: String, CaseIterable, Defaults.Serializable {
var text: Bool {
self == .iconAndText
}
var description: String {
switch self {
case .iconOnly:
return "Icon only"
case .iconAndText:
return "Icon and text"
}
}
}
enum ThumbnailsQuality: String, CaseIterable, Defaults.Serializable {

View File

@@ -1,6 +1,6 @@
import Foundation
struct Delay {
enum Delay {
@discardableResult static func by(_ interval: TimeInterval, block: @escaping () -> Void) -> Timer {
Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { _ in block() }
}

View File

@@ -22,7 +22,6 @@ struct HomeView: View {
@Default(.favorites) private var favorites
@Default(.widgetsSettings) private var widgetsSettings
#endif
@Default(.homeHistoryItems) private var homeHistoryItems
@Default(.showFavoritesInHome) private var showFavoritesInHome
@Default(.showOpenActionsInHome) private var showOpenActionsInHome
@Default(.showQueueInHome) private var showQueueInHome
@@ -116,14 +115,14 @@ struct HomeView: View {
#endif
}
.onAppear {
Defaults.observe(.favorites) { _ in
favoritesChanged.toggle()
Task {
for await _ in Defaults.updates(.favorites) {
favoritesChanged.toggle()
}
for await _ in Defaults.updates(.widgetsSettings) {
favoritesChanged.toggle()
}
}
.tieToLifetime(of: accounts)
Defaults.observe(.widgetsSettings) { _ in
favoritesChanged.toggle()
}
.tieToLifetime(of: accounts)
}
.redrawOn(change: favoritesChanged)

View File

@@ -30,7 +30,6 @@ struct AccountViewButton: View {
if accountPickerDisplaysUsername {
label
.labelStyle(.titleOnly)
}
}
}
@@ -47,6 +46,6 @@ struct AccountViewButton: View {
}
private var label: some View {
Label(model.current?.description ?? "Select Account", systemImage: "globe")
Text(model.current?.description ?? "Select Account")
}
}

View File

@@ -68,6 +68,7 @@ struct ContentView: View {
SettingsView()
}
)
.modifier(ImportSettingsSheetViewModifier(isPresented: $navigation.presentingSettingsImportSheet, settingsFile: $navigation.settingsImportURL))
.background(
EmptyView().sheet(isPresented: $navigation.presentingAccounts) {
AccountsView()

View File

@@ -14,6 +14,11 @@ struct OpenURLHandler {
var navigationStyle: NavigationStyle
func handle(_ url: URL) {
if url.isFileURL, url.standardizedFileURL.absoluteString.hasSuffix(".\(ImportExportSettingsModel.settingsExtension)") {
navigation.presentSettingsImportSheet(url)
return
}
if Self.firstHandle {
Self.firstHandle = false

View File

@@ -2,28 +2,85 @@ import Foundation
import SDWebImageSwiftUI
import SwiftUI
struct ChapterView: View {
var chapter: Chapter
#if !os(tvOS)
struct ChapterView: View {
var chapter: Chapter
var player = PlayerModel.shared
var chapterIndex: Int
@ObservedObject private var player = PlayerModel.shared
var body: some View {
Button {
player.backend.seek(to: chapter.start, seekType: .userInteracted)
} label: {
Group {
#if os(tvOS)
horizontalChapter
#else
verticalChapter
#endif
}
.contentShape(Rectangle())
var isCurrentChapter: Bool {
player.currentChapterIndex == chapterIndex
}
var body: some View {
Button(action: {
player.backend.seek(to: chapter.start, seekType: .userInteracted)
}) {
Group {
verticalChapter
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
var verticalChapter: some View {
VStack(spacing: 12) {
if !chapter.image.isNil {
smallImage(chapter)
}
VStack(alignment: .leading, spacing: 4) {
Text(chapter.title)
.lineLimit(3)
.multilineTextAlignment(.leading)
.font(.headline)
.foregroundColor(isCurrentChapter ? Color("AppRedColor") : .primary)
Text(chapter.start.formattedAsPlaybackTime(allowZero: true) ?? "")
.font(.system(.subheadline).monospacedDigit())
.foregroundColor(.secondary)
}
.frame(maxWidth: !chapter.image.isNil ? Self.thumbnailWidth : nil, alignment: .leading)
}
}
@ViewBuilder func smallImage(_ chapter: Chapter) -> some View {
WebImage(url: chapter.image, options: [.lowPriority])
.resizable()
.placeholder {
ProgressView()
}
.indicator(.activity)
.frame(width: Self.thumbnailWidth, height: Self.thumbnailHeight)
.mask(RoundedRectangle(cornerRadius: 6))
}
static var thumbnailWidth: Double {
250
}
static var thumbnailHeight: Double {
thumbnailWidth / 1.7777
}
.buttonStyle(.plain)
}
#if os(tvOS)
#else
struct ChapterViewTVOS: View {
var chapter: Chapter
var player = PlayerModel.shared
var body: some View {
Button {
player.backend.seek(to: chapter.start, seekType: .userInteracted)
} label: {
Group {
horizontalChapter
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
var horizontalChapter: some View {
HStack(spacing: 12) {
@@ -41,53 +98,36 @@ struct ChapterView: View {
}
.frame(maxWidth: .infinity, alignment: .leading)
}
#else
var verticalChapter: some View {
VStack(spacing: 12) {
if !chapter.image.isNil {
smallImage(chapter)
@ViewBuilder func smallImage(_ chapter: Chapter) -> some View {
WebImage(url: chapter.image, options: [.lowPriority])
.resizable()
.placeholder {
ProgressView()
}
VStack(alignment: .leading, spacing: 4) {
Text(chapter.title)
.lineLimit(2)
.multilineTextAlignment(.leading)
.font(.headline)
Text(chapter.start.formattedAsPlaybackTime(allowZero: true) ?? "")
.font(.system(.subheadline).monospacedDigit())
.foregroundColor(.secondary)
}
.frame(maxWidth: Self.thumbnailWidth, alignment: .leading)
}
.indicator(.activity)
.frame(width: Self.thumbnailWidth, height: Self.thumbnailHeight)
.mask(RoundedRectangle(cornerRadius: 12))
}
#endif
@ViewBuilder func smallImage(_ chapter: Chapter) -> some View {
WebImage(url: chapter.image, options: [.lowPriority])
.resizable()
.placeholder {
ProgressView()
}
.indicator(.activity)
.frame(width: Self.thumbnailWidth, height: Self.thumbnailHeight)
#if os(tvOS)
.mask(RoundedRectangle(cornerRadius: 12))
#else
.mask(RoundedRectangle(cornerRadius: 6))
#endif
}
static var thumbnailWidth: Double {
250
}
static var thumbnailWidth: Double {
250
static var thumbnailHeight: Double {
thumbnailWidth / 1.7777
}
}
static var thumbnailHeight: Double {
thumbnailWidth / 1.7777
}
}
#endif
struct ChapterView_Preview: PreviewProvider {
static var previews: some View {
ChapterView(chapter: .init(title: "Chapter", start: 30))
.injectFixtureEnvironmentObjects()
#if os(tvOS)
ChapterViewTVOS(chapter: .init(title: "Chapter", start: 30))
.injectFixtureEnvironmentObjects()
#else
ChapterView(chapter: .init(title: "Chapter", start: 30), chapterIndex: 0)
.injectFixtureEnvironmentObjects()
#endif
}
}

View File

@@ -4,6 +4,7 @@ import SwiftUI
struct ChaptersView: View {
@ObservedObject private var player = PlayerModel.shared
@Binding var expand: Bool
var chapters: [Chapter] {
player.videoForDisplay?.chapters ?? []
@@ -15,45 +16,71 @@ struct ChaptersView: View {
var body: some View {
if !chapters.isEmpty {
#if os(tvOS)
List {
Section {
ForEach(chapters) { chapter in
ChapterView(chapter: chapter)
}
}
.listRowBackground(Color.clear)
}
.listStyle(.plain)
#else
if chaptersHaveImages {
ScrollView(.horizontal) {
LazyHStack(spacing: 20) {
if chaptersHaveImages {
#if os(tvOS)
List {
Section {
ForEach(chapters) { chapter in
ChapterView(chapter: chapter)
ChapterViewTVOS(chapter: chapter)
}
}
.padding(.horizontal, 15)
.listRowBackground(Color.clear)
}
.frame(minHeight: ChapterView.thumbnailHeight + 100)
} else {
.listStyle(.plain)
#else
ScrollView(.horizontal) {
LazyHStack(spacing: 20) { chapterViews(for: chapters[...]) }.padding(.horizontal, 15)
}
#endif
} else if expand {
#if os(tvOS)
Section {
ForEach(chapters) { chapter in
ChapterView(chapter: chapter)
ChapterViewTVOS(chapter: chapter)
}
}
.padding(.horizontal)
}
#endif
} else {
NoCommentsView(text: "No chapters information available".localized(), systemImage: "xmark.circle.fill")
#else
Section { chapterViews(for: chapters[...]) }.padding(.horizontal)
#endif
} else {
#if os(iOS)
Button(action: {
self.expand.toggle()
}) {
Section {
chapterViews(for: chapters.prefix(3), opacity: 0.3, clickable: false)
}.padding(.horizontal)
}
#elseif os(macOS)
Section {
chapterViews(for: chapters.prefix(3), opacity: 0.3, clickable: false)
}.padding(.horizontal)
#else
Section {
ForEach(chapters) { chapter in
ChapterViewTVOS(chapter: chapter)
}
}
#endif
}
}
}
#if !os(tvOS)
private func chapterViews(for chaptersToShow: ArraySlice<Chapter>, opacity: Double = 1.0, clickable: Bool = true) -> some View {
ForEach(Array(chaptersToShow.indices), id: \.self) { index in
let chapter = chaptersToShow[index]
ChapterView(chapter: chapter, chapterIndex: index)
.opacity(index == 0 ? 1.0 : opacity)
.allowsHitTesting(clickable)
}
}
#endif
}
struct ChaptersView_Previews: PreviewProvider {
static var previews: some View {
ChaptersView()
ChaptersView(expand: .constant(false))
.injectFixtureEnvironmentObjects()
}
}

View File

@@ -100,7 +100,7 @@ struct CommentView: View {
}
private var authorAvatar: some View {
WebImage(url: URL(string: comment.authorAvatarURL)!, options: [.lowPriority])
WebImage(url: URL(string: comment.authorAvatarURL), options: [.lowPriority])
.resizable()
.placeholder {
Rectangle().fill(Color("PlaceholderColor"))

View File

@@ -56,27 +56,24 @@ struct VideoDescription: View {
}
}
var shouldExpand: Bool {
expand
}
@ViewBuilder var textDescription: some View {
#if canImport(AppKit)
Group {
if #available(macOS 12, *) {
DescriptionWithLinks(description: description, detailsSize: detailsSize)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(shouldExpand ? 500 : collapsedLinesDescription)
.lineLimit(expand ? 500 : collapsedLinesDescription)
.textSelection(.enabled)
} else {
Text(description)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(shouldExpand ? 500 : collapsedLinesDescription)
.lineLimit(expand ? 500 : collapsedLinesDescription)
}
}
.multilineTextAlignment(.leading)
.font(.system(size: 14))
.lineSpacing(3)
.allowsHitTesting(expand)
#endif
}

View File

@@ -169,6 +169,7 @@ struct VideoDetails: View {
@State private var subscriptionToggleButtonDisabled = false
@State private var page = DetailsPage.info
@State private var descriptionExpanded = false
@State private var chaptersExpanded = false
@Environment(\.navigationStyle) private var navigationStyle
#if os(iOS)
@@ -190,6 +191,7 @@ struct VideoDetails: View {
@Default(.showScrollToTopInComments) private var showScrollToTopInComments
#endif
@Default(.expandVideoDescription) private var expandVideoDescription
@Default(.expandChapters) private var expandChapters
var body: some View {
VStack(alignment: .leading, spacing: 0) {
@@ -245,6 +247,7 @@ struct VideoDetails: View {
.background(colorScheme == .dark ? Color.black : .white)
.onAppear {
descriptionExpanded = expandVideoDescription
chaptersExpanded = expandChapters
}
}
@@ -320,7 +323,7 @@ struct VideoDetails: View {
!video.chapters.isEmpty
{
Section(header: chaptersHeader) {
ChaptersView()
ChaptersView(expand: $chaptersExpanded)
}
}
@@ -440,11 +443,48 @@ struct VideoDetails: View {
#endif
}
var chaptersHaveImages: Bool {
player.videoForDisplay?.chapters.allSatisfy { $0.image != nil } ?? false
}
var chaptersHeader: some View {
Text("Chapters".localized())
.padding(.horizontal)
.font(.caption)
.foregroundColor(.secondary)
Group {
if !chaptersHaveImages {
#if canImport(UIKit)
Button(action: {
chaptersExpanded.toggle()
}) {
HStack {
Text("Chapters".localized())
Spacer()
Image(systemName: chaptersExpanded ? "chevron.up" : "chevron.down")
.imageScale(.small)
}
.padding(.horizontal)
.font(.caption)
.foregroundColor(.secondary)
}
#elseif canImport(AppKit)
HStack {
Text("Chapters".localized())
Spacer()
Button(action: { chaptersExpanded.toggle() }) {
Image(systemName: chaptersExpanded ? "chevron.up" : "chevron.down")
.imageScale(.small)
}
}
.padding(.horizontal)
.font(.caption)
.foregroundColor(.secondary)
#endif
} else {
// No button, just the title when there are images
Text("Chapters".localized())
.font(.caption)
.foregroundColor(.secondary)
.padding(.horizontal)
}
}
}
}

View File

@@ -153,7 +153,7 @@ struct AccountForm: View {
return
}
let account = AccountsModel.add(instance: instance, name: name, username: username, password: password)
let account = AccountsModel.add(instance: instance, id: nil, name: name, username: username, password: password)
selectedAccount?.wrappedValue = account
presentationMode.wrappedValue.dismiss()

View File

@@ -5,6 +5,7 @@ struct AdvancedSettings: View {
@Default(.showMPVPlaybackStats) private var showMPVPlaybackStats
@Default(.mpvCacheSecs) private var mpvCacheSecs
@Default(.mpvCachePauseWait) private var mpvCachePauseWait
@Default(.mpvDeinterlace) private var mpvDeinterlace
@Default(.mpvEnableLogging) private var mpvEnableLogging
@Default(.showCacheStatus) private var showCacheStatus
@Default(.feedCacheSize) private var feedCacheSize
@@ -87,6 +88,8 @@ struct AdvancedSettings: View {
}
.multilineTextAlignment(.trailing)
Toggle("deinterlace", isOn: $mpvDeinterlace)
if mpvEnableLogging {
logButton
}

View File

@@ -0,0 +1,167 @@
import SwiftUI
struct ExportSettings: View {
@ObservedObject private var model = ImportExportSettingsModel.shared
@State private var presentingShareSheet = false
@StateObject private var settings = SettingsModel.shared
private var filesToShare = [ImportExportSettingsModel.exportFile]
@ObservedObject private var navigation = NavigationModel.shared
var body: some View {
Group {
#if os(macOS)
VStack {
list
importExportButtons
}
#else
list
#if os(iOS)
.listStyle(.insetGrouped)
.sheet(
isPresented: $presentingShareSheet,
onDismiss: { self.model.isExportInProgress = false }
) {
ShareSheet(activityItems: filesToShare)
.id("settings-share-\(filesToShare.count)")
}
#endif
#endif
}
.navigationTitle("Export Settings")
}
var list: some View {
List {
exportView
}
.onAppear {
model.reset()
}
}
var importExportButtons: some View {
HStack {
importButton
Spacer()
exportButton
}
}
@ViewBuilder var importButton: some View {
#if os(macOS)
Button {
navigation.presentingSettingsFileImporter = true
} label: {
Label("Import", systemImage: "square.and.arrow.down")
}
#endif
}
struct ExportGroupRow: View {
let group: ImportExportSettingsModel.ExportGroup
@ObservedObject private var model = ImportExportSettingsModel.shared
var body: some View {
Button(action: { model.toggleExportGroupSelection(group) }) {
HStack {
Text(group.label)
Spacer()
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
.opacity(isGroupInSelectedGroups ? 1 : 0)
}
.animation(nil, value: isGroupInSelectedGroups)
.contentShape(Rectangle())
}
}
var isGroupInSelectedGroups: Bool {
model.selectedExportGroups.contains(group)
}
}
var exportView: some View {
Group {
Section(header: Text("Settings")) {
ForEach(ImportExportSettingsModel.ExportGroup.settingsGroups) { group in
ExportGroupRow(group: group)
}
}
Section(header: Text("Locations")) {
ForEach(ImportExportSettingsModel.ExportGroup.locationsGroups) { group in
ExportGroupRow(group: group)
.disabled(!model.isGroupEnabled(group))
}
}
Section(header: Text("Other"), footer: otherGroupsFooter) {
ForEach(ImportExportSettingsModel.ExportGroup.otherGroups) { group in
ExportGroupRow(group: group)
}
}
#if !os(macOS)
Section {
exportButton
}
#endif
}
.buttonStyle(.plain)
.disabled(model.isExportInProgress)
}
var exportButton: some View {
Button(action: exportSettings) {
Label(model.isExportInProgress ? "Export in progress..." : "Export...", systemImage: model.isExportInProgress ? "fireworks" : "square.and.arrow.up")
.animation(nil, value: model.isExportInProgress)
#if !os(macOS)
.foregroundColor(.accentColor)
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
#endif
}
.disabled(!model.isExportAvailable)
}
@ViewBuilder var otherGroupsFooter: some View {
Text("Other data include last used playback preferences and listing options")
}
func exportSettings() {
let export = {
model.isExportInProgress = true
Delay.by(0.3) {
model.exportAction()
#if !os(macOS)
self.presentingShareSheet = true
#endif
}
}
if model.isGroupSelected(.accountsUnencryptedPasswords) {
settings.presentAlert(Alert(
title: Text("Are you sure you want to export unencrypted passwords?"),
message: Text("Do not share this file with anyone or you can lose access to your accounts. If you don't select to export passwords you will be asked to provide them during import"),
primaryButton: .destructive(Text("Export"), action: export),
secondaryButton: .cancel()
))
} else {
export()
}
}
}
struct ExportSettings_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
ExportSettings()
}
}
}

View File

@@ -0,0 +1,189 @@
import SwiftUI
struct ImportSettingsAccountRow: View {
var account: Account
var fileModel: ImportSettingsFileModel
@State private var password = ""
@State private var isValid = false
@State private var isValidated = false
@State private var isValidating = false
@State private var validationError: String?
@State private var validationDebounce = Debounce()
@ObservedObject private var model = ImportSettingsSheetViewModel.shared
func afterValidation() {
if isValid {
model.importableAccounts.insert(account.id)
model.selectedAccounts.insert(account.id)
model.importableAccountsPasswords[account.id] = password
} else {
model.selectedAccounts.remove(account.id)
model.importableAccounts.remove(account.id)
model.importableAccountsPasswords.removeValue(forKey: account.id)
}
}
var body: some View {
Button(action: { model.toggleAccount(account, accounts: accounts) }) {
let accountExists = AccountsModel.shared.find(account.id) != nil
VStack(alignment: .leading) {
HStack {
Text(account.username)
Spacer()
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
.opacity(isChecked ? 1 : 0)
}
Text(account.instance?.description ?? "")
.font(.caption)
.foregroundColor(.secondary)
Group {
if let instanceID = account.instanceID {
if accountExists {
HStack {
Image(systemName: "xmark.circle.fill")
.foregroundColor(Color("AppRedColor"))
Text("Account already exists")
}
} else {
Group {
if InstancesModel.shared.find(instanceID) != nil {
HStack {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Text("Custom Location already exists")
}
} else if model.selectedInstances.contains(instanceID) {
HStack {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Text("Custom Location selected for import")
}
} else {
HStack {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.red)
Text("Custom Location not selected for import")
}
.foregroundColor(Color("AppRedColor"))
}
}
.frame(minHeight: 20)
if account.password.isNil || account.password!.isEmpty {
Group {
if password.isEmpty {
HStack {
Image(systemName: "key")
Text("Password required to import")
}
.foregroundColor(Color("AppRedColor"))
} else {
AccountValidationStatus(
app: .constant(instance.app),
isValid: $isValid,
isValidated: $isValidated,
isValidating: $isValidating,
error: $validationError
)
}
}
.frame(minHeight: 20)
} else {
HStack {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Text("Password saved in import file")
}
}
}
}
}
.foregroundColor(.primary)
.font(.caption)
.padding(.vertical, 2)
if !accountExists && (account.password.isNil || account.password!.isEmpty) {
SecureField("Password", text: $password)
.onChange(of: password) { _ in validate() }
#if !os(tvOS)
.textFieldStyle(RoundedBorderTextFieldStyle())
#endif
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
.onChange(of: isValid) { _ in afterValidation() }
.animation(nil, value: isChecked)
}
.buttonStyle(.plain)
}
var isChecked: Bool {
model.isSelectedForImport(account)
}
var locationsSettingsGroupImporter: LocationsSettingsGroupImporter? {
fileModel.locationsSettingsGroupImporter
}
var accounts: [Account] {
fileModel.locationsSettingsGroupImporter?.accounts ?? []
}
private var instance: Instance! {
(fileModel.locationsSettingsGroupImporter?.instances ?? []).first { $0.id == account.instanceID }
}
private var validator: AccountValidator {
AccountValidator(
app: .constant(instance.app),
url: instance.apiURLString,
account: Account(instanceID: instance.id, urlString: instance.apiURLString, username: account.username, password: password),
id: .constant(account.username),
isValid: $isValid,
isValidated: $isValidated,
isValidating: $isValidating,
error: $validationError
)
}
private func validate() {
isValid = false
validationDebounce.invalidate()
guard !account.username.isEmpty, !password.isEmpty else {
validator.reset()
return
}
isValidating = true
validationDebounce.debouncing(1) {
validator.validateAccount()
}
}
}
struct ImportSettingsAccountRow_Previews: PreviewProvider {
static var previews: some View {
let fileModel = ImportSettingsFileModel(url: URL(string: "https://gist.githubusercontent.com/arekf/578668969c9fdef1b3828bea864c3956/raw/f794a95a20261bcb1145e656c8dda00bea339e2a/yattee-recents.yatteesettings")!)
return List {
ImportSettingsAccountRow(
account: .init(name: "arekf", urlString: "https://instance.com", username: "arekf"),
fileModel: fileModel
)
ImportSettingsAccountRow(
account: .init(name: "arekf", urlString: "https://instance.com", username: "arekf", password: "a"),
fileModel: fileModel
)
}
}
}

View File

@@ -0,0 +1,30 @@
import Foundation
import SwiftUI
struct ImportSettingsFileImporterViewModifier: ViewModifier {
@Binding var isPresented: Bool
func body(content: Content) -> some View {
content
.fileImporter(isPresented: $isPresented, allowedContentTypes: [.json]) { result in
do {
let selectedFile = try result.get()
var urlToOpen: URL?
if let bookmarkURL = URLBookmarkModel.shared.loadBookmark(selectedFile) {
urlToOpen = bookmarkURL
}
if selectedFile.startAccessingSecurityScopedResource() {
URLBookmarkModel.shared.saveBookmark(selectedFile)
urlToOpen = selectedFile
}
guard let urlToOpen else { return }
NavigationModel.shared.presentSettingsImportSheet(urlToOpen, forceSettings: true)
} catch {
NavigationModel.shared.presentAlert(title: "Could not open Files")
}
}
}
}

View File

@@ -0,0 +1,262 @@
import SwiftUI
struct ImportSettingsSheetView: View {
@Binding var settingsFile: URL?
@StateObject private var model = ImportSettingsSheetViewModel.shared
@StateObject private var importExportModel = ImportExportSettingsModel.shared
@Environment(\.presentationMode) private var presentationMode
@State private var presentingCompletedAlert = false
private let accountsModel = AccountsModel.shared
var body: some View {
Group {
#if os(macOS)
list
.frame(width: 700, height: 800)
#else
NavigationView {
list
}
#endif
}
.onAppear {
guard let fileModel else { return }
model.reset(fileModel.locationsSettingsGroupImporter)
importExportModel.reset(fileModel)
}
.onChange(of: settingsFile) { _ in
importExportModel.reset(fileModel)
}
}
var list: some View {
List {
importGroupView
importOptions
metadata
}
.alert(isPresented: $presentingCompletedAlert) {
completedAlert
}
#if os(iOS)
.backport
.scrollDismissesKeyboardInteractively()
#endif
.navigationTitle("Import Settings")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(action: { presentationMode.wrappedValue.dismiss() }) {
Text("Cancel")
}
}
ToolbarItem(placement: .confirmationAction) {
Button(action: {
fileModel?.performImport()
presentingCompletedAlert = true
ImportExportSettingsModel.shared.reset()
}) {
Text("Import")
}
.disabled(!canImport)
}
}
}
var completedAlert: Alert {
Alert(
title: Text("Import Completed"),
dismissButton: .default(Text("Close")) {
if accountsModel.isEmpty,
let account = InstancesModel.shared.all.first?.anonymousAccount
{
accountsModel.setCurrent(account)
}
presentationMode.wrappedValue.dismiss()
}
)
}
var canImport: Bool {
return !model.selectedAccounts.isEmpty || !model.selectedInstances.isEmpty || !importExportModel.selectedExportGroups.isEmpty
}
var fileModel: ImportSettingsFileModel? {
guard let settingsFile else { return nil }
return ImportSettingsFileModel(url: settingsFile)
}
var locationsSettingsGroupImporter: LocationsSettingsGroupImporter? {
guard let fileModel else { return nil }
return fileModel.locationsSettingsGroupImporter
}
struct ExportGroupRow: View {
let group: ImportExportSettingsModel.ExportGroup
@ObservedObject private var model = ImportExportSettingsModel.shared
var body: some View {
Button(action: { model.toggleExportGroupSelection(group) }) {
HStack {
Text(group.label)
Spacer()
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
.opacity(isChecked ? 1 : 0)
}
.contentShape(Rectangle())
.foregroundColor(.primary)
.animation(nil, value: isChecked)
}
.buttonStyle(.plain)
}
var isChecked: Bool {
model.selectedExportGroups.contains(group)
}
}
var importGroupView: some View {
Group {
Section(header: Text("Settings")) {
ForEach(ImportExportSettingsModel.ExportGroup.settingsGroups) { group in
ExportGroupRow(group: group)
.disabled(!fileModel!.isGroupIncludedInFile(group))
}
}
Section(header: Text("Other")) {
ForEach(ImportExportSettingsModel.ExportGroup.otherGroups) { group in
ExportGroupRow(group: group)
.disabled(!fileModel!.isGroupIncludedInFile(group))
}
}
}
}
@ViewBuilder var metadata: some View {
if let fileModel {
Section(header: Text("File information")) {
MetadataRow(name: Text("Name"), value: Text(fileModel.filename))
if let date = fileModel.metadataDate {
MetadataRow(name: Text("Date"), value: Text(date))
}
if let build = fileModel.metadataBuild {
MetadataRow(name: Text("Build"), value: Text(build))
}
if let platform = fileModel.metadataPlatform {
MetadataRow(name: Text("Platform"), value: Text(platform))
}
}
}
}
struct MetadataRow: View {
let name: Text
let value: Text
var body: some View {
HStack {
name
.layoutPriority(2)
Spacer()
value
.layoutPriority(1)
.lineLimit(2)
.foregroundColor(.secondary)
}
}
}
var instances: [Instance] {
locationsSettingsGroupImporter?.instances ?? []
}
var accounts: [Account] {
locationsSettingsGroupImporter?.accounts ?? []
}
struct ImportInstanceRow: View {
var instance: Instance
var accounts: [Account]
@ObservedObject private var model = ImportSettingsSheetViewModel.shared
var body: some View {
Button(action: { model.toggleInstance(instance, accounts: accounts) }) {
VStack {
Group {
HStack {
Text(instance.description)
Spacer()
Image(systemName: "checkmark")
.opacity(isChecked ? 1 : 0)
.foregroundColor(.accentColor)
}
if model.isInstanceAlreadyAdded(instance) {
HStack {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.red)
Text("Custom Location already exists")
}
.font(.caption)
.padding(.vertical, 2)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.contentShape(Rectangle())
.foregroundColor(.primary)
.transaction { t in t.animation = nil }
}
.buttonStyle(.plain)
}
var isChecked: Bool {
model.isImportable(instance) && model.selectedInstances.contains(instance.id)
}
}
@ViewBuilder var importOptions: some View {
if let fileModel {
if fileModel.isPublicInstancesSettingsGroupInFile || !instances.isEmpty {
Section(header: Text("Locations")) {
if fileModel.isPublicInstancesSettingsGroupInFile {
ExportGroupRow(group: .locationsSettings)
}
ForEach(instances) { instance in
ImportInstanceRow(instance: instance, accounts: accounts)
}
}
}
if !accounts.isEmpty {
Section(header: Text("Accounts")) {
ForEach(accounts) { account in
ImportSettingsAccountRow(account: account, fileModel: fileModel)
}
}
}
}
}
}
struct ImportSettingsSheetView_Previews: PreviewProvider {
static var previews: some View {
ImportSettingsSheetView(settingsFile: .constant(URL(string: "https://gist.githubusercontent.com/arekf/578668969c9fdef1b3828bea864c3956/raw/f794a95a20261bcb1145e656c8dda00bea339e2a/yattee-recents.yatteesettings")!))
}
}

View File

@@ -0,0 +1,77 @@
import Foundation
import SwiftUI
final class ImportSettingsSheetViewModel: ObservableObject {
static let shared = ImportSettingsSheetViewModel()
@Published var selectedInstances = Set<Instance.ID>()
@Published var selectedAccounts = Set<Account.ID>()
@Published var importableAccounts = Set<Account.ID>()
@Published var importableAccountsPasswords = [Account.ID: String]()
func toggleInstance(_ instance: Instance, accounts: [Account]) {
if selectedInstances.contains(instance.id) {
selectedInstances.remove(instance.id)
} else {
guard isImportable(instance) else { return }
selectedInstances.insert(instance.id)
}
removeNonImportableFromSelectedAccounts(accounts: accounts)
}
func toggleAccount(_ account: Account, accounts: [Account]) {
if selectedAccounts.contains(account.id) {
selectedAccounts.remove(account.id)
} else {
guard isImportable(account.id, accounts: accounts) else { return }
selectedAccounts.insert(account.id)
}
}
func isSelectedForImport(_ account: Account) -> Bool {
importableAccounts.contains(account.id) && selectedAccounts.contains(account.id)
}
func isImportable(_ accountID: Account.ID, accounts: [Account]) -> Bool {
guard let account = accounts.first(where: { $0.id == accountID }),
let instanceID = account.instanceID,
AccountsModel.shared.find(accountID) == nil
else { return false }
return ((account.password != nil && !account.password!.isEmpty) ||
importableAccounts.contains(account.id)) && (
(InstancesModel.shared.find(instanceID) != nil) ||
selectedInstances.contains(instanceID)
)
}
func isImportable(_ instance: Instance) -> Bool {
!isInstanceAlreadyAdded(instance)
}
func isInstanceAlreadyAdded(_ instance: Instance) -> Bool {
InstancesModel.shared.find(instance.id) != nil || InstancesModel.shared.findByURLString(instance.apiURLString) != nil
}
func removeNonImportableFromSelectedAccounts(accounts: [Account]) {
selectedAccounts = Set(selectedAccounts.filter { isImportable($0, accounts: accounts) })
}
func reset() {
selectedAccounts = []
selectedInstances = []
importableAccounts = []
}
func reset(_ importer: LocationsSettingsGroupImporter? = nil) {
reset()
guard let importer else { return }
selectedInstances = Set(importer.instances.filter { isImportable($0) }.map(\.id))
importableAccounts = Set(importer.accounts.filter { isImportable($0.id, accounts: importer.accounts) }.map(\.id))
selectedAccounts = importableAccounts
}
}

View File

@@ -0,0 +1,27 @@
import Foundation
import SwiftUI
import SwiftyJSON
struct ImportSettingsSheetViewModifier: ViewModifier {
@Binding var isPresented: Bool
@Binding var settingsFile: URL?
func body(content: Content) -> some View {
content
.sheet(isPresented: $isPresented) {
ImportSettingsSheetView(settingsFile: $settingsFile)
}
}
}
struct ImportSettingsSheetViewModifier_Previews: PreviewProvider {
static var previews: some View {
Text("")
.modifier(
ImportSettingsSheetViewModifier(
isPresented: .constant(true),
settingsFile: .constant(URL(string: "https://gist.githubusercontent.com/arekf/87b4d6702755b01139431dcb809f9fdc/raw/7bb5cdba3ffc0c479f5260430ddc43c4a79a7a72/yattee-177-iPhone.yatteesettings")!)
)
)
}
}

View File

@@ -15,6 +15,7 @@ struct PlayerControlsSettings: View {
@Default(.gestureBackwardSeekDuration) private var gestureBackwardSeekDuration
@Default(.gestureForwardSeekDuration) private var gestureForwardSeekDuration
@Default(.systemControlsSeekDuration) private var systemControlsSeekDuration
@Default(.playerActionsButtonLabelStyle) private var playerActionsButtonLabelStyle
@Default(.actionButtonShareEnabled) private var actionButtonShareEnabled
@Default(.actionButtonSubscribeEnabled) private var actionButtonSubscribeEnabled
@Default(.actionButtonCloseEnabled) private var actionButtonCloseEnabled
@@ -117,6 +118,15 @@ struct PlayerControlsSettings: View {
Section(header: SettingsHeader(text: "Actions Buttons".localized())) {
actionButtonToggles
}
Section {
Picker("Action button labels", selection: $playerActionsButtonLabelStyle) {
ForEach(ButtonLabelStyle.allCases, id: \.rawValue) { style in
Text(style.description).tag(style)
}
}
.modifier(SettingsPickerModifier())
}
}
private var systemControlsCommandsPicker: some View {

View File

@@ -32,6 +32,7 @@ struct PlayerSettings: View {
@Default(.showInspector) private var showInspector
@Default(.showChapters) private var showChapters
@Default(.expandChapters) private var expandChapters
@Default(.showRelated) private var showRelated
@ObservedObject private var accounts = AccountsModel.shared
@@ -80,6 +81,7 @@ struct PlayerSettings: View {
expandVideoDescriptionToggle
collapsedLineDescriptionStepper
showChaptersToggle
expandChaptersToggle
showRelatedToggle
#if os(macOS)
HStack {
@@ -282,7 +284,13 @@ struct PlayerSettings: View {
}
private var showChaptersToggle: some View {
Toggle("Chapters", isOn: $showChapters)
Toggle("Chapters (if available)", isOn: $showChapters)
}
private var expandChaptersToggle: some View {
Toggle("Open vertical chapters expanded", isOn: $expandChapters)
.disabled(!showChapters)
.foregroundColor(showChapters ? .primary : .secondary)
}
private var showRelatedToggle: some View {

View File

@@ -7,7 +7,7 @@ struct SettingsView: View {
#if os(macOS)
private enum Tabs: Hashable {
case browsing, player, controls, quality, history, sponsorBlock, locations, advanced, help
case browsing, player, controls, quality, history, sponsorBlock, locations, advanced, importExport, help
}
@State private var selection: Tabs = .browsing
@@ -24,13 +24,22 @@ struct SettingsView: View {
@Default(.instances) private var instances
@State private var filesToShare = []
@ObservedObject private var navigation = NavigationModel.shared
@ObservedObject private var settingsModel = SettingsModel.shared
var body: some View {
settings
.alert(isPresented: $model.presentingAlert) { model.alert }
#if os(iOS)
.backport
.scrollDismissesKeyboardInteractively()
#if !os(tvOS)
.modifier(ImportSettingsFileImporterViewModifier(isPresented: $navigation.presentingSettingsFileImporter))
.modifier(ImportSettingsSheetViewModifier(isPresented: $settingsModel.presentingSettingsImportSheet, settingsFile: $settingsModel.settingsImportURL))
#endif
#if os(iOS)
.backport
.scrollDismissesKeyboardInteractively()
#endif
.alert(isPresented: $model.presentingAlert) { model.alert }
}
var settings: some View {
@@ -101,6 +110,14 @@ struct SettingsView: View {
}
.tag(Tabs.advanced)
Group {
ExportSettings()
}
.tabItem {
Label("Export", systemImage: "square.and.arrow.up")
}
.tag(Tabs.importExport)
Form {
Help()
}
@@ -110,7 +127,7 @@ struct SettingsView: View {
.tag(Tabs.help)
}
.padding(20)
.frame(width: 650, height: windowHeight)
.frame(width: 700, height: windowHeight)
#else
NavigationView {
settingsList
@@ -206,6 +223,8 @@ struct SettingsView: View {
.padding(.horizontal, 20)
#endif
importView
Section(footer: helpFooter) {
NavigationLink {
Help()
@@ -260,13 +279,35 @@ struct SettingsView: View {
}
#endif
var importView: some View {
Section {
Button(action: importSettings) {
Label("Import Settings...", systemImage: "square.and.arrow.down")
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
}
.foregroundColor(.accentColor)
.buttonStyle(.plain)
NavigationLink(destination: LazyView(ExportSettings())) {
Label("Export Settings", systemImage: "square.and.arrow.up")
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
}
}
}
func importSettings() {
navigation.presentingSettingsFileImporter = true
}
#if os(macOS)
private var windowHeight: Double {
switch selection {
case .browsing:
return 800
case .player:
return 500
return 550
case .controls:
return 920
case .quality:
@@ -278,7 +319,9 @@ struct SettingsView: View {
case .locations:
return 600
case .advanced:
return 380
return 500
case .importExport:
return 580
case .help:
return 650
}

View File

@@ -13,7 +13,7 @@ struct ControlsBar: View {
@State private var shareURL: URL?
@Binding var expansionState: ExpansionState
@State var gestureThrottle = Throttle(interval: 0.25) // swiftlint:disable:this swiftui_state_private
@State var gestureThrottle = Throttle(interval: 0.25) // swiftlint:disable:this private_swiftui_state
var presentingControls = true
var backgroundEnabled = true

View File

@@ -21,6 +21,14 @@ struct YatteeApp: App {
}
static var logsDirectory: URL {
temporaryDirectory
}
static var settingsExportDirectory: URL {
temporaryDirectory
}
private static var temporaryDirectory: URL {
URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
}

View File

@@ -1,18 +1,18 @@
"Add Account" = "أضف حساب";
"Add Account..." = "أضف حساب…";
"Add Location" = "أضف موقع";
"Add Location..." = "إضافة موقع ...";
"%@ Playlist" = "قائمة التشغيل";
"Add Account" = "إضافة حساب";
"Add Account..." = "إضافة حساب…";
"Add Location" = "إضافة موقع";
"Add Location..." = "إضافة موقع...";
"%@ Playlist" = "قائمة تشغيل %@";
"%@ Channel" = "قناة %@";
"%@ subscribers" = "%@ مشترك";
"Add to %@" = "أضف إلى %@";
"%lld videos" = "%lld مقاطع فيديو";
"Add profile..." = "إضافة ملف تعريف ...";
"%@ subscribers" = "مشتركين %@";
"Add to %@" = "إضافة إلى %@";
"%lld videos" = "مقاطع فيديو %Lld";
"Add profile..." = "إضافة ملف تعريف...";
"Add Quality Profile" = "إضافة ملف تعريف الجودة";
"Add to Playlist" = "أضف إلى قائمة التشغيل";
"Add to Playlist..." = "أضف إلى قائمة التشغيل...";
"Add to Playlist" = "إضافة إلى قائمة تشغيل";
"Add to Playlist..." = "إضافة إلى قائمة تشغيل...";
"Advanced" = "متقدم";
/* Trending category, section containing all kinds of videos */
@@ -22,42 +22,42 @@
"Are you sure you want to restore default quality profiles?" = "هل أنت متأكد من أنك تريد استعادة ملفات تعريف الجودة الافتراضية؟";
"Automatic" = "تلقائي";
"Autoplaying Next" = "التشغيل التلقائي للتالي";
"Are you sure you want to unsubscribe from %@?" = "هل أنت متأكد من رغبتك في إلغاء الاشتراك من ٪@؟";
"Badge & Decreased opacity" = "شارة ونقص التعتيم";
"Are you sure you want to unsubscribe from %@?" = "هل أنت متأكد من رغبتك في إلغاء الإشتراك من ٪@؟";
"Badge & Decreased opacity" = "الشارة و إنخفاض التعتيم";
"Browsing" = "التصفح";
"Based on system color scheme" = "بناء على نظام ألوان الجهاز";
"Battery" = "البطارية";
"Blue" = "أزرق";
"Buffering stream..." = "جار تخزين تيار الفيديو…";
"Buffering stream..." = "تخزين بث الفيديو…";
"Bugs and great feature ideas can be sent to the GitHub issues tracker. " = "يمكن إرسال الأخطاء وأفكار الميزات الرائعة إلى أداة تعقب مشكلات فى GitHub. ";
"Button" = "زر";
"Cancel" = "إلغاء";
"Categories to Skip" = "الفئات المطلوب تخطيها";
"Category" = "فئة";
"Chapters" = "الفصول";
"Captions" = "التسميات التوضيحية";
"Captions" = "الترجمة";
"Cellular" = "خلوي";
"Clear History" = "مسح التاريخ";
"Clear Search History" = "مسح سجل البحث";
"Clear Search History..." = "مسح سجل البحث…";
"Clear History" = "مسح سجل التاريخ";
"Clear Search History" = "مسح سجل تاريخ البحث";
"Clear Search History..." = "مسح سجل تاريخ البحث…";
"Clear All" = "مسح الكل";
"Clear All Recents" = "مسح جميع الجديد";
"Close player when closing video" = "أغلق المشغل عند غلق الفيديو";
"Clear" = "مسح الكل";
"Close PiP and open player when application enters foreground" = "أغلق الفيديو المصغر وافتح المشغل عندما يدخل التطبيق في المقدمة";
"Close PiP when player is opened" = "أغلق الفيديو المصغر عند فتح المشغل";
"Close Video" = "اغلق الفيديو";
"Close video after playing last in the queue" = "أغلق الفيديو عند إنتهاء الفيديو الاخير فى قائمة الانتظار";
"Clear All Recents" = "مسح جميع الأخيرة";
"Close player when closing video" = "غلق المشغل عند غلق الفيديو";
"Clear" = "مسح";
"Close PiP and open player when application enters foreground" = "غلق الفيديو المصغر وفتح المشغل عندما يدخل التطبيق في المقدمة";
"Close PiP when player is opened" = "غلق الفيديو المصغر عند فتح المشغل";
"Close Video" = "غلق الفيديو";
"Close video after playing last in the queue" = "غلق الفيديو عند إنتهاء الفيديو الأخير فى قائمة الإنتظار";
"Comments" = "التعليقات";
"Connection failed" = "فشل الاتصال";
"Continue" = "استمر";
"Continue" = "‏الإستمرار";
"Connected successfully (%@)" = "تم الاتصال بنجاح (%@)";
"Country" = "البلد";
"Country Name or Code" = "اسم الدولة أو الرمز";
"Copy %@ link" = "انسخ الرابط: %@";
"Copy %@ link" = "نسخ رابط %@";
"Contributing" = "المساهمة";
"Contact" = "التواصل";
"Continue from %@" = "الاستمرار من %@";
"Continue from %@" = "الإستمرار من %@";
"Controls" = "عناصر التحكم";
"Copy %@ link with time" = "نسخ رابط %@ مع الوقت";
"Could not load locations manifest" = "تعذر تحميل بيان المواقع";
@@ -71,34 +71,34 @@
/* Video sort order in search */
"Date" = "تاريخ";
"Decrease rate" = "انخفاض معدل";
"Decreased opacity" = "انخفاض التعتيم";
"Decrease rate" = "إنخفاض معدل";
"Decreased opacity" = "إنخفاض التعتيم";
"Enable logging" = "تمكين التسجيل";
"Discord Server" = "سيرفر Discord";
"Discord Server" = "خادم Discord";
"Discussions take place in Discord and Matrix. It's a good spot for general questions." = "تجري المناقشات في Discord و Matrix. إنه مكان جيد للأسئلة العامة.";
"Don't use public locations" = "لا تستخدم المواقع العامة";
"Donations" = "التبرعات";
"Done" = "تم";
"Duration" = "مدة";
"Edit" = "تعديل";
"Edit Playlist" = "عدل قائمة التشغيل";
"Edit Playlist" = "‏تعديل قائمة تشغيل";
"Edit Quality Profile" = "تعديل ملف تعريف الجودة";
"Edit..." = "تعديل...";
"Enable Return YouTube Dislike" = "تمكين إرجاع زر لا يعجبني";
"Enter fullscreen in landscape" = "املاء الشاشة عند الدخول فى الوضع الافقي";
"Error" = "خطأ";
"Error when accessing playlist" = "خطأ عند الوصول إلى قائمة التشغيل";
"Favorites" = "المفضلات";
"Filter: active" = "المنقى: فعال";
"Error when accessing playlist" = "خطأ عند الوصول إلى قائمة تشغيل";
"Favorites" = "المفضلة";
"Filter: active" = "‏عامل التصفية: فعال";
"Find Other" = "العثور على غيرها";
"Finding something to play..." = "العثور على شيء للتشغيل ...";
"For videos which feature music as the primary content." = "لمقاطع الفيديو التي تحتوي على الموسيقى كمحتوى أساسي.";
"Gaming" = "اللعب";
"Help" = "مساعدة";
"Hide sidebar" = "إخفاء الشريط الجانبي";
"Highest" = "أعلى";
"Highest quality" = "أعلى جودة";
"History" = "تاريخ";
"Highest" = "‏الأعلى";
"Highest quality" = "جودة أعلى";
"History" = "سجل التاريخ";
/* Video date filter in search */
"Hour" = "ساعة";
@@ -113,15 +113,15 @@
"Info" = "معلومات";
/* SponsorBlock category name */
"Interaction" = "تفاعل";
"Interface" = "واجهه المستخدم";
"Interaction" = "التفاعل";
"Interface" = "واجهة المستخدم";
/* SponsorBlock category name */
"Intro" = "مقدمة";
"Intro" = "المقدمة";
"Issues Tracker" = "تعقب المشاكل";
/* Selected video has just finished playing */
"Just watched" = "تمت مشاهدته للتو";
"Just watched" = "تمت المشاهدة للتو";
/* Player controls layout size */
"Large" = "كبير";
@@ -129,17 +129,17 @@
"LIVE" = "مباشر";
/* Loading stream OSD */
"Loading streams..." = "جارٍ تحميل البثوث ...";
"Loading streams..." = "تحميل بثوث...";
"Loading..." = "تحميل...";
/* Video duration filter in search */
"Long" = "طويل";
"Low" = "منخفض";
"Low quality" = "جودة منخفضة";
"Lowest" = "أدنى";
"Mark as watched" = "وضع علامة كمراقب";
"Mark video as watched after playing" = "ضع علامة على الفيديو كمشاهدة بعد التشغيل";
"Mark watched videos with" = "وضع علامة على مقاطع الفيديو التي تمت مشاهدتها باستخدام";
"Lowest" = "‏الأدنى";
"Mark as watched" = "وضع علامة تمت المشاهدة";
"Mark video as watched after playing" = "وضع علامة تمت المشاهدة على الفيديو بعد التشغيل";
"Mark watched videos with" = "وضع علامة تمت المشاهدة على مقاطع الفيديو باستخدام";
"Matrix Channel" = "قناة Matrix";
"Matrix Chat" = "دردشة Matrix";
"More info can be found in:" = "يمكن العثور على مزيد من المعلومات في:";
@@ -154,87 +154,87 @@
"No results" = "لا توجد نتائج";
"Normal" = "عادي";
"Not available" = "غير متوفر";
"Not Playing" = "لا يلعب";
"Not Playing" = "لا يعمل";
"Nothing" = "لا شئ";
/* SponsorBlock category name */
"Offtopic in Music Videos" = "خارج الموضوع في مقاطع الفيديو الموسيقية";
"Only when signed in" = "فقط عندما يكون مسجل";
"Open Settings" = "افتح الإعدادات";
"Open Settings" = "فتح الإعدادات";
/* Loading stream OSD */
"Opening %@ stream..." = "فتح %@ تيار ...";
"Opening audio stream..." = "يتم الآن فتح الصوت ...";
"Opening %@ stream..." = "فتح بث %@ ...";
"Opening audio stream..." = "فتح بث صوتي...";
"Orientation" = "اتجاه";
"Play in PiP" = "تشغيل الفيديو المصغر";
"Play Last" = "تشغيل الاخير";
"Play Music" = "شغل الموسيقى";
"Play in PiP" = "تشغيل في الفيديو المصغر";
"Play Last" = "تشغيل الأخير";
"Play Music" = "تشغل الموسيقى";
"Play Next" = "تشغيل التالى";
"Play Now" = "شغل الآن";
"Playback" = "التشغيل";
"Play Now" = "تشغيل الآن";
"Playback" = "إعادة تشغيل المقطع";
"Player" = "المشغل";
"Playlist" = "قائمة التشغيل";
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "سيتم حذف قائمة التشغيل \"%@\".\nلا يمكن التراجع عنه.";
"Playlists" = "قوائم التشغيل";
"Popular" = "شعبي";
"Playlist" = "قائمة تشغيل";
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "سيتم حذف قائمة تشغيل \"%@\".\nلا يمكن التراجع عنه.";
"Playlists" = "قوائم تشغيل";
"Popular" = "‏محتوى ذو شعبية";
"Preferred Formats" = "التنسيقات المفضلة";
"Profiles" = "ملفات تعريف";
"Profiles" = "ملفات التعريف";
"Promoting a product or service that is directly related to the creator themselves. This usually includes merchandise or promotion of monetized platforms." = "الترويج لمنتج أو خدمة مرتبطة مباشرة بمنشئ المحتوى نفسه. يتضمن هذا عادة البضائع أو الترويج للمنصات التي يتم تحقيق الدخل منها.";
"Public Locations" = "المواقع العامة";
"Public Manifest" = "البيان العام";
"Quality" = "جودة";
"Quality" = "الجودة";
"Quality Profile" = "ملف تعريف الجودة";
"Queue" = "قائمة الانتظار";
"Queue is empty" = "قائمة الانتظار فارغة";
"Queue" = "قائمة الإنتظار";
"Queue is empty" = "قائمة الإنتظار فارغة";
"Rate" = "معدل";
/* Video sort order in search */
"Rating" = "تقييم";
"Recents" = "الجدد";
"Recents" = "الأخيرة";
"Red" = "أحمر";
"Refresh" = "تحديث";
"Regular size" = "الحجم العادي";
"Regular Size" = "الحجم العادي";
"Related" = "ذات الصله";
"Related" = "ذات الصلة";
/* Video sort order in search */
"Relevance" = "الصلة";
"Remove" = "أزيل";
"Remove" = "‏إزالة";
"Remove from Favorites" = "إزالة من المفضلة";
"Remove from history" = "إزالة من سجل التاريخ";
"Remove from Playlist" = "إزالة من قائمة التشغيل";
"Remove from the queue" = "إزالة من قائمة الانتظار";
"Remove from Playlist" = "إزالة من قائمة تشغيل";
"Remove from the queue" = "إزالة من قائمة الإنتظار";
"Replies" = "الردود";
"Reset" = "إعادة ضبط";
"Reset" = "إعادة تعيين";
"Reset search filters" = "إعادة تعيين عوامل تصفية البحث";
"Reset watched status when playing again" = "إعادة تعيين حالة المشاهدة عند اللعب مرة أخرى";
"Reset watched status when playing again" = "إعادة تعيين حالة المشاهدة عند التشغيل مرة أخرى";
"Resolution" = "دقة";
"Restart" = "اعاده تشغيل";
"Restart the app to apply the settings above." = "أعد تشغيل التطبيق لتطبيق الإعدادات أعلاه.";
"Restart" = "إعادة بدء التشغيل";
"Restart the app to apply the settings above." = "إعادة بدء تشغيل التطبيق لتطبيق الإعدادات أعلاه.";
"Restore default profiles..." = "استعادة ملفات التعريف الافتراضية ...";
"Round corners" = "زوايا مستديرة";
"Save" = "يحفظ";
"Save history of searches, channels and playlists" = "حفظ سجل عمليات البحث والقنوات وقوائم التشغيل";
"Search history is empty" = "سجل البحث فارغ";
"Save" = "حفظ";
"Save history of searches, channels and playlists" = "حفظ سجل تاريخ عمليات البحث والقنوات وقوائم تشغيل";
"Search history is empty" = "سجل تاريخ البحث فارغ";
"Search..." = "بحث...";
"Sections" = "الأقسام";
"Select location closest to you:" = "حدد أقرب موقع إليك:";
/* SponsorBlock category name */
"Self-promotion" = "الترويج الذاتي";
"Settings" = "اعدادات";
"Share %@ link" = "مشاركة الرابط %@";
"Share %@ link with time" = "مشاركة الرابط %@ مع الوقت";
"Share..." = "شارك...";
"Settings" = "الإعدادات";
"Share %@ link" = "مشاركة رابط %@";
"Share %@ link with time" = "مشاركة رابط %@ مع الوقت";
"Share..." = "‏مشاركة...";
/* Video duration filter in search */
"Short" = "قصير";
"Show account username" = "إظهار اسم مستخدم الحساب";
"Show anonymous accounts" = "إظهار الحسابات المجهولة";
"Show channel name" = "إظهار اسم القناة";
"Show history" = "عرض التاريخ";
"Show history" = "‏إظهار سجل التاريخ";
"Show keywords" = "إظهار الكلمات الرئيسية";
"Show playback statistics" = "عرض إحصائيات التشغيل";
"Show playback statistics" = "إظهار إحصائيات التشغيل";
"Show sidebar when space permits" = "إظهار الشريط الجانبي عندما تسمح المساحة";
"Shuffle" = "خلط";
"Shuffle All" = "خلط الجميع";
@@ -243,95 +243,95 @@
/* Player controls layout size */
"Small" = "صغير";
"Sort" = "صنف";
"Sort" = "فرز";
"Sort: %@" = "فرز: %@";
"Source" = "المصدر";
/* SponsorBlock category name */
"Sponsor" = "راعي";
"SponsorBlock" = "مغلق الاعلانات";
"SponsorBlock API Instance" = "مثيل واجهة برمجة تطبيقات مغلق الاعلانات";
"Sponsor" = "الراعي";
"SponsorBlock" = "حظر الإعلانات";
"SponsorBlock API Instance" = "مثيل واجهة برمجة تطبيقات حظر الإعلانات";
"Subscribe" = "الإشتراك";
/* Subscriptions title */
"Subscriptions" = "الاشتراكات";
"Subscriptions" = "الإشتراكات";
"Switch to other public location" = "التبديل إلى موقع عام آخر";
"System controls buttons" = "أزرار التحكم في النظام";
"System controls buttons" = "أزرار نظام عناصر التحكم";
"This cannot be reverted" = "هذا لا يمكن تغييره";
"This cannot be reverted. You might need to switch between views or restart the app to see changes." = "لا يمكن التراجع عن هذا. قد تحتاج إلى التبديل بين طرق العرض أو إعادة تشغيل التطبيق للاطلاع على التغييرات.";
"This cannot be reverted. You might need to switch between views or restart the app to see changes." = "لا يمكن التراجع عن هذا. قد تحتاج إلى التبديل بين طرق العرض أو إعادة بدء تشغيل التطبيق للاطلاع على التغييرات.";
"This information will be processed only on your device and used to connect you to the server in the specified country." = "ستتم معالجة هذه المعلومات فقط على جهازك واستخدامها لتوصيلك بالخادم في البلد المحدد.";
"Upload date" = "تاريخ الرفع";
"URL" = "الرابط";
"Used to create links from videos, channels and playlists" = "يستخدم لإنشاء روابط من مقاطع الفيديو والقنوات وقوائم التشغيل";
"Used to create links from videos, channels and playlists" = "يستخدم لإنشاء روابط من مقاطع الفيديو والقنوات وقوائم تشغيل";
"Username" = "اسم المستخدم";
/* Player controls layout size */
"Very Large" = "كبير جدا";
"Videos" = "الفيديوات";
"Videos" = "مقاطع الفيديو";
/* Video sort order in search */
"Views" = "المشاهدات";
"Watched" = "تمت مشاهدته";
"Watched" = "تمت المشاهدة";
/* Selected video was played on given date */
"Watched %@" = "شاهدت %@";
"Watched %@" = "تمت المشاهدة %@";
/* Selected video is being played */
"Watching now" = "يشاهد الآن";
"Watching now" = "المشاهدة الآن";
/* Video date filter in search */
"Week" = "أسبوع";
"Welcome" = "مرحبا";
"When partially watched video is played" = "عند تشغيل الفيديو الذي تمت مشاهدته جزئيا";
"When partially watched video is played" = "عند تشغيل الفيديو الذي تمت المشاهدة جزئيا";
"Wi-Fi" = "واي فاي";
"Yattee" = "Yattee";
"Yattee %@ (build %@)" = "Yattee%@ (بناء%@)";
"Yattee %@ (build %@)" = "Yattee %@ (بناء %@)";
/* Video date filter in search */
"Year" = "سنة";
"You can find information about using Yattee in the Wiki pages." = "يمكنك العثور على معلومات حول استخدام Yattee في صفحات الWiki.";
"You can use automatic profile selection based on current device status or switch it in video playback settings controls." = "يمكنك استخدام التحديد التلقائي لملف التعريف استنادا إلى حالة الجهاز الحالية أو تبديله في عناصر التحكم في إعدادات تشغيل الفيديو.";
"You have no Playlists" = "ليس لديك قوائم تشغيل";
"You have no playlists\n\nTap on \"New Playlist\" to create one" = "ليس لديك قوائم تشغيل\n\nاضغط على \"قائمة تشغيل جديدة\" لإنشاء واحدة";
"You have no playlists\n\nTap on \"New Playlist\" to create one" = "ليس لديك قوائم تشغيل\n\nالضغط على \"قائمة تشغيل جديدة\" لإنشاء واحدة";
"You need to create an instance and accounts\nto access %@ section" = "تحتاج إلى إنشاء مثيل وحسابات\nللوصول إلى قسم %@";
"You need to select an account\nto access %@ section" = "تحتاج إلى تحديد حساب\nللوصول إلى قسم %@";
"Unlisted" = "غير علنى";
"Unlisted" = "غير مدرج";
"Now Playing" = "يعرض الآن";
"Playback queue is empty" = "قائمة انتظار التشغيل فارغة";
"Playing Next" = "تشغيل الآتي";
"Add Channels, Playlists and Searches to Favorites using" = "إضافة القنوات وقوائم التشغيل وعمليات البحث إلى المفضلات باستخدام";
"Playback queue is empty" = "قائمة الإنتظار التشغيل فارغة";
"Playing Next" = "تشغيل التالي";
"Add Channels, Playlists and Searches to Favorites using" = "إضافة القنوات، قوائم تشغيل، عمليات البحث إلى المفضلة باستخدام";
"Make default" = "جعله افتراضي";
"Visibility" = "امكانية الرؤية";
"Stream & Player" = "تيار ومشغل";
"Stream & Player" = "بث و تشغيل";
"Statistics" = "إحصائيات";
"Hardware decoder" = "وحدة فك ترميز الأجهزة";
"Rate & Captions" = "التقييم والتسميات التوضيحية";
"Rate & Captions" = "معدل سرعة التشغيل و الترجمة";
"Dropped frames" = "الإطارات المتساقطة";
"Stream FPS" = "عدد الإطارات فى الثانية فى التيار";
"Stream FPS" = "عدد الإطارات في الثانية في البث";
"Any format" = "أي شكل";
"%@ formats" = "%@ تنسيقات";
"Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "قائمة التشغيل فارغة\n\nاضغط مع الاستمرار على مقطع فيديو ثم\n\"إضافة إلى قائمة التشغيل\"";
"Press and hold remote button to open captions and quality menus" = "اضغط مع الاستمرار على زر التحكم عن بعد لفتح التسميات التوضيحية وقوائم الجودة";
"%@ formats" = "تنسيقات %@";
"Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "قائمة تشغيل فارغة\n\nالضغط مع الإستمرار على مقطع الفيديو ثم\n\"إضافة إلى قائمة تشغيل\"";
"Press and hold remote button to open captions and quality menus" = "الضغط مع الإستمرار على زر التحكم عن بعد لفتح الترجمة وقوائم الجودة";
"Comments are disabled" = "التعليقات معطلة";
"No comments" = "لا توجد تعليقات";
"No chapters information available" = "لا توجد معلومات متاحة عن الفصول";
"Open logs in Finder" = "فتح السجلات في Finder";
"Could not refresh Subscriptions" = "تعذر تحديث الاشتراكات";
"Could not load streams" = "تعذر تحميل التدفقات";
"Could not refresh Subscriptions" = "تعذر تحديث الإشتراكات";
"Could not load streams" = "تعذر تحميل البثوث";
"Could not extract channel information" = "تعذر استخراج معلومات القناة";
"Could not extract SID from received cookies: %@" = "تعذر استخراج SID من ملفات تعريف الارتباط المستلمة: %@";
"Could not update your token." = "تعذر تحديث الرمز المميز الخاص بك.";
"Could not refresh Trending" = "تعذر تحديث \"المحتويات الشائعة\"";
"Could not refresh Trending" = "تعذر تحديث المحتوى الرائج";
"For custom locations you can configure Frontend URL in Locations settings" = "بالنسبة للمواقع المخصصة، يمكنك تكوين عنوان URL للواجهة الأمامية في إعدادات المواقع";
"This URL could not be opened" = "تعذر فتح عنوان URL هذا";
"Could not open channel" = "تعذر فتح القناة";
"Could not open playlist" = "تعذر فتح قائمة التشغيل";
"Could not open playlist" = "تعذر فتح قائمة تشغيل";
"Could not extract video ID" = "تعذر استخراج معرف الفيديو";
"This video could not be opened" = "تعذر فتح هذا الفيديو";
"Could not extract playlist ID" = "تعذر استخراج معرف قائمة التشغيل";
"Could not extract playlist ID" = "تعذر استخراج معرف قائمة تشغيل";
"Could not load video" = "تعذر تحميل الفيديو";
"No locations available at the moment" = "لا توجد مواقع متاحة في الوقت الحالي";
"Could not refresh Playlists" = "تعذر تحديث قوائم التشغيل";
"Could not refresh Playlists" = "تعذر تحديث قوائم تشغيل";
"If you want this app to be available in your language, join translation project." = "إذا كنت تريد أن يكون هذا التطبيق متاحا بلغتك ، فانضم إلى مشروع الترجمة.";
"Translations" = "الترجمات";
"No documents" = "لا توجد مستندات";
@@ -339,56 +339,56 @@
"Share files from Finder on a Mac\nor iTunes on Windows" = "مشاركة الملفات من Finder على جهاز Mac\nأو iTunes على Windows";
"Home" = "الصفحة الرئيسية";
"Show Open Videos quick actions" = "إظهار الإجراءات السريعة لمقاطع الفيديو المفتوحة";
"Recent History" = "التاريخ الحديث";
"Show Favorites" = "إظهار المفضلات";
"Recent History" = "سجل تاريخ مؤخرًا";
"Show Favorites" = "إظهار المفضلة";
"Inspector visibility" = "امكانية رؤية المفتش";
"Edit Favorites…" = "تعديل المفضلات…";
"Edit Favorites…" = "تعديل المفضلة…";
"Buttons labels" = "تسميات الأزرار";
"Show Documents" = "عرض الوثائق";
"Show Documents" = "إظهار الوثائق";
"Pages toolbar position" = "موضع شريط أدوات الصفحات";
"Show Inspector" = "عرض المفتش";
"Show Inspector" = "إظهار المفتش";
"Reload manifest" = "إعادة تحميل البيان";
"Open" = "فتح";
"Video actions buttons" = "أزرار إجراءات الفيديو";
"Video actions buttons" = "أزرار إجراءات مقطع الفيديو";
"Open Files" = "فتح الملفات";
"Channels" = "القنوات";
"Channel" = "قناة";
"Shorts" = "فيديوهات قصيرة";
"Verified" = "مصدق عليها";
"Live Streams" = "البثوث المباشرة";
"Channel" = "القناة";
"Shorts" = "‏مقاطع الفيديو القصيرة";
"Verified" = "تم التحقق منه";
"Live Streams" = "بثوث مباشرة";
"Are you sure you want to remove %@ location?" = "هل أنت متأكد أنك تريد إزالة موقع @%؟";
"Could not delete document" = "تعذر حذف المستند";
"\"%@\" will be irreversibly removed from this device." = "ستتم إزالة \"%@\" بشكل لا رجعة فيه من هذا الجهاز.";
"Are you sure you want to remove this document?" = "هل أنت متأكد من أنك تريد إزالة هذا المستند؟";
"Open Video" = "افتح الفيديو";
"Address" = "عنوان";
"Remove…" = "أزيل…";
"Show sidebar" = "عرض الشريط الجانبي";
"Open Video" = "فتح الفيديو";
"Address" = "‏العنوان";
"Remove…" = "‏إزالة…";
"Show sidebar" = "إظهار الشريط الجانبي";
"Remove Location" = "إزالة الموقع";
" subscribers" = " المتابعين";
" subscribers" = " المشتركين";
"Accounts" = "الحسابات";
"10 seconds forwards/backwards" = "عشرة ثوان إلى الأمام/الخلف";
"10 seconds forwards/backwards" = "10 ثوان إلى الأمام/للخلف";
/* Video date filter in search
Video duration filter in search */
"Any" = "أي";
"Are you sure you want to clear search history?" = "هل أنت متأكد من أنك تريد مسح سجل البحث؟";
"Add to Favorites" = "أضف إلى المفضلات";
"Are you sure you want to clear search history?" = "هل أنت متأكد من أنك تريد مسح سجل تاريخ البحث؟";
"Add to Favorites" = "إضافة إلى المفضلة";
"Anonymous" = "مجهول";
"Are you sure you want to delete playlist?" = "هل أنت متأكد من أنك تريد حذف قائمة التشغيل؟";
"Are you sure you want to clear history of watched videos?" = "هل أنت متأكد من أنك تريد مسح سجل مقاطع الفيديو التي تمت مشاهدتها؟";
"Clear the queue" = "امسح قائمة الانتظار";
"Are you sure you want to delete playlist?" = "هل أنت متأكد من أنك تريد حذف قائمة تشغيل؟";
"Are you sure you want to clear history of watched videos?" = "هل أنت متأكد من أنك تريد مسح سجل تاريخ مقاطع الفيديو التي تمت المشاهدة؟";
"Clear the queue" = "مسح قائمة الإنتظار";
"Charging" = "جاري الشحن";
"Close" = "أغلق";
"Close player when starting PiP" = "أغلق المشغل عند فتح الفيديو المصغر";
"High" = "عالية";
"Close" = "غلق";
"Close player when starting PiP" = "غلق المشغل عند بدء الفيديو المصغر";
"High" = "عالي";
"Badge color" = "لون الشارة";
"Accounts are not supported for the application of this instance" = "الحسابات غير مدعومة للتطبيق الخاص بهذه الحالة";
"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)." = "تذكيرات صريحة لإبداء الإعجاب بها أو الاشتراك فيها أو التفاعل معها على أي منصة (منصات) مدفوعة أو مجانية (مثل النقر فوق مقطع فيديو).";
"Filter" = "تنقية";
"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)." = "تذكيرات صريحة لإبداء الإعجاب بها أو الإشتراك فيها أو التفاعل معها على أي منصة (منصات) مدفوعة أو مجانية (مثل النقر فوق مقطع الفيديو).";
"Filter" = " عامل التصفية";
"Frontend URL" = "عنوان URL للواجهة الأمامية";
"Fullscreen size" = "حجم ملء الشاشة";
"Locations" = "مواقع";
@@ -401,71 +401,71 @@
/* Video date filter in search */
"Month" = "شهر";
"Open \"Playlists\" tab to create new one" = "افتح علامة التبويب \"قوائم التشغيل\" لإنشاء واحدة جديدة";
"Pause when player is closed" = "إيقاف مؤقت عند إغلاق اللاعب";
"Open \"Playlists\" tab to create new one" = "فتح علامة التبويب \"قوائم تشغيل\" لإنشاء واحدة جديدة";
"Pause when player is closed" = "إيقاف مؤقت عند إغلاق المشغل";
"Play" = "تشغيل";
"Pause when entering background" = "توقف مؤقتا عند إدخال الخلفية";
"Pause" = "وقف مؤقت";
"Pause when entering background" = "إيقاف مؤقت عند دخول الخلفية";
"Pause" = "‏إيقاف مؤقت";
/* SponsorBlock category name */
"Outro" = "الخاتمة";
"Part of a video promoting a product or service not directly related to the creator. The creator will receive payment or compensation in the form of money or free products." = "جزء من فيديو يروج لمنتج أو خدمة لا يرتبط مباشرة بمنشئ المحتوى سيحصل المنشئ على مدفوعات أو تعويض في شكل أموال أو منتجات مجانية.";
"Part of a video promoting a product or service not directly related to the creator. The creator will receive payment or compensation in the form of money or free products." = "جزء من الفيديو يروج لمنتج أو خدمة لا يرتبط مباشرة بمنشئ المحتوى سيحصل المنشئ على مدفوعات أو تعويض في شكل أموال أو منتجات مجانية.";
"Password" = "كلمة المرور";
"Picture in Picture" = "الفيديو المصغر";
"Play All" = "تشغيل الكل";
"Formats will be selected in order as listed.\nHLS is an adaptive format (resolution setting does not apply)." = "سيتم تحديد التنسيقات بالترتيب كما هو مدرج.\nHLS هو تنسيق قابل للتكيف (لا ينطبق إعداد الدقة).";
"Restart/Play next" = "أعد تشغيل / شغل بعد ذلك";
"Restart/Play next" = "إعادة بدء التشغيل / تشغيل التالي";
"Rotate to portrait when exiting fullscreen" = "التدوير إلى عمودي عند الخروج من وضع ملء الشاشة";
"Save history of played videos" = "حفظ تاريخ مقاطع الفيديو المشغلة";
"Save history of played videos" = "حفظ سجل تاريخ مقاطع الفيديو المشغلة";
"Search" = "بحث";
"Segments typically found at the start of a video that include an animation, still frame or clip which are also seen in other videos by the same creator." = "المقاطع التي يتم العثور عليها عادة في بداية الفيديو والتي تتضمن رسما متحركا أو إطارا ثابتا أو مقطعا يظهر أيضا في مقاطع فيديو أخرى من قبل منشئ المحتوى نفسه.";
"Segments typically found at the start of a video that include an animation, still frame or clip which are also seen in other videos by the same creator." = "المقاطع التي يتم العثور عليها عادة في بداية الفيديو والتي تتضمن رسما متحركا أو إطارا ثابتا أو مقطعا يظهر أيضا في مقاطع الفيديو الأخرى من قبل منشئ المحتوى نفسه.";
"Show progress of watching on thumbnails" = "إظهار تقدم المشاهدة على الصور المصغرة";
/* Player controls layout size */
"Smaller" = "الأصغر";
"Switch to public locations" = "التبديل إلى المواقع العامة";
"Show video length" = "عرض طول الفيديو";
"Show video length" = "إظهار طول الفيديو";
"Private" = "خاص";
"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." = "من الجيد سماع ذلك. من الممتع تقديم التطبيقات التي يريد الآخرون استخدامها. يمكنك التفكير في التبرع للمشروع أو المساعدة من خلال المساهمة في تطوير ميزات جديدة.";
/* Player controls layout size for TV */
"TV" = "تلفاز";
"unknown" = "مجهول";
"unknown" = "غير معروف";
"This will remove all your custom profiles and return their default values. This cannot be reverted." = "سيؤدي هذا إلى إزالة جميع ملفات التعريف الخاصة بك وإرجاع قيمها الافتراضية. لا يمكن التراجع عن هذا.";
/* Video date filter in search */
"Today" = "اليوم";
"Trending" = "الشائع";
"Typically near or at the end of the video when the credits pop up and/or endcards are shown." = "عادةً ما تكون بالقرب من الفيديو أو في نهايته عند ظهور قائمة الأسماء و / أو ظهور بطاقات النهاية.";
"Unsubscribe" = "إلغاء الاشتراك";
"Trending" = "المحتوى الرائج";
"Typically near or at the end of the video when the credits pop up and/or endcards are shown." = "عادةً ما تكون بالقرب من نهاية الفيديو عند ظهور قائمة الأسماء و / أو ظهور بطاقات النهاية.";
"Unsubscribe" = "إلغاء الإشتراك";
"Public" = "علني";
"Public" = "عام";
"Share Logs..." = "مشاركة السجلات …";
"Current Location" = "الموقع الحالي";
"You can switch between profiles in playback settings controls." = "يمكنك التبديل بين ملفات التعريف في عناصر التحكم في إعدادات التشغيل.";
"Current Playlist" = "قائمة التشغيل الحالية";
"Keep last played video in the queue after restart" = "احتفظ بآخر فيديو تم تشغيله في قائمة الانتظار بعد إعادة التشغيل";
"Current Playlist" = "قائمة تشغيل الحالية";
"Keep last played video in the queue after restart" = "الإحتفاظ بآخر فيديو تم تشغيله في قائمة الإنتظار بعد إعادة بدء التشغيل";
"Cached time" = "الوقت المخزن مؤقتا";
"It can be changed later in settings. You can use your own locations too." = "يمكن تغييره لاحقا في الإعدادات. يمكنك استخدام المواقع الخاصة بك أيضا.";
"Could not open video" = "تعذر فتح الفيديو";
"Channel could not be found" = "تعذر العثور على القناة";
"Could not refresh Popular" = "تعذر تحديث المحتويات الشعبية";
"Could not refresh Popular" = "تعذر تحديث المحتوى ذو الشعبية";
"Files" = "الملفات";
"Could not create share link" = "تعذر إنشاء رابط مشاركة";
"Show Home" = "عرض الصفحة الرئيسية";
"Video Details" = "تفاصيل الفيديو";
"Clear Queue before opening" = "مسح قائمة الانتظار قبل الفتح";
"Show Home" = "إظهار الصفحة الرئيسية";
"Video Details" = "تفاصيل مقطع الفيديو";
"Clear Queue before opening" = "مسح قائمة الإنتظار قبل الفتح";
"Current: %@\n%@" = "الحالي: %@\n%@";
"Thumbnails" = "المعاينات";
"Show Open Videos toolbar button" = "إظهار زر الفيديوهات المفتوحة في شريط الأدوات";
"Thumbnails" = "‏الصور المصغرة";
"Show Open Videos toolbar button" = "أظهار زر شريط الأدوات لمقاطع الفيديو المفتوحة";
"URL to Open" = "الرابط المراد فتحه";
"Enter link to open" = "أدخل الرابط للفتح";
"Enter link to open" = "‏إدخال الرابط للفتح";
"Pages buttons" = "أزرار الصفحات";
"Could not open Files" = "لا يمكن فتح الملفات";
"Could not open Files" = "تعذر فتح الملفات";
"Paste" = "لصق";
"Open Videos" = "فتح الفيديوهات";
"Enter links to open, one per line" = "أدخل الروابط للفتح, كل رابط في سطر";
"Open Videos" = "فتح مقاطع الفيديو";
"Enter links to open, one per line" = "‏إدخال الروابط للفتح, كل رابط في سطر";
"Playback Mode" = "وضع التشغيل";
"Add" = "إضافة";
"Hide" = "إخفاء";
@@ -479,27 +479,128 @@
"Driver" = "التعريف";
"Show only icons" = "إظهار الأيقونات فقط";
"Center" = "منتصف";
"Documents" = "مستندات";
"File" = "ملف";
"Documents" = "المستندات";
"File" = "‏الملف";
"Codec" = "الترميز";
"Size" = "حجم";
"FPS" = "شريحة كل ثانية";
"Could not find any links to open in your clipboard" = "لا يمكن العثور على روابط للفتح في الحافظة";
"Size" = "الحجم";
"FPS" = "‏إطار لكل ثانية";
"Could not find any links to open in your clipboard" = "تعذر العثور على روابط للفتح في الحافظة";
"Actions buttons" = "أزرار الإجراءات";
"Locations Manifest" = "بيان المواقع";
"Default Profile" = "الملف الشخصي الإفتراضي";
"Playback history is empty" = "سجل التشغيل فارغ";
"Copy%@link" = "نسخ الرابط %@";
"Share%@link" = "مشاركة الرابط %@";
"Playback history is empty" = "سجل تاريخ التشغيل فارغ";
"Copy%@link" = "نسخ رابط %@";
"Share%@link" = "مشاركة رابط %@";
"Instance of current account" = "مثيل الحساب الحالي";
"Seek gesture sensitivity" = "إطار حساسية الإيماءات";
"Video" = "";
"Audio" = "";
"Honor orientation lock" = "";
"Proxy videos" = "توكيل الفيديوهات";
"Seek gesture speed" = "سرعة بحث الإيماءة";
"Seek with horizontal swipe on video" = "للبحث بالتمرير الأفقي على الفيديو";
"System controls show buttons for %@" = "تعرض ضوابط النظام أزرارًا لـ %@";
"Seek gesture sensitivity" = "حساسية إيماءة التقديم";
"Video" = "مقطع الفيديو";
"Audio" = "الصوت";
"Honor orientation lock" = "قفل توجيه الشرف";
"Proxy videos" = "وكيل مقاطع الفيديو";
"Seek gesture speed" = "سرعة إيماءة التقديم";
"Seek with horizontal swipe on video" = "التقديم باستخدام التمرير الأفقي على الفيديو";
"System controls show buttons for %@" = "‏نظام عناصر التحكم إظهار الأزرار لـ %@";
"Wiki" = "ويكي";
"Sample Rate" = "";
"Sample Rate" = "معدل العينة";
"Short videos: visible" = "مقاطع الفيديو القصيرة: مرئية";
"Show channel avatars in channels lists" = "‏إظهار الصور الرمزية للقناة في قوائم القنوات";
"Show channel avatars in videos lists" = "إظهار الصور الرمزية للقناة في قوائم مقاطع الفيديو";
"Keep channels with unwatched videos on top of subscriptions list" = " الإحتفاظ بالقنوات التي تحتوي على مقاطع الفيديو غير المشاهدة على رأس قائمة الإشتراكات";
"Show video context menu options to force selected backend" = "إظهار خيارات قائمة سياق الفيديو لفرض الواجهة الخلفية المحددة";
"Play Now in AVPlayer" = "تشغيل الآن في AVPlayer";
"Play Now in MPV" = "تشغيل الآن في MPV";
"Enter account credentials to connect..." = "أدخل بيانات اعتماد الحساب للاتصال...";
"Seek" = "التقديم";
"Show scroll to top button in comments" = "إظهار التمرير إلى الزر العلوي في التعليقات";
"Enter location address to connect..." = "أدخل عنوان الموقع للإتصال...";
"File Extension" = "ملحق الملف";
"Public account" = "حساب عام";
"Your Accounts" = "حساباتك";
"Browse without account" = "تصفح بدون حساب";
"Mark channel feed as unwatched" = "وضع علامة لم تتم المشاهدة على محتوى القناة";
"Play all unwatched" = "‏تشغيل كل شيء لم تتم المشاهدة";
"Player Bar" = "‏شريط المشغل";
"Double tap gesture" = "إيماءة بنقرة المزدوجة";
"Open expanded" = "فتح موسعاً";
"Always show controls buttons" = "‏دائما إظهار أزرار عناصر التحكم";
"Clear all" = "‏مسح الكل";
"Maximum width expanded" = "الحد الأقصى للعرض الموسع";
"Single tap gesture" = "إيماءة بنقرة الواحدة";
"Tap and hold channel thumbnail to open context menu with more actions" = " الضغط مع الإستمرار على الصورة المصغرة للقناة لفتح قائمة السياق مع المزيد من الإجراءات";
"Right click channel thumbnail to open context menu with more actions" = "نقر زر الماوس الأيمن فوق الصورة المصغرة للقناة لفتح قائمة السياق مع المزيد من الإجراءات";
"Seeking" = "التقديم";
"Gesture: fowards" = "إيماءة: إلى الأمام";
"Show unwatched feed badges" = "إظهار شارات المحتوى التي لم تتم المشاهدة";
"Controls button: forwards" = "زر عناصر التحكم: إلى الأمام";
"Gesture: backwards" = "إيماءة: للخلف";
"Hide player" = "إخفاء المشغل";
"Gesture settings control skipping interval for double tap gesture on left/right side of the player. Changing system controls settings requires restart." = "التحكم في إعدادات إيماءة الفاصل الزمني للتخطي لإيماءة النقر المزدوج على الجانب الأيسر / الأيمن من المشغل. تغيير إعدادات نظام عناصر التحكم يتطلب إعادة بدء التشغيل.";
"Controls Buttons" = "أزرار عناصر التحكم";
"Play next item" = "‏تشغيل العنصر التالي";
"Lock orientation" = "‏إقفال التدوير";
"Music Mode" = "وضع الموسيقى";
"Close video" = "غلق الفيديو";
"Total size: %@" = "الحجم الإجمالي: %@";
"Actions Buttons" = "أزرار الإجراءات";
"Subscribe/Unsubscribe" = "الإشتراك/إلغاء الإشتراك";
"Show cache status" = "إظهار حالة ذاكرة التخزين المؤقت";
"Cache" = "ذاكرة التخزين المؤقت";
"Maximum feed items" = "الحد الأقصى لعناصر المحتوى";
"Open channels with description expanded" = "فتح القنوات مع الوصف موسعاً";
"Close video and player on end" = "غلق الفيديو والمشغل عند النهاية";
"Use system controls with AVPlayer" = "استخدم نظام عناصر التحكم مع AVPlayer";
"Rotate when entering fullscreen on landscape video" = "قم بالتدوير عند إدخال ملء الشاشة على الفيديو الأفقي";
"Landscape left" = "‏يسار الأفقي";
"Landscape right" = "‏يمين الأفقي";
"No rotation" = "‏لا تدوير";
"System controls" = "نظام عناصر التحكم";
"Controls button: backwards" = "زر عناصر التحكم: للخلف";
"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..." = "فتح الملف...";
"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." = "التحكم في إعدادات إيماءة فترة التخطي للنقر المزدوج على الجانب الأيسر/الأيمن من المشغل. تغيير إعدادات نظام عناصر التحكم يتطلب إعادة بدء التشغيل.";
"Available" = "متوفر";
"Startup section" = "‏قسم بدء التشغيل";
"Home Settings" = "‏إعدادات الصفحة الرئيسية";
"Watched: hidden" = "تمت المشاهدة: مخفي";
"Watched: visible" = "تمت المشاهدة: مرئي";
"No videos to show" = "لا توجد مقاطع فيديو لعرضها";
"(watched and shorts hidden)" = "(تمت المشاهدة ومقاطع قصيرة مخفية)";
"(shorts hidden)" = "(مقاطع قصيرة مخفية)";
"Disable filters" = "تعطيل عوامل التصفية";
"(watched hidden)" = "(تمت المشاهدة مخفية)";
"Limit" = "حد";
"Are you sure you want to remove %@ from Favorites?" = "هل أنت متأكد من أنك تريد إزالة %@ من المفضلة؟";
"List" = "قائمة";
"Cells" = "خلايا";
"Toggle size" = "‏حجم التبديل";
"Toggle player" = "تبديل المشغل";
"Show Next in Queue" = "إظهار التالي في قائمة الإنتظار";
"Show toggle watch status button" = "إظهار تبديل زر حالة الساعة";
"Next in Queue" = "التالي في قائمة الإنتظار";
"Do nothing" = "لا تفعل شيئا";
"Open channel" = "فتح القناة";
"Inspector" = "مفتش";
"Open video description expanded" = "فتح وصف الفيديو موسعاً";
"Feed" = "محتوى الإشتراكات";
"Mark all as unwatched" = "وضع علامة لم تتم المشاهدة على الجميع";
"Mark all as watched" = "وضع علامة تمت المشاهدة على الجميع";
"Queue - shuffled" = "قائمة الإنتظار - تم خلطها";
"Replay" = "إعادة المشغل";
"Lock" = "قفل";
"Fullscreen" = "ملء الشاشة";
"Loop one" = "‏حلقة واحدة";
"Playback Settings" = "إعدادات التشغيل";
"Description" = "وصف";
"Autoplay next" = "التشغيل التلقائي التالي";
"Stream" = "البث";
"Chapters (if available)" = "الفصول (إن وجدت)";
"Open vertical chapters expanded" = "فتح الفصول الرأسية موسعاً";
"No preview" = "لا توجد معاينة";
"Description preview" = "معاينة الوصف";
"Podcasts" = "‏بودكاست";
"Releases" = "الإصدارات";
"Add %@" = "إضافة %@";

View File

@@ -0,0 +1,4 @@
" subscribers" = " المشتركين";
"%@ Channel" = "القناة";

View File

@@ -598,3 +598,7 @@
"Podcasts" = "Podcasts";
"Releases" = "Releases";
"Add %@" = "Add %@";
"Description preview" = "Description preview";
"No preview" = "No preview";
"Open vertical chapters expanded" = "Open vertical chapters expanded";
"Chapters (if available)" = "Chapters (if available)";

View File

@@ -600,3 +600,7 @@
"Add %@" = "Añada %@";
"Show video context menu options to force selected backend" = "Mostrar las opciones del menú contextual del vídeo para forzar el backend seleccionado";
"Play Now in AVPlayer" = "Reproducir ahora en AVPlayer";
"Description preview" = "Vista previa de la descripción";
"No preview" = "Sin vista previa";
"Open vertical chapters expanded" = "Abrir capítulos verticales ampliados";
"Chapters (if available)" = "Capítulos (si están disponibles)";

View File

@@ -0,0 +1,485 @@
"Podcasts" = "پادکست‌ها";
"Reset watched status when playing again" = "بازنشانی وضعیت تماشای ویدیو هنگام تماشای مجدد";
"Finding something to play..." = "یافتن چیزی برای پخش…";
"Available" = "در دسترس";
/* Player controls layout size */
"Very Large" = "خیلی بزرگ";
"Audio" = "صدا";
"Custom Locations" = "سرویس دهندگان سفارشی";
"Always use AVPlayer for live videos" = "همیشه از AVPlayer برای ویدیوهای پخش زنده استفاده کن";
"Clear Search History..." = "پاک کردن سابقهٔ جست و جو…";
"Related" = "مرتبط";
"Close Video" = "بستن پخش کننده";
"Add to %@" = "افزودن به %@";
"Could not open playlist" = "فهرست پخش باز نشد";
"Disabled" = "غیرفعال";
"Share..." = "اشتراک گذاری…";
"For videos which feature music as the primary content." = "برای ویدیوهایی که محتوای اصلیشان موسیقی است.";
"Save history of searches, channels and playlists" = "ذخیرهٔ سابقهٔ جست و جو، کانال‌ها و فهرست‌های پخش";
"Clear Queue before opening" = "صف را پیش از باز کردن خالی کن";
"Show playback statistics" = "نمایش آمار پخش";
"Could not extract channel information" = "اطلاعات کانال دریافت نشد";
"Video" = "ویدیو";
"Matrix Chat" = "چت Matrix";
"Loop one" = "پخش دوبارهٔ تکی";
"Settings" = "تنظیمات";
"10 seconds forwards/backwards" = "۱۰ ثانیه جلو/عقب";
"Show progress of watching on thumbnails" = "پیشرفت پخش را روی تصویر بندانگشتی نشان بده";
"Recents" = "تازه‌ترین‌ها";
"Could not open channel" = "کانال باز نمی‌شود";
"Select location closest to you:" = "نزدیکترین سرویس دهنده را انتخاب کنید:";
"Fullscreen size" = "ابعاد تمام صفحه";
"No Playlists" = "بدون فهرست پخش";
"Highest quality" = "بالاترین کیفیت";
"Edit..." = "ویرایش…";
"Based on system color scheme" = "براساس طرح رنگ سیستم";
"Quality" = "کیفیت";
"Pause" = "مکث";
"Controls Buttons" = "دکمه‌های کنترل";
/* Subscriptions title */
"Subscriptions" = "اشتراک‌ها";
" subscribers" = " مشترکان";
"Channels" = "کانال‌ها";
"Resolution" = "وضوح تصویر";
"Button" = "دکمه";
"Badge & Decreased opacity" = "نشان و کاهش شفافیت";
"Milestones" = "نقطهٔ عطف";
"Are you sure you want to restore default quality profiles?" = "آیا از بازنشانی نمایه‌های کیفیت پیش‌فرض مطمئنید؟";
"Show sidebar" = "نمایش نوار کناری";
"Error" = "خطا";
/* Video date filter in search */
"Week" = "هفته";
"Make default" = "پیش فرض شو";
"Verified" = "تاییدشده";
"Subscribe/Unsubscribe" = "اشتراک/لغو اشتراک";
"Close player when closing video" = "هنگامی که ویدیو بسته می‌شود پخش کننده را ببند";
"It can be changed later in settings. You can use your own locations too." = "می‌توانید بعدا در تنظیمات تغییر دهید. همچنان می‌توانید از سرویس دهندهٔ خودتان استفاده کنید.";
"Comments are disabled" = "نظرها غیرفعال است";
"Queue is empty" = "صف خالیست";
"Playback Mode" = "وضعیت پخش";
"Country" = "کشور";
"Share" = "اشتراک‌گذاری";
"Play in PiP" = "در تصویر-در-تصویر پخش کن";
"System controls buttons" = "دکمه‌های کنترل سیستم";
"Picture in Picture" = "تصویر-در-تصویر";
"Comments" = "نظرها";
"\"%@\" will be irreversibly removed from this device." = "\"%@\" از دستگاه حذف شده و غیرقابل بازگردانی خواهد بود.";
"Now Playing" = "در حال پخش";
"Charging" = "در حال شارژ";
"Refresh" = "تازه‌سازی";
/* Player controls layout size */
"Large" = "بزرگ";
"Are you sure you want to unsubscribe from %@?" = "آیا از لفو اشتراک %@ مطمئنید؟";
/* Video duration filter in search */
"Short" = "Short";
"Switch to public locations" = "به سرویس دهندهٔ عمومی منتقل شو";
"Profiles" = "نمایه‌ها";
"New Playlist" = "فهرست پخش جدید";
"Automatic" = "خودکار";
"Opening file..." = "در حال باز کردن فایل…";
"Add Quality Profile" = "افزودن نمایهٔ کیفیت";
"Close video after playing last in the queue" = "ویدیو را پس از پخش آخرین مورد فهرست ببند";
/* Player controls layout size */
"Small" = "کوچک";
"Clear" = "پاک کردن";
"Anonymous" = "ناشناس";
"Channel could not be found" = "کانال پیدا نشد";
"Add to Favorites" = "افزودن به موردعلاقه‌ها";
"Total size: %@" = "مجموع اندازه: %@";
"Wi-Fi" = "اینترنت Wi-Fi";
/* Selected video is being played */
"Watching now" = "در حال تماشا";
"Size" = "اندازه";
"Connection failed" = "اتصال موفق نبود";
"Could not open video" = "ویدیو باز نشد";
"Badge color" = "رنگ نشان";
/* Player controls layout size */
"Medium" = "متوسط";
"Reset search filters" = "بازنشانی صافی‌های جست و جو";
"Play" = "پخش";
/* Trending category, section containing all kinds of videos */
"All" = "همه";
"No videos to show" = "ویدیویی برای نمایش نیست";
"Username" = "نام کاربری";
"Don't use public locations" = "از سرویس دهندگان عمومی استفاده نکنید";
"I have a feature request" = "درخواست ویژگی و قابلیت دارم";
"Clear All Recents" = "همهٔ موارد اخیر را پاک کن";
"Regular size" = "ابعاد معمولی";
"Autoplay next" = "پخش خودکار بعدی";
"Shuffle" = "شافل";
"This cannot be reverted" = "این گزینه غیرقابل بازگشت است";
"unknown" = "ناشناخته";
"Could not create share link" = "پیوند اشتراک‌گذاری ساخته نشد";
"Home Settings" = "تنظیمات صفحهٔ اصلی";
"You have no Playlists" = "فهرست پخشی ندارید";
"Watched: visible" = "تماشاشده‌ها: قابل رویت";
"Are you sure you want to remove this document?" = "از حذف این سند مطمئنید؟";
/* Video sort order in search */
"Rating" = "امتیازدهی";
"Public" = "عمومی";
"Add to Playlist..." = "افزودن به فهرست پخش…";
"Category" = "دسته‌بندی";
"Fullscreen" = "تمام صفحه";
"No description" = "بدون شرح";
"Gaming" = "بازی";
"Apply to all" = "اعمال روی همه";
"Close" = "بستن";
/* Video date filter in search */
"Year" = "سال";
"Continue" = "ادامه";
"Show channel name" = "نمایش نام کانال";
"Remove Location" = "حذف سرویس دهنده";
"Large layout is not suitable for all devices and using it may cause controls not to fit on the screen." = "چیدمان بزرگ مناسب همهٔ دستگاه‌ها نیست و استفاده از آن ممکن است باعث بیرون ماندن کنترل‌ها از صفحهٔ نمایش شود.";
"Shorts" = "Shortها";
"Show Favorites" = "نمایش موردعلاقه‌ها";
"Interface" = "رابط کاربری";
"Unsubscribe" = "لغو اشتراک";
"URL" = "نشانی";
"Short videos: visible" = "ویدیوهای Short: قابل رویت";
"Hardware decoder" = "دکودر سخت‌افزاری";
"Search history is empty" = "سابقهٔ جست و جو خالیست";
"Playback Settings" = "تنظیمات پخش";
"Are you sure you want to clear search history?" = "آیا از حذف سابقهٔ جست و جو مطمئنید؟";
"Could not load video" = "ویدیو دریافت نشد";
"Show video length" = "طول ویدیو را نشان بده";
"Hide" = "مخفی کردن";
"Clear Search History" = "پاک کردن سابقهٔ جست و جو";
"Play Now in MPV" = "الان در MPV پخش کن";
"Switch to other public location" = "به یک سرویس دهندهٔ عمومی دیگر منتقل شو";
"Restart the app to apply the settings above." = "برای اعمال تنظیمات بالا اپ را دوباره راه‌اندازی کنید.";
"Are you sure you want to clear cache?" = "آیا از حذف حافظهٔ مخفی مطمئنید؟";
"Filter" = "صافی";
"Open Settings" = "باز کردن تنظیمات";
"Buffering stream..." = "بافر کردن استریم…";
"Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "فهرست پخش خالیست\n\nروی ویدیو ضربه بزنید و نگه دارید، سپس\n\"افزودن به فهرست پخش\" را انتخاب کنید";
"Playback history is empty" = "سابقهٔ پخش خالیست";
"Chapters" = "فصل‌های ویدیو";
"(watched and shorts hidden)" = "(تماشاشده‌ها و Shortها مخفی)";
/* Video sort order in search */
"Views" = "بازدیدها";
"I want to ask a question" = "می‌خواهم سوالی بپرسم";
"Show keywords" = "نمایش کلیدواژه‌ها";
"Edit" = "ویرایش";
"Could not refresh Playlists" = "فهرست پخش بازسازی نشد";
"Categories to Skip" = "دسته‌بندی‌هایی که رد می‌شوند";
"Private" = "خصوصی";
"This video could not be opened" = "این ویدیو باز نمی‌شود";
"Center" = "وسط";
"High" = "بالا";
/* Video date filter in search */
"Today" = "امروز";
"Remove from Playlist" = "حذف از فهرست پخش";
"URL to Open" = "نشانی برای باز کردن";
"Replies" = "پاسخ‌ها";
"Movies" = "فیلم‌ها";
"Unlisted" = "فهرست نشده";
"Wiki" = "ویکی";
"Sort" = "ترتیب";
"Frontend URL" = "نشانی Frontend";
"Restore default profiles..." = "بازنشانی نمایه‌های پیش‌فرض…";
"Any format" = "هر فرمتی";
"Open" = "باز کردن";
"Filter: active" = "صافی: فعال";
"Play Music" = "موسیقی را پخش کن";
"Clear All" = "همه را پاک کن";
"Add Account" = "افزودن حساب کاربری";
"Cancel" = "لغو";
"Accounts are not supported for the application of this instance" = "حساب کاربری در این سرویس دهنده پشتیبانی نمی‌شود";
"System controls" = "کنترل‌های سیستم";
"Hide player" = "پخش‌کننده را مخفی کن";
"Lock orientation" = "قفل چرخش صفحه";
"Cache" = "حافظهٔ مخفی";
/* Video date filter in search */
"Hour" = "ساعت";
"Open Videos" = "باز کردن ویدیو";
"Add" = "افزودن";
"Show icons and text when space permits" = "اگر جا بود متن و آیکن‌ها را نشان بده";
"Documents" = "مستندات";
"Play Next" = "بعدی را پخش کن";
"Battery" = "باتری";
"Rotate to portrait when exiting fullscreen" = "پس از خروج از تمام صفحه به حالت عمودی بچرخ";
"Could not delete document" = "سند حذف نشد";
"Show account username" = "نمایش نام کاربری";
"Queue - shuffled" = "صف - شافل";
"Source" = "منبع";
"Always" = "همیشه";
/* Video duration filter in search */
"Long" = "دراز";
"Mark video as watched after playing" = "ویدیو را پس از پخش به عنوان تماشا شده نشانه بگذار";
"%@ Playlist" = "%@ فهرست‌ پخش";
"Playback queue is empty" = "صف پخش خالیست";
"Add profile..." = "افزودن نمایه…";
"Short videos: hidden" = "ویدیوهای Short: مخفی";
"No documents" = "بدون مستندات";
"Lowest" = "پایینترین";
"Not available" = "در دسترس نیست";
"Add Location" = "افزودن سرویس دهنده";
"Public Locations" = "سرویس دهندگان عمومی";
"Remove from history" = "حذف از سابقه";
"Could not update your token." = "Token شما به روز نشد.";
"Channel" = "کانال";
"Limit" = "محدود";
/* Locations settings, custom instance is selected as current */
"Custom" = "سفارشی";
"Close video" = "ویدیو را ببند";
"Remove from Favorites" = "حذف از موردعلاقه‌ها";
"Watched: hidden" = "تماشاشده: مخفی";
"Close PiP and open player when application enters foreground" = "هنگامی که اپ باز می‌شود تصویر-در-تصویر را ببند و پخش‌کننده را باز کن";
"Show Documents" = "نمایش مستندات";
/* SponsorBlock category name */
"Outro" = "انتها";
"Music Mode" = "وضعیت موسیقی";
"Edit Playlist" = "ویرایش فهرست پخش";
"Mark as watched" = "نشانه گذاری به عنوان تماشا شده";
"Stream" = "استریم";
"Remove…" = "حذف…";
"Add Location..." = "افزودن سرویس دهنده…";
"%@ subscribers" = "%@ مشترکان";
/* SponsorBlock category name */
"Sponsor" = "حامی مالی";
"Remove from the queue" = "حذف از صف";
"Quality Profile" = "نمایهٔ کیفیت";
"LIVE" = "زنده";
"Accounts" = "حساب‌ها";
"Done" = "انجام شد";
"Enter links to open, one per line" = "پیوندها را وارد کنید تا باز شوند، هر خط یک پیوند";
"Files" = "فایل‌ها";
"No results" = "بدون نتیجه";
"Current: %@\n%@" = "فعلی: %@\n%@";
"Donations" = "حمایت مالی";
"%@ Channel" = "%@ کانال";
"Do nothing" = "کاری نکن";
"File" = "فایل";
"Yattee %@ (build %@)" = "Yattee %@ (بیلد %@)";
"Proxy videos" = "پروکسی کردن ویدیوها";
"Player" = "پخش‌کننده";
"Play next item" = "مورد بعدی را پخش کن";
"Controls" = "کنترل‌ها";
"This URL could not be opened" = "این نشانی باز نمی‌شود";
"Trending" = "پرطرفدار";
"Opening audio stream..." = "باز کردن استریم صوتی…";
"Statistics" = "آمار";
"Pause when player is closed" = "پس از بسته شدن پخش‌کننده مکث کن";
"Play All" = "همه را پخش کن";
"Sort: %@" = "ترتیب: %@";
/* Loading stream OSD */
"Opening %@ stream..." = "باز کردن استریم %@…";
"Next in Queue" = "مورد بعد در صف";
"Honor orientation lock" = "قفل چرخش صفحه را در نظر بگیر";
"Rate" = "امتیاز";
"Playing Next" = "پخش بعدی";
"%lld videos" = "%lld ویدیو";
"Enable Return YouTube Dislike" = "فعال کردن بازگرداندن YouTube Dislike";
"Country Name or Code" = "نام یا کد کشور";
"Clear the queue" = "پاک کردن صف";
"Could not extract playlist ID" = "شناسهٔ فهرست پخش دریافت نشد";
"Could not load streams" = "استریم قابل دریافت نیست";
"Are you sure you want to clear history of watched videos?" = "آیا از حذف سابقهٔ تماشا مطمئنید؟";
"Mark all as unwatched" = "همه را به عنوان تماشانشده نشانه بگذار";
"Thumbnails" = "بندانگشتی‌ها";
"Left" = "چپ";
"Open \"Playlists\" tab to create new one" = "زبانهٔ «فهرست پخش» را باز کنید تا یک فهرست جدید بسازید";
"Replay" = "پخش دوباره";
"Remove" = "حذف کردن";
"Close player when starting PiP" = "هنگامی که تصویر-در-تصویر آغاز می‌شود پخش کننده را ببند";
"Controls button: backwards" = "دکمه‌های کنترل: عقب";
"I am lost" = "گم شده‌ام";
"Show only icons" = "فقط آیکن‌ها را نشان بده";
"Clear all" = "همه را پاک کن";
"Shuffle All" = "شافل کردن همه";
"Discord Server" = "سرور Discord";
"Subscribe" = "اشتراک";
"Loading..." = "درحال دریافت…";
"Edit Favorites…" = "ویرایش موردعلاقه‌ها…";
"Sections" = "بخش‌ها";
"Play Last" = "آخرین را پخش کن";
"Pages buttons" = "دکمه‌های صفحه‌ها";
"No comments" = "بدون نظر";
"Connected successfully (%@)" = "با موفقیت وصل شد (%@)";
"(watched hidden)" = "(تماشاشده‌ها مخفی)";
"Enable logging" = "فعال کردن ثبت log";
"Are you sure you want to delete playlist?" = "آیا از حذف فهرست پخش مطمئنید؟";
"Description preview" = "پیش‌نمایش شرح";
"Open Files" = "باز کردن فایل‌ها";
"Hide sidebar" = "نوار کناری را مخفی کن";
"Show anonymous accounts" = "نمایش حساب‌های ناشناس";
"Always show controls buttons" = "همیشه دکمه‌های کنترل را نشان بده";
"Mark all as watched" = "همه را به عنوان تماشاشده نشانه بگذار";
/* Player controls layout size */
"Smaller" = "کوچکتر";
"Search" = "جست و جو";
"Favorites" = "مورد علاقه‌ها";
"Address" = "نشانی";
"MPV Documentation" = "مستندات MPV";
"If you are reporting a bug, include all relevant details (especially: app version, used device and system version, steps to reproduce)." = "اگر ایرادی را گزارش می‌کنید، همهٔ جزییات مربوط (به ویژه نسخهٔ اپ، دستگاه مورد استفاده و نسخهٔ سیستم عامل و گام‌هایی که برای بازسازی ایراد لازم است) را ذکر کنید.";
"Only when signed in" = "تنها پس از ورود";
"Sign In Required" = "لازم است که وارد شوید";
/* SponsorBlock category name */
"Self-promotion" = "تبلیغ شخصی";
"Only for local files and URLs" = "فقط برای فایل‌های محلی و پیوندها";
/* SponsorBlock category name */
"Interaction" = "تعامل";
"Preferred Formats" = "فرمت‌های ترجیحی";
"Reset" = "بازنشانی";
"Badge" = "نشان";
"Error when accessing playlist" = "خطا هنگام دسترسی به فهرست پخش";
"Right" = "راست";
"Blue" = "آبی";
"Low quality" = "کیفیت پایین";
"Popular" = "محبوب";
"List" = "فهرست";
"Create Playlist" = "ساخت فهرست پخش";
"Show sidebar when space permits" = "اگر جا بود نوارکناری را نشان بده";
"Locations" = "سرویس دهندگان";
"Playlists" = "فهرست‌های پخش";
"Current Location" = "سرویس دهندهٔ فعلی";
"Could not open Files" = "فایل باز نشد";
"Regular Size" = "ابعاد معمولی";
"Could not refresh Popular" = "محبوبترین‌ها به روز نمی‌شود";
"Recent History" = "سابقهٔ اخیر";
"Clear History" = "پاک کردن سابقه";
"Play all unwatched" = "همهٔ دیده‌نشده‌ها را پخش کن";
"Show Home" = "نمایش صفحهٔ اصلی";
"More info can be found in:" = "اطلاعات بیشتر در:";
"Name" = "نام";
"History" = "سابقه";
/* Video date filter in search */
"Month" = "ماه";
"Matrix Channel" = "کانال Matrix";
"Round corners" = "گوشه‌های گرد";
"Pause when entering background" = "پس از ورود به پس‌زمینه مکث کن";
"Music" = "موسیقی";
"I like this app!" = "از این اپ خوشم می‌آید!";
"Upload date" = "تاریخ انتشار";
"Save history of played videos" = "ذخیرهٔ سابقهٔ ویدیوهای پخش شده";
"Add Account..." = "افزودن حساب کاربری…";
"Public account" = "حساب کاربری عمومی";
"Add %@" = "افزودن %@";
"Autoplaying Next" = "پخش خودکار بعدی";
"You have no playlists\n\nTap on \"New Playlist\" to create one" = "فهرست پخش ندارید\n\nروی «فهرست جدید» ضربه بزنید و یکی بسازید";
"No chapters information available" = "اطلاعاتی در مورد فصل‌های ویدیو در دسترس نیست";
"Lock" = "قفل";
"Nothing" = "هیچ";
"Cellular" = "اینترنت همراه";
"Password" = "رمز عبور";
"Add to Playlist" = "افزودن به فهرست پخش";
"(shorts hidden)" = "(Shortها مخفی)";
"Are you sure you want to remove %@ location?" = "از حذف سرویس دهندهٔ \"%@\" مطمئنید؟";
"Current Playlist" = "فهرست پخش فعلی";
/* Selected video was played on given date */
"Watched %@" = "تماشا شده %@";
"Videos" = "ویدیوها";
"No locations available at the moment" = "در حال حاضر سرویس دهنده‌ای در دسترس نیست";
"Show history" = "نمایش سابقه";
"Default Profile" = "نمایهٔ پیش‌فرض";
"Red" = "قرمز";
"No preview" = "بدون پیش‌نمایش";
"Info" = "اطلاعات";
/* Loading stream OSD */
"Loading streams..." = "درحال دریافت استریم…";
"No rotation" = "بدون چرخش";
"Codec" = "کدک (Codec)";
"Startup section" = "بخش آغازین";
"Edit Quality Profile" = "ویرایش نمایهٔ کیفیت";
"Browse without account" = "استفاده بدون حساب کاربری";
/* Video sort order in search */
"Relevance" = "ارتباط";
"Help" = "کمک";
"Find Other" = "یافتن دیگری";
"Play Now" = "الان پخش کن";
"Close PiP when starting playing other video" = "هنگامی که ویدیوی دیگری آغاز به پخش می‌کند تصویر-در-تصویر را ببند";
"Show cache status" = "نمایش وضعیت حافظهٔ مخفی";
"Play Now in AVPlayer" = "الان در AVPlayer پخش کن";
"Medium quality" = "کیفیت متوسط";
"Stream & Player" = "استریم و پخش‌کننده";
"Contributing" = "مشارکت";
"Save" = "ذخیره";
"Buttons labels" = "برچسب دکمه‌ها";
"Close video and player on end" = "در پایان ویدیو و پخش‌کننده را ببند";
"Delete" = "حذف";
/* Player controls layout size for TV */
"TV" = "تلویزیون";
"Continue from %@" = "از %@ ادامه بده";
"I found a bug /" = "ایرادی پیدا کرده‌ام /";
"Recent Documents" = "مستندات تازه";
"Orientation" = "چرخش صفحه";
"Low" = "پایین";
"Restart" = "راه‌اندازی دوباره";
"Search..." = "جست و جو…";
"Open Video" = "ویدیو را باز کن";
"Sidebar" = "نوار کناری";
"Translations" = "ترجمه‌ها";
"Duration" = "مدت زمان";
/* Video sort order in search */
"Date" = "تاریخ";
"Disable filters" = "غیرفعال کردن صافی‌ها";
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "فهرست پخش \"%@\" حذف خواهد شد.\nقابل بازگردانی نیست.";
"Enter link to open" = "پیوند را وارد کنید تا باز شود";
"Restart/Play next" = "پخش دوباره/بعدی";
"Your Accounts" = "حساب کاربری شما";
"Open channel" = "کانال را باز کن";
"Queue" = "صف";
"Controls button: forwards" = "دکمه‌های کنترل: جلو";
"Normal" = "عادی";
"Next" = "بعدی";
"Home" = "صفحهٔ اصلی";
"Contact" = "تماس";
"Show Next in Queue" = "مورد بعدی صف را نشان بده";
"Browsing" = "مرور";
"Watched" = "تماشا شده";
"Highest" = "بالاترین";
"You can find information about using Yattee in the Wiki pages." = "اطلاعات در مورد چگونگی استفاده از Yattee را می‌توانید در صفحات ویکی پیدا کنید.";
"Captions" = "زیرنویس";
"Playlist" = "فهرست پخش";
/* SponsorBlock category name */
"Intro" = "مقدمه";
"Description" = "شرح";
"Instance of current account" = "سرویس دهندهٔ حساب کاربری فعلی";
"Advanced" = "پیشرفته";
"Welcome" = "خوش آمدید";
/* Selected video has just finished playing */
"Just watched" = "همین الان تماشا کردم";
"Close PiP when player is opened" = "هنگامی که پخش‌کننده باز می‌شود تصویر-در-تصویر را ببند";
"Lock portrait mode" = "قفل حالت عمودی";
/* Video date filter in search
Video duration filter in search */
"Any" = "هر";
"Mark watched videos with" = "نشانه گذاری ویدیوها به عنوان تماشا شده با";
"Not Playing" = "پخش نمی‌شود";
"Video Details" = "جزییات ویدیو";
"Live Streams" = "پخش زنده";

View File

@@ -600,3 +600,7 @@
"Podcasts" = "Podcasts";
"Releases" = "Sorties";
"Add %@" = "Ajouter %@";
"Description preview" = "Aperçu de la description";
"No preview" = "Aucun aperçu";
"Open vertical chapters expanded" = "Ouvrir les chapitres verticaux étendus";
"Chapters (if available)" = "Chapitres (si disponibles)";

View File

@@ -1,19 +1,19 @@
"%@ Channel" = "%@ चैनल";
"%@ Playlist" = "%@ प्लेलिस्ट";
"%@ Playlist" = "%@ गीतमाला";
"%@ subscribers" = "%@ सदस्य";
"%lld videos" = "%lld वीडियो";
"%lld videos" = "%lld वीडियो";
" subscribers" = " सदस्य";
"10 seconds forwards/backwards" = "10 पल आगे/पीछे";
"Accounts" = "खाते";
"Accounts are not supported for the application of this instance" = "इस इन्सटेंस के लिए खातों उपलब्ध नहीं हैं";
"10 seconds forwards/backwards" = "10 सेकंड आगे/पीछे";
"Accounts" = "खाते";
"Accounts are not supported for the application of this instance" = "इस इन्सटेंस के लिए खात उपलब्ध नहीं हैं";
"Add to Playlist..." = "गीतमाला में जोड़ें…";
"Advanced" = "उन्नत";
/* Trending category, section containing all kinds of videos */
"All" = "सभी";
"Always use AVPlayer for live videos" = "लाइव वीडियो के लिए सदा ए.वी. प्लेयर का उपयोग करें";
"Always use AVPlayer for live videos" = "लाइव वीडियो के लिए सदा AVPlayer का उपयोग करें";
"Anonymous" = "गुप्त";
/* Video date filter in search
@@ -24,16 +24,16 @@
"Are you sure you want to clear search history?" = "क्या आप निश्चित रूप से खोज का इतिहास मिटाना चाहते हैं?";
"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 बंद करें जब दूसरा वीडियो प्ले हो";
"Close player when closing video" = "प्लेयर बंद करें जब वीडियो बंद हो";
"Close player when starting PiP" = "प्लेयर बंद करें जब PiP चालू हो";
"Close PiP when starting playing other video" = "PiP बंद करें जब दूसरा वीडियो चालू हो";
"Close player when closing video" = "जब वीडियो बंद हो तब प्लेयर बंद करें";
"Close player when starting PiP" = "जब PiP चालू हो रहा हो तब प्लेयर बंद करें";
"Close Video" = "वीडियो बंद करें";
"Close video after playing last in the queue" = "कतार में आख़िरी समाप्त होने के पश्चात वीडियो बंद करें";
"Close video after playing last in the queue" = "अंतिम पंक्तिबद्ध समाप्त होने के पश्चात वीडियो बंद करें";
"Comments" = "टिप्पणियाँ";
"Connected successfully (%@)" = "जुड़ाव सफल";
"Connected successfully (%@)" = "जुड़ाव सफल (%@)";
"Connection failed" = "जुड़ाव असफल";
"Contact" = "संपर्क";
"Continue" = "जारी";
"Continue" = "जारी रखें";
"Disabled" = "निर्योग्य";
"Discord Server" = "डिस्कॉर्ड सर्वर";
"Discussions take place in Discord and Matrix. It's a good spot for general questions." = "चर्चाएँ डिस्कॉर्ड एवं मेट्रिक्स पर होतीं हैं। यह सामान्य प्रश्नों के लिए अच्छा मंच है।";
@@ -154,18 +154,18 @@
"Play Now" = "अभी चलाएँ";
"Playback" = "प्लेबैक";
"Player" = "प्लेयर";
"Playlist" = "प्लेलिस्ट";
"Playlist" = "गीतमाला";
"Decreased opacity" = "पारदर्शिता घटी";
"Continue from %@" = "%@ से जारी रें";
"Continue from %@" = "%@ से जारी रें";
"Current: %@\n%@" = "वर्तमान: %@\n%@";
"Contributing" = "योगदान दें";
"Contributing" = "योगदान";
"Controls" = "नियंत्रक";
"Copy %@ link" = "%@ लिंक नक़ल करें";
"Copy %@ link" = "%@ लिंक कॉपी करें";
"Country" = "देश";
"Country Name or Code" = "देश नाम एवं कोड";
"Could not load locations manifest" = "लोकेशन मैनिफ़ेस्ट लोड नहीं हो पाई";
"Country Name or Code" = "देश का नाम एवं कोड";
"Could not load locations manifest" = "लोकेशन मैनिफ़ेस्ट लोड नहीं हो सका";
"Custom Locations" = "कस्टम स्थान";
"Copy %@ link with time" = "%@ लिंक समय के साथ नक़ल करें";
"Copy %@ link with time" = "%@ लिंक समय के साथ कॉपी करें";
"Add Account" = "खाता जोड़ें";
"Add Account..." = "खाता जोड़ें…";
"Add Location" = "स्थान जोड़ें";
@@ -177,35 +177,35 @@
"Add to %@" = "%@ में जोड़ें";
"Badge color" = "बिल्ले का रंग";
"Category" = "श्रेणी";
"Clear Search History..." = "खोज इतिहास साफ़ करें…";
"Clear Search History..." = "खोज इतिहास हटायें…";
"Are you sure you want to restore default quality profiles?" = "क्या आप निश्चित रूप से डीफ़ॉल्ट गुणवत्ता प्रोफ़ाइल को पुनः प्राप्त करना चाहते हैं?";
"Based on system color scheme" = "मूल तंत्र रंग प्रणाली पर निर्भर";
"Based on system color scheme" = "सिस्टम के रंग प्रणाली पर निर्भर";
"Categories to Skip" = "श्रेणियाँ जिन्हें छोड़ें";
"Chapters" = "अध्यायें";
"Chapters" = "अध्याय";
"Are you sure you want to delete playlist?" = "क्या आप निश्चित रूप से इस गीतमाला को मिटाना चाहते हैं?";
"Are you sure you want to unsubscribe from %@?" = "क्या आप निश्चित रूप से %@ की सदस्यता छोड़ना चाहते हैं?";
"Automatic" = "स्वत";
"Autoplaying Next" = "अगला ऑटोप्ले हो रहा";
"Automatic" = "स्वचालित";
"Autoplaying Next" = "अगला स्वतः चालू हो रहा";
"Badge" = "बिल्ला";
"Blue" = "नीला";
"Browsing" = "ब्राउज़ करना";
"Buffering stream..." = "स्ट्रीम बफ़र हो रहा…";
"Bugs and great feature ideas can be sent to the GitHub issues tracker. " = "आप बग एवं फ़ीचर सुझाव गिटहब के इशु ट्रैकर पर भेज सकते हैं ";
"Cancel" = "रद्द";
"Captions" = "कैप्शन";
"Charging" = "चार्ज हो रहा";
"Clear" = "साफ़";
"Clear All" = "सभी साफ़";
"Clear All Recents" = "सभी हालिया साफ़ करें";
"Clear History" = "इतिहास साफ़ करें";
"Clear Search History" = "खोज इतिहास साफ़ करें";
"Clear the queue" = "क़तार साफ़ करें";
"Captions" = "अनुशीर्षक";
"Charging" = "चार्ज हो रहा है";
"Clear" = "हटायें";
"Clear All" = "सभी हटायें";
"Clear All Recents" = "सभी हालिया हटायें";
"Clear History" = "इतिहास हटायें";
"Clear Search History" = "खोज इतिहास हटायें";
"Clear the queue" = "पंक्तिबद्ध हटायें";
"Close" = "बंद";
"Backend" = "बैकऐंड";
"Backend" = "पृष्ठभाग";
"Badge & Decreased opacity" = "बिल्ला एवं पारदर्शिता में कमी";
"Battery" = "बैटरी";
"Button" = "बटन";
"Cellular" = "मोबाइल डाटा";
"Cellular" = "मोबाइल";
"Create Playlist" = "गीतमाला बनाएँ";
/* Player controls layout size */
@@ -267,7 +267,7 @@
"Statistics" = "आँकड़े";
"Hardware decoder" = "हार्डवेयर डीकोडर";
"Stream FPS" = "स्ट्रीम FPS";
"Playlists" = "प्लेलिस्ट";
"Playlists" = "गीतमाला";
"Popular" = "लोकप्रिय";
"Preferred Formats" = "पसंदीदा प्रारूप";
"Profiles" = "प्रोफ़ाइल";
@@ -415,7 +415,7 @@
"Locations Manifest" = "";
"Could not delete document" = "";
"Show Inspector" = "";
"Could not load streams" = "";
"Could not load streams" = "स्ट्रीम्स नहीं खोला जा सका";
"Address" = "";
"Playback Mode" = "";
"Left" = "";
@@ -447,17 +447,17 @@
"Verified" = "";
"No documents" = "";
"Show Open Videos toolbar button" = "";
"Could not refresh Subscriptions" = "";
"Could not refresh Subscriptions" = "सदस्यता पुनर्स्थापित नहीं किया जा सका";
"Remove Location" = "";
"Only for local files and URLs" = "";
"Codec" = "";
"File" = "";
"Could not extract playlist ID" = "";
"Paste" = "";
"Could not extract SID from received cookies: %@" = "";
"Could not extract SID from received cookies: %@" = "प्राप्त कुकीज़ से SID नहीं निकाला जा सका: %@";
"Files" = "";
"Show Home" = "";
"Could not extract channel information" = "";
"Could not extract channel information" = "चैनल की जानकारी नहीं निकाली जा सकी";
"Enter link to open" = "";
"Home" = "";
"No locations available at the moment" = "";
@@ -469,7 +469,7 @@
"Format" = "";
"Share files from Finder on a Mac\nor iTunes on Windows" = "";
"Share%@link" = "";
"Could not open video" = "";
"Could not open video" = "वीडियो नहीं खोला जा सका";
"Center" = "";
"Size" = "";
"Recent Documents" = "";
@@ -484,14 +484,14 @@
"Could not create share link" = "";
"Sample Rate" = "";
"Translations" = "";
"Open logs in Finder" = "";
"Open logs in Finder" = "लॉग को Finder में खोलें";
"Could not refresh Playlists" = "";
"Could not update your token." = "";
"Could not refresh Trending" = "";
"Could not update your token." = "आपका टोकन अपडेट नहीं हो सका।";
"Could not refresh Trending" = "ट्रेंडिंग को पुनर्स्थापित नहीं किया जा सका";
"Could not open playlist" = "";
"This URL could not be opened" = "";
"Could not open channel" = "";
"Channel could not be found" = "";
"This URL could not be opened" = "इस URL को नहीं खोला जा सका";
"Could not open channel" = "चैनल नहीं खुल सका";
"Channel could not be found" = "चैनल नहीं मिला";
"Could not refresh Popular" = "";
"Could not extract video ID" = "";
"Channel" = "";
@@ -501,4 +501,4 @@
"Pages toolbar position" = "";
"Default Profile" = "";
"Copy%@link" = "";
"Share Logs..." = "";
"Share Logs..." = "लॉग साझा करें…";

View File

@@ -12,7 +12,7 @@
" subscribers" = " 人の登録者";
"%@ subscribers" = "%@ 人の登録者";
"Accounts are not supported for the application of this instance" = "このインスタンスはアカウントに対応していません";
"%lld videos" = "本の動画";
"%lld videos" = "%lld本の動画";
"%@ Channel" = "%@ チャンネル";
"%@ Playlist" = "%@ 再生リスト";
"Add Location" = "場所を追加";
@@ -600,3 +600,7 @@
"Podcasts" = "ポッドキャスト";
"Releases" = "リリース";
"Add %@" = "追加 %@";
"Description preview" = "説明のプレビュー";
"No preview" = "プレビューなし";
"Open vertical chapters expanded" = "チャプターを縦方向に開く";
"Chapters (if available)" = "チャプター (あれば)";

View File

@@ -0,0 +1,261 @@
"Always use AVPlayer for live videos" = "Gebruik altijd AVPlayer voor live video's";
"Clear Search History..." = "Wis zoek geschiedenis...";
"Add to %@" = "Toevoegen aan %@";
"10 seconds forwards/backwards" = "10 seconden vooruit/achteruit";
"Based on system color scheme" = "Gebaseerd op systeem kleuren pallet";
" subscribers" = " Abonnees";
"Button" = "Knop";
"Are you sure you want to restore default quality profiles?" = "Weet u zeker dat u de standaardkwaliteitsprofielen wilt herstellen?";
"Charging" = "Opladen";
"Are you sure you want to unsubscribe from %@?" = "Weet u zeker dat u zich wilt afmelden voor %@?";
"Automatic" = "Automatisch";
"Add Quality Profile" = "Kwaliteitsprofiel toevoegen";
"Clear" = "Leeg";
"Anonymous" = "Anoniem";
"Add to Favorites" = "Toevoegen aan favorieten";
"Badge color" = "Embleem kleur";
/* Trending category, section containing all kinds of videos */
"All" = "Alles";
"Clear All Recents" = "Leeg alle recenten";
"Bugs and great feature ideas can be sent to the GitHub issues tracker. " = "Bugs en functie-ideeën kunnen naar de GitHub-problementracker worden gestuurd. ";
"Add to Playlist..." = "Toevoegen aan afspeellijst...";
"Category" = "Categorie";
"Apply to all" = "Toepassen op alles";
"Close" = "Dicht";
"Are you sure you want to clear search history?" = "Weet u zeker dat u de zoekgeschiedenis wilt wissen?";
"Clear Search History" = "Wis zoek geschiedenis";
"Buffering stream..." = "Stream bufferen...";
"Chapters" = "Hoofdstukken";
"Categories to Skip" = "Categorieën om over te slaan";
"Clear All" = "Leeg alles";
"Add Account" = "Account toevoegen";
"Cancel" = "Annuleren";
"Accounts are not supported for the application of this instance" = "Accounts zijn niet ondersteund voor de toepassing van dit exemplaar";
"Battery" = "Batterij";
"%@ Playlist" = "Afspeellijst";
"Add profile..." = "Profiel toevoegen...";
"Add Location" = "Locatie toevoegen";
"Add Location..." = "Locatie toevoegen...";
"%@ subscribers" = "abonnees";
"Accounts" = "Accounts";
"%@ Channel" = "Kanaal";
"%lld videos" = "%lld video's";
"Clear the queue" = "Wis de wachtrij";
"Are you sure you want to clear history of watched videos?" = "Weet je zeker dat je de geschiedenis van bekeken video's wilt wissen?";
"Are you sure you want to delete playlist?" = "Weet je zeker dat je de afspeellijst wilt verwijderen?";
"Badge" = "Embleem";
"Blue" = "Blauw";
"Clear History" = "Wis geschiedenis";
"Add Account..." = "Account toevoegen...";
"Autoplaying Next" = "Volgende automatisch afspelen";
"Cellular" = "Mobiele data";
"Add to Playlist" = "Toevoegen aan afspeellijst";
"Browsing" = "Bladeren";
"Captions" = "Ondertiteling";
"Advanced" = "Geavanceerd";
/* Video date filter in search
Video duration filter in search */
"Any" = "Elk";
"If you are reporting a bug, include all relevant details (especially: app version, used device and system version, steps to reproduce)." = "Als je een fout meldt, voeg dan alle relevantie details toe (in het bijzonder: versienummer van de app, gebruikte apparaat en systeemversie, stappen om te reproduceren).";
"Opening audio stream..." = "Audio stream aan het openen...";
"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." = "Deel van een video dat een product of dienst promoot dat niet direct van de maker van de video is. De maker ontvangt compensatie in de vorm van betaling of gratis producten.";
"Pause when player is closed" = "Pauzeer bij sluiten van de speler";
"Remove" = "Verwijder";
"Restart" = "Herstart";
"Round corners" = "Ronde hoeken";
"Restart the app to apply the settings above." = "Herstart de app om deze instellingen toe te passen.";
"Save" = "Opslaan";
"Close Video" = "Sluit Video";
"Close video after playing last in the queue" = "Sluit video na het laatste item in de wachtrij";
"Contact" = "Contact";
"Copy %@ link" = "Kopieer %@ link";
"Copy %@ link with time" = "Kopieer %@ link inclusief tijd";
"Custom Locations" = "Aangepaste locaties";
"Continue" = "Verder";
/* Video sort order in search */
"Date" = "Datum";
"Contributing" = "Bijdragen";
"Controls" = "Bediening";
"Discussions take place in Discord and Matrix. It's a good spot for general questions." = "Discussies vinden plaats op Discord en Matrix. Je kan er terecht voor algemene vragen.";
"Enter fullscreen in landscape" = "Open volledig scherm in landschapmodus";
"Error when accessing playlist" = "Fout bij openen afspeellijst";
"Donations" = "Donaties";
"Formats will be selected in order as listed.\nHLS is an adaptive format (resolution setting does not apply)." = "Formaten worden geselecteerd in de genoemde volgorde.\nHLS is een adaptief formaat (instellingen voor resolutie worden niet toegepast).";
"Backend" = "Backend";
"Badge & Decreased opacity" = "Embleem & Verminderde helderheid";
"Close PiP and open player when application enters foreground" = "Sluit Beeld in Beeld en open speler wanneer de app op de voorgrond treedt";
"Close PiP when player is opened" = "Sluit Beeld in Beeld wanneer de speler wordt geopend";
"Close PiP when starting playing other video" = "Sluit Beeld in Beeld wanneer een andere video begint te spelen";
"Close player when closing video" = "Sluit speler bij het sluiten van een video";
"Close player when starting PiP" = "Sluit speler bij het starten van Beeld in Beeld";
"Comments" = "Commentaar";
"Connected successfully (%@)" = "Succesvol verbonden (%@)";
"Connection failed" = "Verbinding mislukt";
"Continue from %@" = "Verder vanaf %@";
"Could not load locations manifest" = "Kon locatie manifest niet laden";
"Country" = "Land";
"Country Name or Code" = "Landnaam of -code";
"Create Playlist" = "Creëer afspeellijst";
"Current: %@\n%@" = "Huidig: %@\n%@";
/* Locations settings, custom instance is selected as current */
"Custom" = "Aangepast";
"Decreased opacity" = "Verlaagde helderheid";
"Delete" = "Verwijder";
"Disabled" = "Uitgeschakeld";
"Don't use public locations" = "Gebruik geen publieke locaties";
"Decrease rate" = "Verlaag tempo";
"Done" = "Klaar";
"Duration" = "Duur";
"Edit" = "Bewerken";
"Edit Playlist" = "Bewerk Afspeellijst";
"Edit Quality Profile" = "Bewerk Kwaliteitsprofiel";
"Edit..." = "Bewerk...";
"Enable Return YouTube Dislike" = "Schakel Return YouTube Dislike in";
"Error" = "Fout";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Expliciete herinneringen om te liken, abonneren of communiceren met hen op enig ander gratis of betaald platform (bijv. op een video klikken).";
"Favorites" = "Favorieten";
"Filter" = "Filter";
"Filter: active" = "Filter: actief";
"Find Other" = "Vind Andere";
"Finding something to play..." = "Vind iets om af te spelen...";
"For videos which feature music as the primary content." = "Voor videos waarin muziek centraal staat als inhoud.";
"Frontend URL" = "Frontend URL";
"Fullscreen size" = "Volledig schermformaat";
"Gaming" = "Gamen";
"Help" = "Hulp";
"Hide sidebar" = "Verberg zijbalk";
"High" = "Hoog";
"Highest" = "Hoogste";
"Highest quality" = "Hoogste kwaliteit";
"History" = "Geschiedenis";
"Honor orientation lock" = "Staanderichtingsslot opvolgen";
/* Video date filter in search */
"Hour" = "Uur";
"I am lost" = "Ik ben verdwaald";
"I found a bug /" = "Ik heb een fout gevonden /";
"I have a feature request" = "Ik heb een functieverzoek";
"I like this app!" = "Ik vind deze app leuk!";
"I want to ask a question" = "Ik wil een vraag stellen";
"If you are interested what's coming in future updates, you can track project Milestones." = "Als je geïnteresseerd bent in toekomstige updates, kan je Milestones van het project volgen.";
"Increase rate" = "Verhoog tempo";
"Info" = "";
"Instance of current account" = "Instantie van huidig account";
/* SponsorBlock category name */
"Interaction" = "Interactie";
"Issues Tracker" = "Problementracker";
/* Selected video has just finished playing */
"Just watched" = "Net bekeken";
/* Player controls layout size */
"Large" = "Groot";
"Large layout is not suitable for all devices and using it may cause controls not to fit on the screen." = "Grote layout is niet geschikt voor alle apparaten en kan er voor zorgen dat niet alles op het scherm past.";
/* Loading stream OSD */
"Loading streams..." = "Streams aan het laden...";
"Loading..." = "Laden...";
"Locations" = "Locaties";
"Lock portrait mode" = "Vergrendel protretmodus";
/* Video duration filter in search */
"Long" = "Lang";
"Low" = "Laag";
"Low quality" = "Lage kwaliteit";
"Lowest" = "Laagste";
"Mark as watched" = "Markeer als bekeken";
"Mark video as watched after playing" = "Markeer videos als bekeken na afspelen";
"Mark watched videos with" = "Markeer videos als bekeken met";
"Matrix Channel" = "Matrix Kanaal";
/* Player controls layout size */
"Medium" = "Gemiddeld";
"Medium quality" = "Gemiddelde kwaliteit";
"Milestones" = "Mijlpalen";
/* Video date filter in search */
"Month" = "Maand";
"More info can be found in:" = "Meer informatie beschikbaar in:";
"Movies" = "Films";
"MPV Documentation" = "MPV Documentatie";
"Music" = "Muziek";
"Name" = "Naam";
"New Playlist" = "Nieuwe Afspeellijst";
"Next" = "Volgende";
"No description" = "Geen omschrijving";
"No Playlists" = "Geen Afspeellijsten";
"No results" = "Geen resultaten";
"Normal" = "Normaal";
"Not available" = "Niet beschikbaar";
"Not Playing" = "Niet aan het spelen";
"Nothing" = "Niets";
/* SponsorBlock category name */
"Offtopic in Music Videos" = "Ongerelateerd in muziekvideos";
"Only when signed in" = "Alleen wanneer ingelogd";
"Open \"Playlists\" tab to create new one" = "Open tabblad \"Afspeellijsten\" om een nieuwe te creëeren";
"Open Settings" = "Open Instellingen";
/* Loading stream OSD */
"Opening %@ stream..." = "Steam %@ aan het openen...";
"Orientation" = "Oriëntatie";
"Password" = "Wachtwoord";
"Pause" = "Pauzeren";
"Pause when entering background" = "Pauzeer wanneer op de achtergrond";
"Picture in Picture" = "Beeld in Beeld";
"Play" = "Speel Af";
"Play in PiP" = "Speel af in BiB";
"Play All" = "Speel Alle";
"Play Last" = "Speel Laatste";
"Play Music" = "Speel Muziek";
"Play Next" = "Speel Volgende";
"Play Now" = "Speel Nu";
"Playback" = "Afspelen";
"Player" = "Speler";
"Playlist" = "Afspeellijst";
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "Afspeellijst \"%@\" wordt verwijderd.\nDit kan niet ongedaan gemaakt worden.";
"Playlists" = "Afspeellijsten";
"Popular" = "Populair";
"Preferred Formats" = "Voorkeursformaten";
"Profiles" = "Profielen";
"Promoting a product or service that is directly related to the creator themselves. This usually includes merchandise or promotion of monetized platforms." = "Promotie van een product of dienst van de maker van de video zelf. Dit is inclusief promotie van merchandise of betaalde platformen.";
"Public Locations" = "Publieke Locaties";
"Public Manifest" = "Openbaar Manifest";
"Quality" = "Kwaliteit";
"Quality Profile" = "Kwaliteitsprofiel";
"Queue" = "Wachtrij";
"Queue is empty" = "Lege wachtrij";
"Rate" = "Tempo";
/* Video sort order in search */
"Rating" = "Beoordeling";
"Recents" = "Recent";
"Red" = "Rood";
"Refresh" = "Ververs";
"Regular size" = "Normaal formaat";
"Regular Size" = "Normaal Formaat";
"Related" = "Gerelateerd";
/* Video sort order in search */
"Relevance" = "Relevantie";
"Remove from Favorites" = "Verwijder uit Favorieten";
"Remove from history" = "Verwijder uit geschiedenis";
"Remove from Playlist" = "Verwijder uit Afspeellijst";
"Remove from the queue" = "Verwijder uit de wachtrij";
"Replies" = "Reacties";
"Reset search filters" = "Reset zoekfilters";
"Reset watched status when playing again" = "Reset bekeken-status bij opnieuw afspelen";
"Resolution" = "Resolutie";
"Restart/Play next" = "Herstart/Speel hierna";
"Restore default profiles..." = "Herstel standaard profielen...";
"Rotate to portrait when exiting fullscreen" = "Roteer naar portretmodus bij het verlaten van volledig scherm";
"Save history of played videos" = "Sla geschiedenis van afgespeelde videos op";
"Save history of searches, channels and playlists" = "Sla geschiedenis van zoekopdrachten, kanalen en afspeellijsten op";
"Search" = "Zoeken";

View File

@@ -601,3 +601,7 @@
"Podcasts" = "Podkasty";
"Releases" = "Wydania";
"Add %@" = "Dodaj %@";
"Description preview" = "Podgląd opisu";
"No preview" = "Brak podglądu";
"Open vertical chapters expanded" = "Otwórz pionowe rozdziały rozwinięte";
"Chapters (if available)" = "Rozdziały (jeśli dostępne)";

View File

@@ -600,3 +600,7 @@
"Podcasts" = "Podcasts";
"Releases" = "Lançamentos";
"Add %@" = "Adicionar %@";
"Description preview" = "Descrição da prévia";
"No preview" = "Sem prévia";
"Open vertical chapters expanded" = "Abrir capítulos verticais expandidos";
"Chapters (if available)" = "Capítulos (se disponível)";

View File

@@ -595,3 +595,12 @@
"Replay" = "Replay";
"Autoplay next" = "Tocar automaticamente próximo";
"Stream" = "Stream";
"Podcasts" = "Podcasts";
"Releases" = "Lançamentos";
"Show channel avatars in channels lists" = "Mostrar avatares de canais em listas de canais";
"Open vertical chapters expanded" = "Abrir capítulos verticais expandidos";
"Description preview" = "Descrição da prévia";
"Show channel avatars in videos lists" = "Mostrar avatares de canais em listas de vídeos";
"Add %@" = "Adicionar %@";
"No preview" = "Sem prévia";
"Chapters (if available)" = "Capítulos (se disponível)";

View File

@@ -600,3 +600,7 @@
"Podcasts" = "Podcast-uri";
"Releases" = "Lansări";
"Add %@" = "Adaugă %@";
"Open vertical chapters expanded" = "Deschideți capitolele verticale extinse";
"Description preview" = "Descriere preview";
"No preview" = "Fără previzualizare";
"Chapters (if available)" = "Capitole (dacă există)";

View File

@@ -597,3 +597,8 @@
"Close video and player on end" = "Закрыть видео и плеер в конце";
"Use system controls with AVPlayer" = "Использование системных элементов управления с помощью AVPlayer";
"File Extension" = "Расширение файла";
"No preview" = "Без предварительного просмотра";
"Description preview" = "Предварительный просмотр описания";
"Podcasts" = "Подкасты";
"Releases" = "Релизы";
"Add %@" = "Добавить %@";

View File

@@ -267,76 +267,76 @@
"Save history of searches, channels and playlists" = "Arama, kanal ve çalma listelerinin geçmişini kaydet";
"Restore default profiles..." = "Varsayılan profilleri geri yükle...";
"Visibility" = "";
"Translations" = "";
"Translations" = "Çeviriler";
"Enter links to open, one per line" = "";
"Open Videos" = "";
"Playback Mode" = "";
/* Selected video was played on given date */
"Watched %@" = "";
"Yattee %@ (build %@)" = "";
"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." = "";
"Thumbnails" = "";
"Watched %@" = "İzlendi %@";
"Yattee %@ (build %@)" = "Yattee %@ (versiyon %@)";
"That's nice to hear. It is fun to deliver apps other people want to use. You can consider donating to the project or help by contributing to new features development." = "Bunu duymak çok güzel. Başkalarının kullanmak isteyeceği uygulamalar sunmak eğlenceli. Projeye bağış yapmayı düşünebilir veya yeni özelliklerin geliştirilmesine katkıda bulunarak yardımcı olabilirsiniz.";
"Thumbnails" = "Küçük Resimler";
"Dropped frames" = "";
"SponsorBlock API Instance" = "";
/* Selected video is being played */
"Watching now" = "";
"Video Details" = "";
"Watching now" = "Şu an izlenen";
"Video Details" = "Video Bilgileri";
"Live Streams" = "";
"Typically near or at the end of the video when the credits pop up and/or endcards are shown." = "";
"Typically near or at the end of the video when the credits pop up and/or endcards are shown." = "Genellikle videonun sonunda veya sonuna yakın özet açılır ve/veya bitiş arayüzü gösterilir.";
"Default Profile" = "";
/* Player controls layout size for TV */
"TV" = "";
"TV" = "TV";
"Add" = "";
"This URL could not be opened" = "";
"Hide" = "";
"Playing Next" = "";
"Are you sure you want to remove this document?" = "";
"Show channel name" = "";
"Show channel name" = "Kanal adını göster";
"Unlisted" = "";
"Paste" = "";
"Rate & Captions" = "";
"Format" = "";
"Right" = "";
"Right" = "Sağ";
"Stream FPS" = "";
"Cached time" = "";
"Sign In Required" = "";
"Sign In Required" = "Giriş yapmanız gerekiyor";
"Could not create share link" = "";
"Locations Manifest" = "";
"When partially watched video is played" = "";
"When partially watched video is played" = "Video kısmi olarak izlendiyse";
"Open Video" = "";
"Add Channels, Playlists and Searches to Favorites using" = "";
"Always" = "";
/* Video date filter in search */
"Year" = "";
"Year" = "Yıl";
"Playback queue is empty" = "";
"Show Favorites" = "";
"Show Favorites" = "Favorileri Göster";
"Driver" = "";
"Show progress of watching on thumbnails" = "";
"Show progress of watching on thumbnails" = "İzlenme durumu görsellerde görünsün";
"Left" = "";
"URL to Open" = "";
"Subscribe" = "";
"Yattee" = "";
"Show Documents" = "";
"Subscribe" = "Üye ol";
"Yattee" = "Yattee";
"Show Documents" = "Belgeleri Göster";
"Press and hold remote button to open captions and quality menus" = "";
"No locations available at the moment" = "";
"Show account username" = "";
"Used to create links from videos, channels and playlists" = "";
"Show account username" = "Hesabın kullanıcı adını göster";
"Used to create links from videos, channels and playlists" = "Videolardan, kanallardan ve oynatma listelerinden bağlantılar oluşturmak için kullanılır";
"Size" = "";
"You have no playlists\n\nTap on \"New Playlist\" to create one" = "";
"Sort: %@" = "";
"Select location closest to you:" = "";
"You have no playlists\n\nTap on \"New Playlist\" to create one" = "Çalma listeniz yok\n\nBir çalma listesi oluşturmak için \"Yeni Çalma Listesi\" üzerine dokunun";
"Sort: %@" = "Sırala: %@";
"Select location closest to you:" = "Size en yakın konumu seçin:";
"Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "";
/* Video duration filter in search */
"Short" = "";
"Home" = "";
"Short" = "Kısa";
"Home" = "Ana Sayfa";
"Remove Location" = "";
"Edit Favorites…" = "";
"Show Open Videos toolbar button" = "";
"Edit Favorites…" = "Favorileri düzenle…";
"Show Open Videos toolbar button" = "Video Aç arayüzünü göster";
"Sample Rate" = "";
"Private" = "";
"Browsing" = "Gezinti";
@@ -347,10 +347,10 @@
"Current Playlist" = "";
"Center" = "";
"Address" = "";
"Video actions buttons" = "";
"Video actions buttons" = "Video eylem butonları";
"Keep last played video in the queue after restart" = "";
"Remove…" = "";
"Trending" = "";
"Trending" = "Trendler";
"Statistics" = "";
"Copy%@link" = "";
"Now Playing" = "";
@@ -358,111 +358,111 @@
"No comments" = "";
"Could not open Files" = "";
"You need to select an account\nto access %@ section" = "";
"Reload manifest" = "";
"Reload manifest" = "Yeniden Yükle";
"Could not refresh Subscriptions" = "";
/* Subscriptions title */
"Subscriptions" = "";
"Upload date" = "";
"This information will be processed only on your device and used to connect you to the server in the specified country." = "";
"Shuffle" = "";
"Buttons labels" = "";
"Share %@ link" = "";
"Subscriptions" = "Üyelik";
"Upload date" = "Yüklenme tarihi";
"This information will be processed only on your device and used to connect you to the server in the specified country." = "Bu bilgiler yalnızca cihazınızda işlenecek ve sizi belirtilen ülkedeki sunucuya bağlamak için kullanılacaktır.";
"Shuffle" = "Karıştır";
"Buttons labels" = "Eylem düğmeleri etiketi";
"Share %@ link" = "%@ bağlantısını paylaş";
"Could not load streams" = "";
"Playback history is empty" = "";
"Show icons and text when space permits" = "";
"unknown" = "";
"Share..." = "";
"unknown" = "Bilinmeyen";
"Share..." = "Paylaş...";
/* Video sort order in search */
"Views" = "";
"Views" = "İzlenme";
"You need to create an instance and accounts\nto access %@ section" = "";
"Verified" = "";
"Open Files" = "";
"Could not refresh Playlists" = "";
"Could not refresh Playlists" = "Çalma listesi güncellenemedi";
"Actions buttons" = "";
"Any format" = "";
"Show playback statistics" = "";
"Pages buttons" = "";
"Videos" = "";
"Show playback statistics" = "Oynatma istatistiklerini göster";
"Pages buttons" = "Sayfa butonları";
"Videos" = "Videolar";
"Codec" = "";
"Comments are disabled" = "";
"Audio" = "";
"Public" = "";
"Files" = "";
"Show Home" = "";
"Open" = "";
"Files" = "Dosyalar";
"Show Home" = "Ana Sayfayı Göster";
"Open" = "";
/* Loading stream OSD */
"Opening %@ stream..." = "";
"Clear Queue before opening" = "";
"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" = "";
"Show Inspector" = "Denetleyiciyi Göster";
"Make default" = "";
"Are you sure you want to remove %@ location?" = "";
"No chapters information available" = "";
"Share Logs..." = "";
"Enter link to open" = "";
"No documents" = "";
"Inspector visibility" = "";
"No documents" = "Belge yok";
"Inspector visibility" = "Denetleyici görünümü";
"Could not update your token." = "";
"Could not find any links to open in your clipboard" = "";
/* Video date filter in search */
"Week" = "";
"Sidebar" = "";
"Week" = "Hafta";
"Sidebar" = "Kenar çubuğu";
"Show only icons" = "";
"Current: %@\n%@" = "Şuan: %@\n%@";
"Show anonymous accounts" = "";
"Could not open playlist" = "";
"Show anonymous accounts" = "Anonim hesapları göster";
"Could not open playlist" = "Çalma listesi açılamadı";
"Round corners" = "";
"URL" = "";
"URL" = "URL";
"Recents" = "";
"Show sidebar when space permits" = "";
"System controls buttons" = "";
"Show sidebar when space permits" = "Alan olduğu sürece kenar çubuğunu göster";
"System controls buttons" = "Sistem kontrol butonları";
"Could not extract channel information" = "";
"Public Locations" = "";
"You can find information about using Yattee in the Wiki pages." = "";
"You can find information about using Yattee in the Wiki pages." = "Wiki sayfalarında Yattee kullanımı hakkında bilgilere erişebilirsiniz.";
/* Player controls layout size */
"Very Large" = "";
"Very Large" = "Çok Büyük";
"Continue from %@" = "% devam et";
"Copy %@ link" = "Bağlantıyı %@ kopyala";
"Seek gesture sensitivity" = "";
"Show keywords" = "";
"Wiki" = "";
"Username" = "";
"Could not extract video ID" = "";
"Seek gesture sensitivity" = "Kaydırma hassasiyeti";
"Show keywords" = "Anahtar kelimeleri göster";
"Wiki" = "Wiki";
"Username" = "Kullanıcı adı";
"Could not extract video ID" = "Video ID bilgisi alınamadı";
/* Player controls layout size */
"Smaller" = "";
"Sort" = "";
"This cannot be reverted" = "";
"Smaller" = "Küçült";
"Sort" = "Sırala";
"This cannot be reverted" = "Geriye alınamaz";
"Public Manifest" = "";
"You have no Playlists" = "";
"Watched" = "";
"You have no Playlists" = "Çalma listeniz bulunmamaktadır";
"Watched" = "İzlendi";
"Could not open video" = "";
"Channel could not be found" = "";
"Show video length" = "";
"Source" = "";
"Welcome" = "";
"Wi-Fi" = "";
"Show video length" = "Video uzunluğunu göster";
"Source" = "Kaynak";
"Welcome" = "Hoşgeldiniz";
"Wi-Fi" = "Wi-Fi";
"Could not open channel" = "";
"This video could not be opened" = "";
"Could not extract playlist ID" = "";
"Could not load video" = "";
"This video could not be opened" = "Bu video oynatılamadı";
"Could not extract playlist ID" = "Çalma listesi ID bilgisi alınamadı";
"Could not load video" = "Video yüklenemedi";
"Decreased opacity" = "Düşük şeffaflık";
"Shuffle All" = "";
"Share %@ link with time" = "";
"This cannot be reverted. You might need to switch between views or restart the app to see changes." = "";
"Unsubscribe" = "";
"Shuffle All" = "Tümünü karıştır";
"Share %@ link with time" = "%@ bağlantısını zaman bilgisiyle birlikte paylaş";
"This cannot be reverted. You might need to switch between views or restart the app to see changes." = "Bu geri döndürülemez. Değişiklikleri görmek için görünümler arasında geçiş yapmanız veya uygulamayı yeniden başlatmanız gerekebilir.";
"Unsubscribe" = "Abonelikten çık";
"Current Location" = "";
"Stream & Player" = "";
"Hardware decoder" = "";
"Hardware decoder" = "Donanımsal çözücü";
"Honor orientation lock" = "";
"Seek with horizontal swipe on video" = "";
"Seek with horizontal swipe on video" = "Video üzerinde yatay kaydırma";
"Segments typically found at the start of a video that include an animation, still frame or clip which are also seen in other videos by the same creator." = "";
"Switch to public locations" = "";
"%@ formats" = "";
@@ -473,32 +473,35 @@
"Self-promotion" = "";
/* Player controls layout size */
"Small" = "";
"Small" = "Küçük";
/* SponsorBlock category name */
"Sponsor" = "";
"Sponsor" = "Sponsor";
"System controls show buttons for %@" = "";
"Show history" = "";
"Show history" = "Kullanım geçmişini göster";
"Could not extract SID from received cookies: %@" = "";
"This will remove all your custom profiles and return their default values. This cannot be reverted." = "";
"This will remove all your custom profiles and return their default values. This cannot be reverted." = "Bu eylem tüm kişiselleştirilmiş ayarlarınızı kaldıracak ve varsayılan ayarları geri getirecektir. Bu işlem geri döndürülemez.";
"Switch to other public location" = "";
"SponsorBlock" = "";
"Seek gesture speed" = "";
"If you want this app to be available in your language, join translation project." = "";
"Seek gesture speed" = "Kaydırma hızı";
"If you want this app to be available in your language, join translation project." = "Uygulamanın kendi dilinize çevrilmesini istiyorsanız, çeviri projesine katılın.";
"Could not refresh Trending" = "";
/* Video date filter in search */
"Today" = "";
"Today" = "Bugün";
"Shorts" = "";
"Channel" = "";
"Share files from Finder on a Mac\nor iTunes on Windows" = "";
"Share files from Finder on a Mac\nor iTunes on Windows" = "Finder üzerinden Mac ile belge paylaşın\nveya iTunes üzerinden Windows ile";
"\"%@\" will be irreversibly removed from this device." = "";
"Recent Documents" = "";
"Recent History" = "";
"Show Open Videos quick actions" = "";
"Recent Documents" = "Son kullanılan belgeler";
"Recent History" = "Yakın zamanda izlenilenler";
"Show Open Videos quick actions" = "Video Aç hızlı eylemlerini göster";
"Pages toolbar position" = "";
"Video" = "";
"Channels" = "";
"Share" = "";
"File" = "";
"Share%@link" = "";
"Cache" = "Önbellek";
"Enter account credentials to connect..." = "Bağlanmak için hesap bilgilerini girin...";
"Show scroll to top button in comments" = "Yorumlarda yukarıya götür düğmesini göster";

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
{
"originHash" : "9899ef48b3ee49eae175e25421b8330438e40c30a266d96473b299a6ab7c4188",
"pins" : [
{
"identity" : "activelabel.swift",
@@ -32,8 +33,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/sindresorhus/Defaults",
"state" : {
"revision" : "981ccb0a01c54abbe3c12ccb8226108527bbf115",
"version" : "6.3.0"
"revision" : "3efef5a28ebdbbe922d4a2049493733ed14475a6",
"version" : "7.3.1"
}
},
{
@@ -59,8 +60,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/cxfksword/MPVKit.git",
"state" : {
"branch" : "main",
"revision" : "dca1e345a26d09a3d621d7656a94e6427f3f7b83"
"revision" : "96825b3dc2b38e5550268156148d47798ce6aa74",
"version" : "0.36.0"
}
},
{
@@ -86,8 +87,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/ashleymills/Reachability.swift",
"state" : {
"revision" : "c01bbdf2d633cf049ae1ed1a68a2020a8bda32e2",
"version" : "5.1.0"
"revision" : "c01127cb51f591045696128effe43c16840d08bf",
"version" : "5.2.0"
}
},
{
@@ -105,7 +106,7 @@
"location" : "https://github.com/SDWebImage/SDWebImage",
"state" : {
"branch" : "master",
"revision" : "191b94cd545274603fcffce275dd4c1247587b6e"
"revision" : "a41be90abd89b125cd7588f20b9788108254091a"
}
},
{
@@ -122,8 +123,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImageSwiftUI.git",
"state" : {
"revision" : "5f025e54a03d3d33dff24d7a19331f446f00ed9d",
"version" : "2.2.4"
"revision" : "261b6cec35686d2dc192b809ab50742b4502a73b",
"version" : "2.2.6"
}
},
{
@@ -131,8 +132,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImageWebPCoder.git",
"state" : {
"revision" : "db4603921b31a6ce0f8c26d36d6a3fffc2dba481",
"version" : "0.14.2"
"revision" : "acfb824ca5cd9dbde2c43dc6b5a008c6757dee85",
"version" : "0.14.3"
}
},
{

View File

@@ -34,6 +34,16 @@
<string>public.file-url</string>
</array>
</dict>
<dict>
<key>CFBundleTypeName</key>
<string>Settings text</string>
<key>LSHandlerRank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>public.json</string>
</array>
</dict>
</array>
<key>CFBundleURLTypes</key>
<array>
@@ -68,5 +78,31 @@
</array>
<key>UIFileSharingEnabled</key>
<true/>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.json</string>
</array>
<key>UTTypeDescription</key>
<string>Yattee Settings</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>stream.yattee.app-settings</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>yatteesettings</string>
</array>
<key>public.mime-type</key>
<array>
<string>application/json</string>
</array>
</dict>
</dict>
</array>
</dict>
</plist>

View File

@@ -3,7 +3,7 @@ import Defaults
import Logging
import UIKit
struct Orientation {
enum Orientation {
static var logger = Logger(label: "stream.yattee.orientation")
static func lockOrientation(_ orientation: UIInterfaceOrientationMask) {

View File

@@ -86,9 +86,11 @@ public class OrientationTracker {
if accelerometerData.acceleration.y <= -threshold {
return .portrait
}
if accelerometerData.acceleration.y >= threshold {
if UIDevice.current.userInterfaceIdiom == .pad && accelerometerData.acceleration.y >= threshold {
return .portraitUpsideDown
}
return currentDeviceOrientation
}
}

View File

@@ -16,6 +16,18 @@
<string>public.mpeg-4</string>
</array>
</dict>
<dict>
<key>CFBundleTypeName</key>
<string>Settings</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>public.json</string>
</array>
</dict>
</array>
<key>CFBundleURLTypes</key>
<array>
@@ -37,5 +49,51 @@
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.json</string>
</array>
<key>UTTypeDescription</key>
<string>Yattee Settings</string>
<key>UTTypeIcons</key>
<dict/>
<key>UTTypeIdentifier</key>
<string>stream.yattee.app-settings</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>yatteesettings</string>
</array>
<key>public.mime-type</key>
<array/>
</dict>
</dict>
</array>
<key>UTImportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.json</string>
</array>
<key>UTTypeDescription</key>
<string>Yattee Settings</string>
<key>UTTypeIcons</key>
<dict/>
<key>UTTypeIdentifier</key>
<string>stream.yattee.app-settings</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>yatteesettings</string>
</array>
</dict>
</dict>
</array>
</dict>
</plist>

View File

@@ -16,7 +16,7 @@ final class PlayerViewController: NSViewController {
return [ratio, 1.0].max()!
}
override func viewDidDisappear() {
func viewDidDisappear() {
super.viewDidDisappear()
}

View File

@@ -1,6 +1,6 @@
import Foundation
struct Power {
enum Power {
static var hasInternalBattery: Bool {
let psInfo = IOPSCopyPowerSourcesInfo().takeRetainedValue()
let psList = IOPSCopyPowerSourcesList(psInfo).takeRetainedValue() as [CFTypeRef]

View File

@@ -130,7 +130,7 @@ struct NowPlayingView: View {
} else {
Section(header: Text("Chapters")) {
ForEach(video.chapters) { chapter in
ChapterView(chapter: chapter)
ChapterViewTVOS(chapter: chapter)
.padding(.horizontal, 40)
}
}