Compare commits

...

101 Commits

Author SHA1 Message Date
Arkadiusz Fal
d1cf45c6a1 Bump build number to 181 2024-04-02 23:14:51 +02:00
Arkadiusz Fal
07f3d841b3 Update CHANGELOG 2024-04-02 23:14:18 +02:00
Arkadiusz Fal
b488f86160 Downgrade MPVKit to 0.36.0-1
commit dca1e345a26d09a3d621d7656a94e6427f3f7b83
2024-04-02 23:14:18 +02:00
Arkadiusz Fal
e64c3a3c77 Update packages 2024-04-02 23:14:17 +02:00
Arkadiusz Fal
576a993faf Merge pull request #631 from stonerl/comments-piped
hopefully fixes #629
2024-04-02 22:51:07 +02:00
Toni Förster
c77c5a6d21 don't add empty comments 2024-04-02 15:08:36 +02:00
Toni Förster
ae16680fc2 removed some unnecessary print() 2024-04-02 14:28:06 +02:00
Toni Förster
807c0a1e2e hopefully fixes #629
should also get rid of empty comments if they couldn't be loaded
2024-04-02 14:28:06 +02:00
Arkadiusz Fal
96a2119a05 Merge pull request #632 from stonerl/invidious-html-comments
iv: use html comments instead of plain text
2024-04-01 23:12:12 +02:00
Arkadiusz Fal
7e940d6304 Merge pull request #624 from weblate/weblate-yattee-localizable-strings
Translations update from Hosted Weblate
2024-04-01 22:59:05 +02:00
mere
11402cc2a6 Translated using Weblate (Romanian)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ro/
2024-04-01 22:58:48 +02:00
floe nele
975d8b0ba0 Translated using Weblate (Dutch)
Currently translated at 44.3% (249 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/nl/
2024-04-01 22:58:48 +02:00
jonnysemon
e349898d9e Translated using Weblate (Arabic)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ar/
2024-04-01 22:58:48 +02:00
rexcsk
a8802da5a7 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/zh_Hant/
2024-04-01 22:58:48 +02:00
maboroshin
19993dfc04 Translated using Weblate (Japanese)
Currently translated at 98.3% (553 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ja/
2024-04-01 22:58:48 +02:00
joaooliva
e99dd442e1 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/pt_BR/
2024-04-01 22:58:48 +02:00
gallegonovato
d886113f27 Translated using Weblate (Spanish)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/es/
2024-04-01 22:58:48 +02:00
Ophiushi
ea9b759887 Translated using Weblate (French)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/fr/
2024-04-01 22:58:48 +02:00
rexcsk
0e784be231 Translated using Weblate (Chinese (Traditional))
Currently translated at 99.6% (560 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/zh_Hant/
2024-04-01 22:58:48 +02:00
rexcsk
d0ab73eeb2 Translated using Weblate (English)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/en/
2024-04-01 22:58:48 +02:00
Arkadiusz Fal
2193129818 Merge pull request #622 from rickykresslein/limit-recents-shown
Show/hide recents and limit number of recents shown
2024-04-01 22:58:42 +02:00
Toni Förster
f84c6d319a iv: use html comments instead of plain text
It now correctly displays emojis hyphens
2024-04-01 15:08:08 +02:00
Ricky Kresslein
87337f31a5 Updated importer and exporter to include new defaults 2024-02-28 18:35:03 +01:00
Arkadiusz Fal
cf5262a86e Bump build number to 180 2024-02-28 14:35:54 +01:00
Arkadiusz Fal
d6be0ffa5b Update CHANGELOG 2024-02-28 14:35:54 +01:00
Arkadiusz Fal
1df8241a01 Update packages 2024-02-28 14:35:54 +01:00
Arkadiusz Fal
43e5eae658 Use latest stable Xcode for build 2024-02-28 13:50:03 +01:00
Arkadiusz Fal
71b4560ff8 Merge pull request #623 from rickykresslein/help-text
Add help text to all header buttons
2024-02-28 13:38:33 +01:00
Arkadiusz Fal
f6bb2fe5d1 Merge pull request #621 from weblate/weblate-yattee-localizable-strings
Translations update from Hosted Weblate
2024-02-28 13:38:00 +01:00
rexcsk
272aafe504 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/zh_Hant/
2024-02-28 13:36:29 +01:00
rexcsk
580d782c56 Translated using Weblate (Chinese (Simplified))
Currently translated at 93.9% (528 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/zh_Hans/
2024-02-28 13:36:25 +01:00
Ricky Kresslein
238ddc7ad9 Add help text to all header buttons 2024-02-28 01:03:33 +01:00
Ricky Kresslein
5559e78bc0 Add setting to show/hide recents and limit number of recents shown 2024-02-28 00:56:12 +01:00
rexcsk
6cc38df4e9 Added translation using Weblate (Chinese (Traditional)) 2024-02-27 15:30:55 +01:00
Arkadiusz Fal
7b34c7e72b Bump build number to 179 2024-02-21 10:17:38 +01:00
Arkadiusz Fal
0dd7943849 Update CHANGELOG 2024-02-21 10:17:22 +01:00
Arkadiusz Fal
6745934a78 Update packages 2024-02-21 10:16:05 +01:00
Arkadiusz Fal
76801a34ee Merge pull request #616 from rickykresslein/main
Add skip, play/pause, and fullscreen shortcuts to macOS player
2024-02-21 10:11:57 +01:00
Arkadiusz Fal
4d0318d4b0 Merge pull request #612 from weblate/weblate-yattee-localizable-strings
Translations update from Hosted Weblate
2024-02-21 10:11:19 +01:00
Ricky Kresslein
9d4446a6ef Add skip, play/pause, and fullscreen shortcuts to macOS player 2024-02-17 10:40:27 +01:00
mere
b74017894c Translated using Weblate (Romanian)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ro/
2024-02-09 22:02:08 +01:00
Mohammed Al Otaibi
9fef6c0276 Translated using Weblate (Arabic)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ar/
2024-02-08 10:01:56 +01:00
gallegonovato
fcbeb45d1e Translated using Weblate (Spanish)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/es/
2024-02-08 10:01:55 +01:00
maboroshin
66f7286cdc Translated using Weblate (Japanese)
Currently translated at 98.5% (554 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ja/
2024-02-06 11:01:58 +01:00
jonnysemon
e1e068ba11 Translated using Weblate (Arabic)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ar/
2024-02-06 11:01:58 +01:00
Ophiushi
524c99dd54 Translated using Weblate (French)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/fr/
2024-02-06 11:01:58 +01:00
joaooliva
b57ed7055c Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/pt_BR/
2024-02-04 22:01:56 +01:00
Arkadiusz Fal
d84d701b07 Translated using Weblate (Polish)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/pl/
2024-02-04 22:01:56 +01:00
Arkadiusz Fal
bcfd4126b6 Translated using Weblate (English)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/en/
2024-02-04 22:01:56 +01:00
Arkadiusz Fal
97b16cfd04 Fix Package.resolved 2024-02-03 22:02:14 +01:00
Arkadiusz Fal
d5b81ceba1 Use macOS 13 in release workflow 2024-02-03 22:00:00 +01:00
Arkadiusz Fal
f3ba61a168 Bump build number to 178 2024-02-03 21:55:29 +01:00
Arkadiusz Fal
c68aa1d30c Update CHANGELOG 2024-02-03 21:55:17 +01:00
Arkadiusz Fal
d187fc322c Update packages 2024-02-03 21:55:09 +01:00
Arkadiusz Fal
e616022278 Use Xcode 14.3.1 for fastlane builds 2024-02-03 21:49:45 +01:00
Arkadiusz Fal
1b0486df05 Localizations improvements 2024-02-03 21:49:45 +01:00
Arkadiusz Fal
e6deb9ef26 Add import on tvOS, other export/import improvements 2024-02-03 21:49:45 +01:00
Arkadiusz Fal
0216c17b95 Merge pull request #610 from weblate/weblate-yattee-localizable-strings
Translations update from Hosted Weblate
2024-02-03 21:47:22 +01:00
Arkadiusz Fal
1eff757caf Translated using Weblate (Polish)
Currently translated at 100.0% (561 of 561 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/pl/
2024-02-03 21:47:09 +01:00
Arkadiusz Fal
4cfd00b307 Translated using Weblate (English)
Currently translated at 100.0% (561 of 561 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/en/
2024-02-03 21:47:09 +01:00
Arkadiusz Fal
8075db3ac8 Merge pull request #609 from weblate/weblate-yattee-localizable-strings
Translations update from Hosted Weblate
2024-02-03 21:30:20 +01:00
Arkadiusz Fal
2cd867e344 Translated using Weblate (Polish)
Currently translated at 100.0% (560 of 560 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/pl/
2024-02-03 21:30:07 +01:00
Arkadiusz Fal
b5b2e7f13d Translated using Weblate (English)
Currently translated at 100.0% (560 of 560 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/en/
2024-02-03 21:30:07 +01:00
Arkadiusz Fal
cbd7c417d2 Merge pull request #608 from weblate/weblate-yattee-localizable-strings
Translations update from Hosted Weblate
2024-02-03 21:21:11 +01:00
Arkadiusz Fal
ed7a233c9b Translated using Weblate (Polish)
Currently translated at 100.0% (554 of 554 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/pl/
2024-02-03 21:20:49 +01:00
Arkadiusz Fal
d75e3e9a61 Translated using Weblate (English)
Currently translated at 100.0% (554 of 554 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/en/
2024-02-03 21:20:49 +01:00
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
86 changed files with 4526 additions and 530 deletions

View File

@@ -27,7 +27,7 @@ jobs:
# lane: ['mac beta', 'ios beta', 'tvos beta']
lane: ['ios beta', 'tvos beta']
name: Releasing ${{ matrix.lane }} version to TestFlight
runs-on: macos-latest
runs-on: macos-13
steps:
- uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1
@@ -38,6 +38,9 @@ jobs:
run: |
sed -i '' 's/match Development/match AppStore/' Yattee.xcodeproj/project.pbxproj
sed -i '' 's/iPhone Developer/iPhone Distribution/' Yattee.xcodeproj/project.pbxproj
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- uses: maierj/fastlane-action@v3.0.0
with:
lane: ${{ matrix.lane }}
@@ -48,7 +51,7 @@ jobs:
if-no-files-found: ignore
mac_notarized:
name: Build and notarize macOS app
runs-on: macos-latest
runs-on: macos-13
steps:
- uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1
@@ -59,6 +62,9 @@ jobs:
run: |
sed -i '' 's/match AppStore/match Direct/' Yattee.xcodeproj/project.pbxproj
sed -i '' 's/3rd Party Mac Developer Application/Developer ID Application/' Yattee.xcodeproj/project.pbxproj
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- uses: maierj/fastlane-action@v3.0.0
with:
lane: mac build_and_notarize

View File

@@ -1,22 +1,25 @@
## Build 173
* Updated dependencies
## Build 181
* History Setting: hide the recent activity in the sidebar or limit the number of items shown (by @rickykresslein)
* Fix issues with empty comments (by @stonerl)
* Improved Invidious comments (by @stonerl)
* Downgrade MPVKit to 0.36.0-1 due to issues with WebVTT subtitles
* Updated localizations
* Fixed reported crashes
* Other minor changes and improvements
* Updated dependencies
## Previous builds
* 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
* Collapsible chapters view, player setting "Open vertical chapters expanded"
* Current chapter is highlighted
* Disabled portrait upside down orientation on iPhone
* Fixed issue with handling private Invidious instances requests
* Fixed issue where Piped login token would not refresh
* Fixed issue with MPV subtitles not working
* Added Persian, Spanish, Turkish and Russian localizations
* Fixed issue with displaying account username
* Add skip, play/pause, and fullscreen shortcuts to macOS player (by @rickykresslein)
* Added Settings Import/Export
* Export all settings, instances and accounts
* Import selected elements from the file
* Include unencrypted passwords in the export or provide them during the import
* Import via URL for tvOS
* Added Controls setting "Action button labels" icon or icon and text
* Added Advanced setting for MPV: "deinterlace"
* Add help text to all header buttons (by @rickykresslein)
* Add Chinese (Traditional) localization (by @rexcsk)
* Localization fixes
* Updated localizations
* Fixed reported crash
* Other minor changes and improvements
**Big thanks to the past, current and future project contributors!**
**Big thanks to the current, past 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,29 +1,79 @@
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)
CFPropertyList (3.0.7)
base64
nkf
rexml
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
artifactory (3.0.15)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.877.0)
aws-sdk-core (3.190.1)
aws-partitions (1.906.0)
aws-sdk-core (3.191.6)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.76.0)
aws-sdk-core (~> 3, >= 3.188.0)
aws-sdk-kms (1.78.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.142.0)
aws-sdk-core (~> 3, >= 3.189.0)
aws-sdk-s3 (1.146.1)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8)
aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
@@ -35,7 +85,7 @@ GEM
domain_name (0.6.20240107)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.109.0)
excon (0.110.0)
faraday (1.10.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
@@ -64,52 +114,11 @@ GEM
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.3.0)
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
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)
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.1)
gh_inspector (1.1.3)
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)
@@ -117,19 +126,18 @@ 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.31.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.6.1)
google-cloud-core (1.7.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.3.1)
google-cloud-errors (1.4.0)
google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
@@ -150,18 +158,20 @@ GEM
httpclient (2.8.3)
jmespath (1.6.2)
json (2.7.1)
jwt (2.7.1)
jwt (2.8.1)
base64
mini_magick (4.12.0)
mini_mime (1.1.5)
multi_json (1.15.0)
multipart-post (2.3.0)
multipart-post (2.4.0)
nanaimo (0.3.0)
naturally (2.2.1)
nkf (0.2.0)
optparse (0.4.0)
os (1.1.4)
plist (3.7.1)
public_suffix (5.0.4)
rake (13.1.0)
public_suffix (5.0.5)
rake (13.2.0)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
@@ -172,7 +182,7 @@ GEM
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
signet (0.18.0)
signet (0.19.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
@@ -190,9 +200,8 @@ GEM
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)
@@ -211,7 +220,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

@@ -123,7 +123,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
content.json.dictionaryValue["videos"]?.arrayValue.map(self.extractVideo) ?? []
}
["latest", "playlists", "streams", "shorts", "channels", "videos", "releases", "podcasts"].forEach { type in
for type in ["latest", "playlists", "streams", "shorts", "channels", "videos", "releases", "podcasts"] {
configureTransformer(pathPattern("channels/*/\(type)"), requestMethods: [.get]) { (content: Entity<JSON>) -> ChannelPage in
self.extractChannelPage(from: content.json)
}
@@ -691,6 +691,8 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
let author = details["author"]?.string ?? ""
let channelId = details["authorId"]?.string ?? UUID().uuidString
let authorAvatarURL = details["authorThumbnails"]?.arrayValue.last?.dictionaryValue["url"]?.string ?? ""
let htmlContent = details["contentHtml"]?.string ?? ""
let decodedContent = decodeHtml(htmlContent)
return Comment(
id: UUID().uuidString,
author: author,
@@ -699,12 +701,25 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
pinned: false,
hearted: false,
likeCount: details["likeCount"]?.int ?? 0,
text: details["content"]?.string ?? "",
text: decodedContent,
repliesPage: details["replies"]?.dictionaryValue["continuation"]?.string,
channel: Channel(app: .invidious, id: channelId, name: author)
)
}
private func decodeHtml(_ htmlEncodedString: String) -> String {
if let data = htmlEncodedString.data(using: .utf8) {
let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [
.documentType: NSAttributedString.DocumentType.html,
.characterEncoding: String.Encoding.utf8.rawValue
]
if let attributedString = try? NSAttributedString(data: data, options: options, documentAttributes: nil) {
return attributedString.string
}
}
return htmlEncodedString
}
private func extractCaptions(from content: JSON) -> [Captions] {
content["captions"].arrayValue.compactMap { details in
guard let url = URL(string: details["url"].stringValue, relativeTo: account.url) else { return nil }

View File

@@ -113,8 +113,11 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
content.json.arrayValue.compactMap { self.extractVideo(from: $0) }
}
configureTransformer(pathPattern("comments/*")) { (content: Entity<JSON>) -> CommentsPage in
let details = content.json.dictionaryValue
configureTransformer(pathPattern("comments/*")) { (content: Entity<JSON>?) -> CommentsPage in
guard let details = content?.json.dictionaryValue else {
return CommentsPage(comments: [], nextPage: nil, disabled: true)
}
let comments = details["comments"]?.arrayValue.compactMap { self.extractComment(from: $0) } ?? []
let nextPage = details["nextpage"]?.string
let disabled = details["disabled"]?.bool ?? false
@@ -154,7 +157,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
}
@@ -662,16 +666,16 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
let videoStreams = content.dictionaryValue["videoStreams"]?.arrayValue ?? []
videoStreams.forEach { videoStream in
for videoStream in videoStreams {
let videoCodec = videoStream.dictionaryValue["codec"]?.string ?? ""
if Self.disallowedVideoCodecs.contains(where: videoCodec.contains) {
return
continue
}
guard let audioAssetUrl = audioStream.dictionaryValue["url"]?.url,
let videoAssetUrl = videoStream.dictionaryValue["url"]?.url
else {
return
continue
}
let audioAsset = AVURLAsset(url: audioAssetUrl)
@@ -723,15 +727,23 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
let commentorUrl = details["commentorUrl"]?.string
let channelId = commentorUrl?.components(separatedBy: "/")[2] ?? ""
let commentText = extractCommentText(from: details["commentText"]?.stringValue)
let commentId = details["commentId"]?.string ?? UUID().uuidString
// Sanity checks: return nil if required data is missing
if commentText.isEmpty || commentId.isEmpty || author.isEmpty {
return nil
}
return Comment(
id: details["commentId"]?.string ?? UUID().uuidString,
id: commentId,
author: author,
authorAvatarURL: details["thumbnail"]?.string ?? "",
time: details["commentedTime"]?.string ?? "",
pinned: details["pinned"]?.bool ?? false,
hearted: details["hearted"]?.bool ?? false,
likeCount: details["likeCount"]?.int ?? 0,
text: extractCommentText(from: details["commentText"]?.stringValue),
text: commentText,
repliesPage: details["repliesPage"]?.string,
channel: Channel(app: .piped, id: channelId, name: author)
)

View File

@@ -35,26 +35,22 @@ final class CommentsModel: ObservableObject {
func load(page: String? = nil) {
guard let video = player.currentVideo else { return }
if !firstPage && !nextPageAvailable {
return
}
firstPage = page.isNil || page!.isEmpty
guard firstPage || nextPageAvailable else { return }
player
.playerAPI(video)?
.comments(video.videoID, page: page)?
.load()
.onSuccess { [weak self] response in
if let page: CommentsPage = response.typedContent() {
self?.all += page.comments
self?.nextPage = page.nextPage
self?.disabled = page.disabled
guard let self = self else { return }
if let commentsPage: CommentsPage = response.typedContent() {
self.all += commentsPage.comments
self.nextPage = commentsPage.nextPage
self.disabled = commentsPage.disabled
}
}
.onFailure { [weak self] requestError in
self?.disabled = !requestError.json.dictionaryValue["error"].isNil
.onFailure { [weak self] _ in
self?.disabled = true
}
.onCompletion { [weak self] _ in
self?.loaded = true

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,25 @@
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],
"showRecents": Defaults[.showRecents],
"limitRecents": Defaults[.limitRecents],
"limitRecentsAmount": Defaults[.limitRecentsAmount]
]
}
}

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,150 @@
import Defaults
import Foundation
import SwiftyJSON
final class ImportSettingsFileModel: ObservableObject {
static let shared = ImportSettingsFileModel()
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
var loadTask: URLSessionTask?
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()
}
}
@Published var json = JSON()
func loadData(_ url: URL) {
json = JSON()
loadTask?.cancel()
loadTask = URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in
guard let data else { return }
if let json = try? JSON(data: data) {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.json = json
self.sheetViewModel.reset(locationsSettingsGroupImporter)
self.importExportModel.reset(self)
}
}
}
loadTask?.resume()
}
func filename(_ url: URL) -> String {
String(url.lastPathComponent.dropLast(ImportExportSettingsModel.settingsExtension.count + 1))
}
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,66 @@
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
}
if let showRecents = json["showRecents"].bool {
Defaults[.showRecents] = showRecents
}
if let limitRecents = json["limitRecents"].bool {
Defaults[.limitRecents] = limitRecents
}
if let limitRecentsAmount = json["limitRecentsAmount"].int {
Defaults[.limitRecentsAmount] = limitRecentsAmount
}
}
}

View File

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

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

@@ -176,6 +176,11 @@ final class PlayerModel: ObservableObject {
@Default(.resetWatchedStatusOnPlaying) var resetWatchedStatusOnPlaying
@Default(.playerRate) var playerRate
@Default(.systemControlsSeekDuration) var systemControlsSeekDuration
#if os(macOS)
@Default(.buttonBackwardSeekDuration) private var buttonBackwardSeekDuration
@Default(.buttonForwardSeekDuration) private var buttonForwardSeekDuration
#endif
#if !os(macOS)
@Default(.closePiPAndOpenPlayerOnEnteringForeground) var closePiPAndOpenPlayerOnEnteringForeground
@@ -187,6 +192,10 @@ final class PlayerModel: ObservableObject {
var onPlayStream = [(Stream) -> Void]()
var rateToRestore: Float?
private var remoteCommandCenterConfigured = false
#if os(macOS)
var keyPressMonitor: Any?
#endif
init() {
#if !os(macOS)
@@ -212,6 +221,7 @@ final class PlayerModel: ObservableObject {
#if os(macOS)
if presentingPlayer {
Windows.player.focus()
assignKeyPressMonitor()
return
}
#endif
@@ -227,6 +237,7 @@ final class PlayerModel: ObservableObject {
#if os(macOS)
Windows.player.open()
Windows.player.focus()
assignKeyPressMonitor()
#endif
}
@@ -246,6 +257,7 @@ final class PlayerModel: ObservableObject {
}
#if os(macOS)
destroyKeyPressMonitor()
Windows.player.hide()
#endif
}
@@ -1146,4 +1158,46 @@ final class PlayerModel: ObservableObject {
return nil
}
#if os(macOS)
private func assignKeyPressMonitor() {
keyPressMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { keyEvent -> NSEvent? in
switch keyEvent.keyCode {
case 124:
if !self.liveStreamInAVPlayer {
let interval = TimeInterval(self.buttonForwardSeekDuration) ?? 10
self.backend.seek(
relative: .secondsInDefaultTimescale(interval),
seekType: .userInteracted
)
}
case 123:
if !self.liveStreamInAVPlayer {
let interval = TimeInterval(self.buttonBackwardSeekDuration) ?? 10
self.backend.seek(
relative: .secondsInDefaultTimescale(-interval),
seekType: .userInteracted
)
}
case 3:
self.toggleFullscreen(
self.playingFullScreen,
showControls: false
)
case 49:
if !self.controls.isLoadingVideo {
self.backend.togglePlay()
}
default: return keyEvent
}
return nil
}
}
private func destroyKeyPressMonitor() {
if let keyPressMonitor = keyPressMonitor {
NSEvent.removeMonitor(keyPressMonitor)
}
}
#endif
}

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

@@ -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,70 @@ 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 showRecents = Key<Bool>("showRecents", default: true)
static let limitRecents = Key<Bool>("limitRecents", default: false)
static let limitRecentsAmount = Key<Int>("limitRecentsAmount", default: 10)
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,11 +286,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 expandChapters = Key<Bool>("expandChapters", 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 {
@@ -401,6 +435,15 @@ enum ButtonLabelStyle: String, CaseIterable, Defaults.Serializable {
var text: Bool {
self == .iconAndText
}
var description: String {
switch self {
case .iconOnly:
return "Icon only".localized()
case .iconAndText:
return "Icon and text".localized()
}
}
}
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

View File

@@ -5,12 +5,14 @@ struct AppSidebarRecents: View {
var recents = RecentsModel.shared
@Default(.recentlyOpened) private var recentItems
@Default(.limitRecents) private var limitRecents
@Default(.limitRecentsAmount) private var limitRecentsAmount
var body: some View {
Group {
if !recentItems.isEmpty {
Section(header: Text("Recents")) {
ForEach(recentItems.reversed()) { recent in
ForEach(recentItems.reversed().prefix(limitRecents ? limitRecentsAmount : recentItems.count)) { recent in
Group {
switch recent.type {
case .channel:

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

@@ -13,6 +13,7 @@ struct Sidebar: View {
@Default(.showDocuments) private var showDocuments
#endif
@Default(.showUnwatchedFeedBadges) private var showUnwatchedFeedBadges
@Default(.showRecents) private var showRecents
var body: some View {
ScrollViewReader { scrollView in
@@ -20,8 +21,10 @@ struct Sidebar: View {
mainNavigationLinks
if !accounts.isEmpty {
AppSidebarRecents()
.id("recentlyOpened")
if showRecents {
AppSidebarRecents()
.id("recentlyOpened")
}
if accounts.api.signedIn {
if visibleSections.contains(.subscriptions), accounts.app.supportsSubscriptions {

View File

@@ -21,6 +21,11 @@ struct OpenURLHandler {
return
}
if url.isFileURL, url.standardizedFileURL.absoluteString.hasSuffix(".\(ImportExportSettingsModel.settingsExtension)") {
navigation.presentSettingsImportSheet(url)
return
}
if accounts.current.isNil {
accounts.setCurrent(accounts.any)
}

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

@@ -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,168 @@
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
}
#if os(iOS)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
exportButton
}
}
#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.localized())
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)
}
}
}
.buttonStyle(.plain)
.disabled(model.isExportInProgress)
}
var exportButton: some View {
Button(action: exportSettings) {
Text(model.isExportInProgress ? "In progress..." : "Export")
.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

@@ -10,6 +10,9 @@ struct HistorySettings: View {
@Default(.saveRecents) private var saveRecents
@Default(.saveLastPlayed) private var saveLastPlayed
@Default(.saveHistory) private var saveHistory
@Default(.showRecents) private var showRecents
@Default(.limitRecents) private var limitRecents
@Default(.limitRecentsAmount) private var limitRecentsAmount
@Default(.showWatchingProgress) private var showWatchingProgress
@Default(.watchedThreshold) private var watchedThreshold
@Default(.watchedVideoStyle) private var watchedVideoStyle
@@ -56,6 +59,26 @@ struct HistorySettings: View {
Section(header: SettingsHeader(text: "History".localized())) {
Toggle("Save history of searches, channels and playlists", isOn: $saveRecents)
Toggle("Save history of played videos", isOn: $saveHistory)
Toggle("Show recents in sidebar", isOn: $showRecents)
#if os(macOS)
HStack {
Toggle("Limit recents shown", isOn: $limitRecents)
.frame(minWidth: 140, alignment: .leading)
.disabled(!showRecents)
Spacer()
counterButtons(for: $limitRecentsAmount)
.disabled(!limitRecents)
}
#else
Toggle("Limit recents shown", isOn: $limitRecents)
.disabled(!showRecents)
HStack {
Text("Recents shown")
Spacer()
counterButtons(for: $limitRecentsAmount)
.disabled(!limitRecents)
}
#endif
Toggle("Show progress of watching on thumbnails", isOn: $showWatchingProgress)
.disabled(!saveHistory)
Toggle("Keep last played video in the queue after restart", isOn: $saveLastPlayed)
@@ -169,6 +192,71 @@ struct HistorySettings: View {
.foregroundColor(.red)
}
}
private func counterButtons(for _value: Binding<Int>) -> some View {
var value: Binding<Int> {
Binding(
get: { return _value.wrappedValue },
set: {
if $0 < 1 {
_value.wrappedValue = 1
} else {
_value.wrappedValue = $0
}
}
)
}
return HStack {
#if !os(tvOS)
Label("Minus", systemImage: "minus")
.imageScale(.large)
.labelStyle(.iconOnly)
.padding(7)
.foregroundColor(limitRecents ? .accentColor : .gray)
.accessibilityAddTraits(.isButton)
#if os(iOS)
.frame(minHeight: 35)
.background(RoundedRectangle(cornerRadius: 4).strokeBorder(lineWidth: 1).foregroundColor(.accentColor))
#endif
.contentShape(Rectangle())
.onTapGesture {
value.wrappedValue -= 1
}
#endif
#if os(tvOS)
let textFieldWidth = 100.00
#else
let textFieldWidth = 30.00
#endif
TextField("Duration", value: value, formatter: NumberFormatter())
.frame(width: textFieldWidth, alignment: .trailing)
.multilineTextAlignment(.center)
.labelsHidden()
.foregroundColor(limitRecents ? .accentColor : .gray)
#if !os(macOS)
.keyboardType(.numberPad)
#endif
#if !os(tvOS)
Label("Plus", systemImage: "plus")
.imageScale(.large)
.labelStyle(.iconOnly)
.padding(7)
.foregroundColor(limitRecents ? .accentColor : .gray)
.accessibilityAddTraits(.isButton)
#if os(iOS)
.background(RoundedRectangle(cornerRadius: 4).strokeBorder(lineWidth: 1).foregroundColor(.accentColor))
#endif
.contentShape(Rectangle())
.onTapGesture {
value.wrappedValue += 1
}
#endif
}
}
}
struct HistorySettings_Previews: PreviewProvider {

View File

@@ -66,6 +66,7 @@ struct HomeSettings: View {
.font(.system(size: 30))
#endif
}
.help("Add to Favorites")
#if !os(tvOS)
.buttonStyle(.borderless)
#endif

View File

@@ -0,0 +1,34 @@
import SwiftUI
struct ImportSettings: View {
@State private var fileURL = ""
var body: some View {
VStack(spacing: 100) {
VStack(alignment: .leading, spacing: 20) {
Text("1. Export settings from Yattee for iOS or macOS")
Text("2. Upload it to a file hosting (e. g. Pastebin or GitHub Gist)")
Text("3. Enter file URL in the field below. You can use iOS remote to paste.")
}
TextField("URL", text: $fileURL)
Button {
if let url = URL(string: fileURL) {
NavigationModel.shared.presentSettingsImportSheet(url)
}
} label: {
Text("Import")
}
}
.padding(20)
.navigationTitle("Import Settings")
}
}
struct ImportSettings_Previews: PreviewProvider {
static var previews: some View {
ImportSettings()
}
}

View File

@@ -0,0 +1,198 @@
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 {
#if os(tvOS)
row
#else
Button(action: { model.toggleAccount(account, accounts: accounts) }) {
row
}
.buttonStyle(.plain)
#endif
}
var row: some View {
let accountExists = AccountsModel.shared.find(account.id) != nil
return 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)
}
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()
fileModel.loadData(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
@StateObject private var fileModel = ImportSettingsFileModel.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 settingsFile else { return }
fileModel.loadData(settingsFile)
}
.onChange(of: settingsFile) { _ in
guard let settingsFile else { return }
fileModel.loadData(settingsFile)
}
}
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 locationsSettingsGroupImporter: LocationsSettingsGroupImporter? {
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.localized())
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 settingsFile {
Section(header: Text("File information")) {
MetadataRow(name: Text("Name"), value: Text(fileModel.filename(settingsFile)))
if let date = fileModel.metadataDate {
MetadataRow(name: Text("Date"), value: Text(date))
#if os(tvOS)
.focusable()
#endif
}
if let build = fileModel.metadataBuild {
MetadataRow(name: Text("Build"), value: Text(build))
#if os(tvOS)
.focusable()
#endif
}
if let platform = fileModel.metadataPlatform {
MetadataRow(name: Text("Platform"), value: Text(platform))
#if os(tvOS)
.focusable()
#endif
}
}
}
}
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 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

@@ -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()
.modifier(ImportSettingsSheetViewModifier(isPresented: $settingsModel.presentingSettingsImportSheet, settingsFile: $settingsModel.settingsImportURL))
#if !os(tvOS)
.modifier(ImportSettingsFileImporterViewModifier(isPresented: $navigation.presentingSettingsFileImporter))
#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,43 @@ struct SettingsView: View {
}
#endif
var importView: some View {
Section {
#if os(tvOS)
NavigationLink(destination: LazyView(ImportSettings())) {
Label("Import Settings", systemImage: "square.and.arrow.down")
.labelStyle(SettingsLabel())
}
.padding(.horizontal, 20)
#else
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())
}
#endif
}
}
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 +327,9 @@ struct SettingsView: View {
case .locations:
return 600
case .advanced:
return 380
return 500
case .importExport:
return 580
case .help:
return 650
}

View File

@@ -113,6 +113,7 @@ struct SubscriptionsView: View {
} label: {
Label("Play all unwatched", systemImage: "play")
}
.help("Play all unwatched")
.disabled(!feed.canPlayUnwatchedFeed)
}
@@ -130,6 +131,7 @@ struct SubscriptionsView: View {
} label: {
Label("Mark all as watched", systemImage: "checkmark.circle.fill")
}
.help("Mark all as watched")
.disabled(!feed.canMarkAllFeedAsWatched)
}
@@ -139,6 +141,7 @@ struct SubscriptionsView: View {
} label: {
Label("Mark all as unwatched", systemImage: "checkmark.circle")
}
.help("Mark all as unwatched")
}
}

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

@@ -37,6 +37,7 @@ struct FavoriteButton: View {
.contentShape(Rectangle())
#endif
}
.help(isFavorite ? "Remove from Favorites" : "Add to Favorites")
.disabled(item.isNil)
.onAppear {
isFavorite = item.isNil ? false : favorites.contains(item)

View File

@@ -11,6 +11,7 @@ struct HomeSettingsButton: View {
}
.font(.caption)
.imageScale(.small)
.help("Home Settings")
}
}

View File

@@ -16,6 +16,7 @@ struct ListingStyleButtons: View {
.imageScale(.small)
#endif
}
.help(listingStyle == .cells ? "List" : "Cells")
#endif
}

View File

@@ -38,6 +38,7 @@ struct ShareButton<LabelView: View>: View {
label
}
.menuStyle(.borderlessButton)
.help("Share")
#if os(macOS)
.frame(maxWidth: 60)
#endif

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

@@ -3,12 +3,12 @@
"Add Account" = "إضافة حساب";
"Add Account..." = "إضافة حساب…";
"Add Location" = "إضافة موقع";
"Add Location..." = "إضافة موقع...";
"%@ Playlist" = "%@ قائمة تشغيل";
"%@ Channel" = "%@ قناة";
"%@ subscribers" = "%@ المتابعين";
"Add Location..." = "أضِف موقع..";
"%@ Playlist" = "قائمة تشغيل %@";
"%@ Channel" = "قناة %@";
"%@ subscribers" = "مشتركين %@";
"Add to %@" = "إضافة إلى %@";
"%lld videos" = "%Lld مقاطع الفيديو";
"%lld videos" = "مقاطع فيديو %Lld";
"Add profile..." = "إضافة ملف تعريف...";
"Add Quality Profile" = "إضافة ملف تعريف الجودة";
"Add to Playlist" = "إضافة إلى قائمة تشغيل";
@@ -23,23 +23,23 @@
"Automatic" = "تلقائي";
"Autoplaying Next" = "التشغيل التلقائي للتالي";
"Are you sure you want to unsubscribe from %@?" = "هل أنت متأكد من رغبتك في إلغاء الإشتراك من ٪@؟";
"Badge & Decreased opacity" = "شارة ونقص التعتيم";
"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 Search History" = "مسح سجل تاريخ البحث";
"Clear Search History..." = "مسح سجل تاريخ البحث…";
"Clear All" = "مسح الكل";
"Clear All Recents" = "مسح جميع الأخيرة";
"Close player when closing video" = "غلق المشغل عند غلق الفيديو";
@@ -47,14 +47,14 @@
"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" = "غلق الفيديو عند إنتهاء الفيديو الاخير فى قائمة الانتظار";
"Close video after playing last in the queue" = "غلق الفيديو عند إنتهاء الفيديو الأخير فى قائمة الإنتظار";
"Comments" = "التعليقات";
"Connection failed" = "فشل الاتصال";
"Continue" = "‏الإستمرار";
"Connected successfully (%@)" = "تم الاتصال بنجاح (%@)";
"Country" = "البلد";
"Country Name or Code" = "اسم الدولة أو الرمز";
"Copy %@ link" = "نسخ الرابط: %@";
"Copy %@ link" = "نسخ رابط %@";
"Contributing" = "المساهمة";
"Contact" = "التواصل";
"Continue from %@" = "الإستمرار من %@";
@@ -71,8 +71,8 @@
/* Video sort order in search */
"Date" = "تاريخ";
"Decrease rate" = "انخفاض معدل";
"Decreased opacity" = "انخفاض التعتيم";
"Decrease rate" = "إنخفاض معدل";
"Decreased opacity" = "إنخفاض التعتيم";
"Enable logging" = "تمكين التسجيل";
"Discord Server" = "خادم Discord";
"Discussions take place in Discord and Matrix. It's a good spot for general questions." = "تجري المناقشات في Discord و Matrix. إنه مكان جيد للأسئلة العامة.";
@@ -97,7 +97,7 @@
"Help" = "مساعدة";
"Hide sidebar" = "إخفاء الشريط الجانبي";
"Highest" = "‏الأعلى";
"Highest quality" = "أعلى جودة";
"Highest quality" = "جودة أعلى";
"History" = "سجل التاريخ";
/* Video date filter in search */
@@ -113,11 +113,11 @@
"Info" = "معلومات";
/* SponsorBlock category name */
"Interaction" = "تفاعل";
"Interface" = "واجهه المستخدم";
"Interaction" = "التفاعل";
"Interface" = "واجهة المستخدم";
/* SponsorBlock category name */
"Intro" = "مقدمة";
"Intro" = "المقدمة";
"Issues Tracker" = "تعقب المشاكل";
/* Selected video has just finished playing */
@@ -129,7 +129,7 @@
"LIVE" = "مباشر";
/* Loading stream OSD */
"Loading streams..." = "جارٍ تحميل بثوث ...";
"Loading streams..." = "تحميل بثوث...";
"Loading..." = "تحميل...";
/* Video duration filter in search */
@@ -138,8 +138,8 @@
"Low quality" = "جودة منخفضة";
"Lowest" = "‏الأدنى";
"Mark as watched" = "وضع علامة تمت المشاهدة";
"Mark video as watched after playing" = "ضع علامة على الفيديو تمت المشاهدة بعد التشغيل";
"Mark watched videos with" = "وضع علامة على مقاطع الفيديو التي تمت المشاهدة باستخدام";
"Mark video as watched after playing" = "وضع علامة تمت المشاهدة على الفيديو بعد التشغيل";
"Mark watched videos with" = "وضع علامة تمت المشاهدة على مقاطع الفيديو باستخدام";
"Matrix Channel" = "قناة Matrix";
"Matrix Chat" = "دردشة Matrix";
"More info can be found in:" = "يمكن العثور على مزيد من المعلومات في:";
@@ -163,11 +163,11 @@
"Open Settings" = "فتح الإعدادات";
/* Loading stream OSD */
"Opening %@ stream..." = "فتح %@ بث ...";
"Opening audio stream..." = "يتم فتح بث صوتي ...";
"Opening %@ stream..." = "فتح بث %@ ...";
"Opening audio stream..." = "فتح بث صوتي...";
"Orientation" = "اتجاه";
"Play in PiP" = "تشغيل في الفيديو المصغر";
"Play Last" = "تشغيل الاخير";
"Play Last" = "تشغيل الأخير";
"Play Music" = "تشغل الموسيقى";
"Play Next" = "تشغيل التالى";
"Play Now" = "تشغيل الآن";
@@ -176,16 +176,16 @@
"Playlist" = "قائمة تشغيل";
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "سيتم حذف قائمة تشغيل \"%@\".\nلا يمكن التراجع عنه.";
"Playlists" = "قوائم تشغيل";
"Popular" = "شعبي";
"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 Profile" = "ملف تعريف الجودة";
"Queue" = "قائمة الانتظار";
"Queue is empty" = "قائمة الانتظار فارغة";
"Queue" = "قائمة الإنتظار";
"Queue is empty" = "قائمة الإنتظار فارغة";
"Rate" = "معدل";
/* Video sort order in search */
@@ -203,19 +203,19 @@
"Remove from Favorites" = "إزالة من المفضلة";
"Remove from history" = "إزالة من سجل التاريخ";
"Remove from Playlist" = "إزالة من قائمة تشغيل";
"Remove from the queue" = "إزالة من قائمة الانتظار";
"Remove from the queue" = "إزالة من قائمة الإنتظار";
"Replies" = "الردود";
"Reset" = "إعادة تعيين";
"Reset search filters" = "إعادة تعيين عوامل تصفية البحث";
"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 history of searches, channels and playlists" = "حفظ سجل تاريخ عمليات البحث والقنوات وقوائم تشغيل";
"Search history is empty" = "سجل تاريخ البحث فارغ";
"Search..." = "بحث...";
"Sections" = "الأقسام";
"Select location closest to you:" = "حدد أقرب موقع إليك:";
@@ -224,7 +224,7 @@
"Self-promotion" = "الترويج الذاتي";
"Settings" = "الإعدادات";
"Share %@ link" = "مشاركة رابط %@";
"Share %@ link with time" = "مشاركة الرابط %@ مع الوقت";
"Share %@ link with time" = "مشاركة رابط %@ مع الوقت";
"Share..." = "‏مشاركة...";
/* Video duration filter in search */
@@ -243,22 +243,22 @@
/* Player controls layout size */
"Small" = "صغير";
"Sort" = "صنف";
"Sort" = "فرز";
"Sort: %@" = "فرز: %@";
"Source" = "المصدر";
/* SponsorBlock category name */
"Sponsor" = "راعي";
"Sponsor" = "الراعي";
"SponsorBlock" = "حظر الإعلانات";
"SponsorBlock API Instance" = "مثيل واجهة برمجة تطبيقات حظر الإعلانات";
"Subscribe" = "الإشتراك";
/* Subscriptions title */
"Subscriptions" = "الاشتراكات";
"Subscriptions" = "الإشتراكات";
"Switch to other public location" = "التبديل إلى موقع عام آخر";
"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" = "الرابط";
@@ -285,19 +285,19 @@
"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" = "غير مدرج";
"Now Playing" = "يعرض الآن";
"Playback queue is empty" = "قائمة انتظار التشغيل فارغة";
"Playback queue is empty" = "قائمة الإنتظار التشغيل فارغة";
"Playing Next" = "تشغيل التالي";
"Add Channels, Playlists and Searches to Favorites using" = "إضافة القنوات، قوائم تشغيل، عمليات البحث إلى المفضلة باستخدام";
"Make default" = "جعله افتراضي";
@@ -305,23 +305,23 @@
"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 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" = "تعذر فتح القناة";
@@ -349,7 +349,7 @@
"Show Inspector" = "إظهار المفتش";
"Reload manifest" = "إعادة تحميل البيان";
"Open" = "فتح";
"Video actions buttons" = "أزرار إجراءات الفيديو";
"Video actions buttons" = "أزرار إجراءات مقطع الفيديو";
"Open Files" = "فتح الملفات";
"Channels" = "القنوات";
"Channel" = "القناة";
@@ -365,29 +365,29 @@
"Remove…" = "‏إزالة…";
"Show sidebar" = "إظهار الشريط الجانبي";
"Remove Location" = "إزالة الموقع";
" subscribers" = " المتابعين";
" subscribers" = " المشتركين";
"Accounts" = "الحسابات";
"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?" = "هل أنت متأكد من أنك تريد مسح سجل البحث؟";
"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 clear history of watched videos?" = "هل أنت متأكد من أنك تريد مسح سجل تاريخ مقاطع الفيديو التي تمت المشاهدة؟";
"Clear the queue" = "مسح قائمة الإنتظار";
"Charging" = "جاري الشحن";
"Close" = "غلق";
"Close player when starting PiP" = "غلق المشغل عند فتح الفيديو المصغر";
"Close player when starting PiP" = "غلق المشغل عند بدء الفيديو المصغر";
"High" = "عالي";
"Badge color" = "لون الشارة";
"Accounts are not supported for the application of this instance" = "الحسابات غير مدعومة للتطبيق الخاص بهذه الحالة";
"Backend" = "الواجهة الخلفية";
"Badge" = "شارة";
"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)." = "تذكيرات صريحة لإبداء الإعجاب بها أو الإشتراك فيها أو التفاعل معها على أي منصة (منصات) مدفوعة أو مجانية (مثل النقر فوق مقطع الفيديو).";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "تذكيرات صريحة لإبداء الإعجاب بها أو الإشتراك فيها أو التفاعل معها على أي منصة (منصات) مدفوعة أو مجانية (مثل النقر فوق مقطع الفيديو).\n";
"Filter" = " عامل التصفية";
"Frontend URL" = "عنوان URL للواجهة الأمامية";
"Fullscreen size" = "حجم ملء الشاشة";
@@ -414,7 +414,7 @@
"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" = "حفظ سجل تاريخ مقاطع الفيديو المشغلة";
"Search" = "بحث";
@@ -430,12 +430,12 @@
/* 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" = "الشائع";
"Trending" = "المحتوى الرائج";
"Typically near or at the end of the video when the credits pop up and/or endcards are shown." = "عادةً ما تكون بالقرب من نهاية الفيديو عند ظهور قائمة الأسماء و / أو ظهور بطاقات النهاية.";
"Unsubscribe" = "إلغاء الإشتراك";
@@ -445,17 +445,17 @@
"Current Location" = "الموقع الحالي";
"You can switch between profiles in playback settings controls." = "يمكنك التبديل بين ملفات التعريف في عناصر التحكم في إعدادات التشغيل.";
"Current Playlist" = "قائمة تشغيل الحالية";
"Keep last played video in the queue after restart" = "احتفظ بآخر فيديو تم تشغيله في قائمة الانتظار بعد إعادة التشغيل";
"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" = "مسح قائمة الانتظار قبل الفتح";
"Video Details" = "تفاصيل مقطع الفيديو";
"Clear Queue before opening" = "مسح قائمة الإنتظار قبل الفتح";
"Current: %@\n%@" = "الحالي: %@\n%@";
"Thumbnails" = "‏الصور المصغرة";
"Show Open Videos toolbar button" = "أظهار زر شريط الأدوات لمقاطع الفيديو المفتوحة";
@@ -488,12 +488,12 @@
"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" = "الفيديو";
"Video" = "مقطع الفيديو";
"Audio" = "الصوت";
"Honor orientation lock" = "قفل توجيه الشرف";
"Proxy videos" = "وكيل مقاطع الفيديو";
@@ -504,20 +504,20 @@
"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" = "أظهر خيارات قائمة سياق الفيديو لفرض الواجهة الخلفية المحددة";
"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" = "أظهر التمرير إلى الزر العلوي في التعليقات";
"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" = "ضع علامة على تغذية القناة لم تتم المشاهدة";
"Mark channel feed as unwatched" = "وضع علامة لم تتم المشاهدة على محتوى القناة";
"Play all unwatched" = "‏تشغيل كل شيء لم تتم المشاهدة";
"Player Bar" = "‏شريط المشغل";
"Double tap gesture" = "إيماءة بنقرة المزدوجة";
@@ -526,15 +526,15 @@
"Clear all" = "‏مسح الكل";
"Maximum width expanded" = "الحد الأقصى للعرض الموسع";
"Single tap gesture" = "إيماءة بنقرة الواحدة";
"Tap and hold channel thumbnail to open context menu with more actions" = " اضغط مع الاستمرار على الصورة المصغرة للقناة لفتح قائمة السياق مع المزيد من الإجراءات";
"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" = "أظهر شارات التغذية التي لم تتم المشاهدة";
"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." = "التحكم في إعدادات إيماءة الفاصل الزمني للتخطي لإيماءة النقر المزدوج على الجانب الأيسر / الأيمن من المشغل. تغيير إعدادات نظام عناصر التحكم يتطلب إعادة التشغيل.";
"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" = "‏إقفال التدوير";
@@ -543,9 +543,9 @@
"Total size: %@" = "الحجم الإجمالي: %@";
"Actions Buttons" = "أزرار الإجراءات";
"Subscribe/Unsubscribe" = "الإشتراك/إلغاء الإشتراك";
"Show cache status" = "أظهر حالة ذاكرة التخزين المؤقت";
"Show cache status" = "إظهار حالة ذاكرة التخزين المؤقت";
"Cache" = "ذاكرة التخزين المؤقت";
"Maximum feed items" = "الحد الأقصى لعناصر التغذية";
"Maximum feed items" = "الحد الأقصى لعناصر المحتوى";
"Open channels with description expanded" = "فتح القنوات مع الوصف موسعاً";
"Close video and player on end" = "غلق الفيديو والمشغل عند النهاية";
"Use system controls with AVPlayer" = "استخدم نظام عناصر التحكم مع AVPlayer";
@@ -556,12 +556,12 @@
"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 أو أحدث). تغيير إعدادات نظام عناصر التحكم يتطلب إعادة التشغيل.";
"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." = "التحكم في إعدادات إيماءة فترة التخطي للنقر المزدوج على الجانب الأيسر/الأيمن من المشغل. تغيير إعدادات نظام عناصر التحكم يتطلب إعادة التشغيل.";
"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" = "‏إعدادات الصفحة الرئيسية";
@@ -571,23 +571,23 @@
"(watched and shorts hidden)" = "(تمت المشاهدة ومقاطع قصيرة مخفية)";
"(shorts hidden)" = "(مقاطع قصيرة مخفية)";
"Disable filters" = "تعطيل عوامل التصفية";
"(watched hidden)" = "(تم المشاهدة مخفية)";
"(watched hidden)" = "(تمت المشاهدة مخفية)";
"Limit" = "حد";
"Are you sure you want to remove %@ from Favorites?" = "هل أنت متأكد من أنك تريد إزالة %@ من المفضلة؟";
"List" = "قائمة";
"Cells" = "خلايا";
"Toggle size" = "‏حجم التبديل";
"Toggle player" = "تبديل المشغل";
"Show Next in Queue" = "أظهر التالي في قائمة الانتظار";
"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" = "ضع علامة على الجميع تمت المشاهدة";
"Feed" = "محتوى الإشتراكات";
"Mark all as unwatched" = "وضع علامة لم تتم المشاهدة على الجميع";
"Mark all as watched" = "وضع علامة تمت المشاهدة على الجميع";
"Queue - shuffled" = "قائمة الإنتظار - تم خلطها";
"Replay" = "إعادة المشغل";
"Lock" = "قفل";
@@ -596,7 +596,7 @@
"Playback Settings" = "إعدادات التشغيل";
"Description" = "وصف";
"Autoplay next" = "التشغيل التلقائي التالي";
"Stream" = "بث";
"Stream" = "البث";
"Chapters (if available)" = "الفصول (إن وجدت)";
"Open vertical chapters expanded" = "فتح الفصول الرأسية موسعاً";
"No preview" = "لا توجد معاينة";
@@ -604,3 +604,28 @@
"Podcasts" = "‏بودكاست";
"Releases" = "الإصدارات";
"Add %@" = "إضافة %@";
"Import Settings..." = "إستيراد الإعدادات...";
"Accounts passwords (unencrypted)" = "كلمات مرور الحسابات (غير مشفرة)";
"Other data" = "بيانات أخرى";
"Export..." = "تصدير…";
"Export" = "تصدير";
"File information" = "معلومات الملف";
"Build" = "بناء";
"Platform" = "المنصة";
"Import" = "‏إستيراد";
"Action button labels" = "تسميات زر الإجراء";
"Icon only" = "أيقونة فقط";
"Icon and text" = "أيقونة و نص";
"Custom Location not selected for import" = "لم يتم تحديد الموقع المخصّص للإستيراد";
"Account already exists" = "الحساب موجود بالفعل";
"Export Settings" = "تصدير الإعدادات";
"Other" = "أخرى";
"Other data include last used playback preferences and listing options" = "بيانات أخرى تتضمن آخر تفضيلات التشغيل المستخدمة وخيارات القائمة";
"Are you sure you want to export unencrypted passwords?" = "هل أنت متأكد من أنك تريد تصدير كلمات المرور غير المشفرة؟";
"Do not share this file with anyone or you can lose access to your accounts. If you don't select to export passwords you will be asked to provide them during import" = "لا تشارك هذا الملف مع أي شخص وإلا قد تفقد إمكانية الوصول إلى حساباتك. إذا لم تحدّد تصدير كلمات المرور، فسوف يُطلب منك تقديمها أثناء الإستيراد";
"Custom Location selected for import" = "حدّد الموقع المخصّص للإستيراد";
"Custom Location already exists" = "الموقع المخصّص موجود بالفعل";
"Password required to import" = "كلمة المرور مطلوبة للإستيراد";
"Password saved in import file" = "كلمة المرور محفوظة في ملف الإستيراد";
"Export in progress..." = "جارِ التصدير...";
"In progress..." = "في تَقَدم…";

View File

@@ -9,7 +9,7 @@
"Add Account" = "Add Account";
"Add Account..." = "Add Account...";
"Add Location" = "Add Location";
"Add Location..." = "Add Location...";
"Add Location..." = "Add Location..";
"Add profile..." = "Add profile...";
"Add Quality Profile" = "Add Quality Profile";
"Add to %@" = "Add to %@";
@@ -108,7 +108,7 @@
"Enter fullscreen in landscape" = "Enter fullscreen in landscape";
"Error" = "Error";
"Error when accessing playlist" = "Error when accessing playlist";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video).";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video).\n";
"Favorites" = "Favorites";
"Filter" = "Filter";
"Filter: active" = "Filter: active";
@@ -602,3 +602,28 @@
"No preview" = "No preview";
"Open vertical chapters expanded" = "Open vertical chapters expanded";
"Chapters (if available)" = "Chapters (if available)";
"Import Settings..." = "Import Settings...";
"Export Settings" = "Export Settings";
"Accounts passwords (unencrypted)" = "Accounts passwords (unencrypted)";
"Other" = "Other";
"Other data" = "Other data";
"Export..." = "Export…";
"Other data include last used playback preferences and listing options" = "Other data include last used playback preferences and listing options";
"Are you sure you want to export unencrypted passwords?" = "Are you sure you want to export unencrypted passwords?";
"Do not share this file with anyone or you can lose access to your accounts. If you don't select to export passwords you will be asked to provide them during import" = "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";
"Icon only" = "Icon only";
"Export" = "Export";
"File information" = "File information";
"Build" = "Build";
"Import" = "Import";
"Platform" = "Platform";
"Action button labels" = "Action button labels";
"Icon and text" = "Icon and text";
"Password required to import" = "Password required to import";
"Custom Location already exists" = "Custom Location already exists";
"Custom Location selected for import" = "Custom Location selected for import";
"Custom Location not selected for import" = "Custom Location not selected for import";
"Account already exists" = "Account already exists";
"Password saved in import file" = "Password saved in import file";
"Export in progress..." = "Export in progress...";
"In progress..." = "In progress…";

View File

@@ -112,12 +112,12 @@
"Help" = "Ayuda";
"Hide sidebar" = "Ocultar barra lateral";
"Add Location" = "Añadir ubicación";
"Add Location..." = "Añadir ubicación...";
"Add Location..." = "Añadir ubicación..";
"Decrease rate" = "Tasa de disminución";
"Decreased opacity" = "Opacidad disminuida";
"High" = "Alto";
"%lld videos" = "%lld videos";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Recordatorios explícitos para marquen \"me gusta\", se suscriban o interactúen con ellos en cualquier plataforma de pago o gratuita (por ejemplo, haciendo clic en un vídeo).";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Recordatorios explícitos para que indiquen les guste, se suscriban o interactúen con ellos en cualquier plataforma de pago o gratuita (por ejemplo, haciendo clic en un vídeo).\n";
"Formats will be selected in order as listed.\nHLS is an adaptive format (resolution setting does not apply)." = "Los formatos se seleccionarán en orden como se indica.\nHLS es un formato adaptable (no aplica la configuración de resolución).";
"Fullscreen size" = "Tamaño de pantalla completa";
"Badge & Decreased opacity" = "Insignia y opacidad dosminuída";
@@ -604,3 +604,28 @@
"No preview" = "Sin vista previa";
"Open vertical chapters expanded" = "Abrir capítulos verticales ampliados";
"Chapters (if available)" = "Capítulos (si están disponibles)";
"Password required to import" = "Se requiere contraseña para importar";
"Export Settings" = "Ajustes de exportación";
"Other" = "Otro";
"Other data" = "Información adicional";
"Export..." = "Exportar…";
"Are you sure you want to export unencrypted passwords?" = "¿Estás seguro de que quieres exportar las contraseñas sin cifrar?";
"Export" = "Exportar";
"Build" = "Compilación";
"Platform" = "Plataforma";
"Import" = "Importar";
"Action button labels" = "Etiquetas para los botones de acción";
"Icon only" = "Solo icono";
"Icon and text" = "Icono y texto";
"Custom Location already exists" = "Ya existe una ubicación personalizada";
"Custom Location selected for import" = "Ubicación personalizada seleccionada para la importación";
"Custom Location not selected for import" = "Ubicación personalizada no seleccionada para la importación";
"Password saved in import file" = "Contraseña guardada en el archivo de importación";
"Export in progress..." = "Exportación en curso...";
"In progress..." = "En proceso…";
"Import Settings..." = "Importar configuración...";
"Accounts passwords (unencrypted)" = "Contraseñas de las cuentas (no cifradas)";
"Other data include last used playback preferences and listing options" = "Información adicional incluye las últimas preferencias de reproducción utilizadas y las opciones de listado";
"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" = "No compartas este archivo con nadie o puedes perder el acceso a tus cuentas. Si no selecciona exportar contraseñas se le pedirá que las proporcione durante la importación";
"File information" = "Información del archivo";
"Account already exists" = "La cuenta ya existe";

View File

@@ -1,7 +1,7 @@
" subscribers" = " abonnés";
"Add Location..." = "Ajouter une instance";
"Add Location..." = "Ajouter une instance..";
"Add profile..." = "Ajouter un profil…";
"Add Quality Profile" = "Ajouter un profil de qualité";
"Delete" = "Supprimer";
@@ -264,7 +264,7 @@
"Don't use public locations" = "Ne pas utiliser d'instances publiques";
"Enable Return YouTube Dislike" = "Activer Return YouTube Dislike";
"Enter fullscreen in landscape" = "Entrer en plein écran en mode paysage";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Rappels d'aimer la vidéo, de s'abonner ou d'interagir avec le créateur sur une plateforme gratuite ou payante.";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Rappels d'aimer la vidéo, de s'abonner ou d'interagir avec le créateur sur une plateforme gratuite ou payante.\n";
"Frontend URL" = "URL frontale";
"Public Locations" = "Instances publiques";
"Public Manifest" = "Manifeste publique";
@@ -604,3 +604,28 @@
"No preview" = "Aucun aperçu";
"Open vertical chapters expanded" = "Ouvrir les chapitres verticaux étendus";
"Chapters (if available)" = "Chapitres (si disponibles)";
"Accounts passwords (unencrypted)" = "Mots de passe des comptes (non chiffrés)";
"Export..." = "Exporter…";
"Export" = "Exporter";
"Build" = "Build";
"Import" = "Importer";
"Action button labels" = "Textes des boutons d'action";
"File information" = "Informations sur le fichier";
"Export Settings" = "Paramètres d'exportation";
"Import Settings..." = "Importer des paramètres...";
"Other" = "Autres";
"Other data" = "Autres données";
"Other data include last used playback preferences and listing options" = "Les autres données incluent les dernières préférences de lecture et de liste utilisées";
"Are you sure you want to export unencrypted passwords?" = "Êtes-vous sûr de vouloir exporter les mots de passe non chiffrés?";
"Do not share this file with anyone or you can lose access to your accounts. If you don't select to export passwords you will be asked to provide them during import" = "Ne partagez pas ce fichier avec qui que ce soit, sinon vous risquez de perdre l'accès à vos comptes. Si vous ne choisissez pas d'exporter les mots de passe, il vous sera demandé de les fournir lors de l'importation";
"Platform" = "Plateforme";
"Icon only" = "Icône uniquement";
"Icon and text" = "Icône et texte";
"Custom Location already exists" = "L'emplacement personnalisé existe déjà";
"Custom Location selected for import" = "Emplacement personnalisé sélectionné pour l'importation";
"Custom Location not selected for import" = "Emplacement personnalisé non sélectionné pour l'importation";
"Password required to import" = "Mot de passe requis pour l'importation";
"Account already exists" = "Le compte existe déjà";
"Password saved in import file" = "Mot de passe enregistré dans le fichier d'importation";
"Export in progress..." = "Exportation en cours...";
"In progress..." = "En cours…";

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" = "場所を追加";
@@ -529,7 +529,7 @@
"For custom locations you can configure Frontend URL in Locations settings" = "場所を指定するには、場所の設定からフロントエンドのURLを設定します";
"Public Locations" = "公開された場所";
"Switch to public locations" = "公開された場所に切り替え";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "有料/無料のプラットフォームかを問わず、いいね、登録などを明示的に操作を促す(例: 動画をクリック)。";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "有料/無料のプラットフォームかを問わず、いいね、登録などを明示的に操作を促す(例: 動画をクリック)。\n";
"Proxy videos" = "動画閲覧にプロキシ使用";
"Sections" = "表示するボタン";
"System controls show buttons for %@" = "システム制御「%@」用のボタンを表示";
@@ -604,3 +604,28 @@
"No preview" = "プレビューなし";
"Open vertical chapters expanded" = "チャプターを縦方向に開く";
"Chapters (if available)" = "チャプター (あれば)";
"Password required to import" = "取り込むにはパスワードが必要です";
"Export..." = "出力…";
"Other data include last used playback preferences and listing options" = "ほかのデータには、最後に使った再生設定と一覧オプションを含む";
"File information" = "ファイル情報";
"Platform" = "プラットフォーム";
"Icon and text" = "アイコンと文字";
"Custom Location not selected for import" = "指定の場所は取り込み用に選択されていません";
"Import Settings..." = "設定の取り込み...";
"Export Settings" = "設定を出力";
"Accounts passwords (unencrypted)" = "アカウントのパスワード (暗号化なし)";
"Other" = "ほか";
"Other data" = "ほかのデータ";
"Are you sure you want to export unencrypted passwords?" = "暗号化のないパスワードを本当に出力しますか?";
"Custom Location selected for import" = "指定の場所は取り込み用に選択済み";
"Export" = "出力";
"Build" = "ビルド";
"Import" = "取り込み";
"Icon only" = "アイコンのみ";
"Action button labels" = "操作ボタンの表示";
"Export in progress..." = "エクスポート中...";
"In progress..." = "実行中…";
"Password saved in import file" = "取り込みファイルにパスワードを保存しました";
"Account already exists" = "アカウントは既に存在します";
"Do not share this file with anyone or you can lose access to your accounts. If you don't select to export passwords you will be asked to provide them during import" = "このファイルを他の人と共有しないでください。パスワードを出力していなければ、取り込み時にパスワードが求められます";
"Custom Location already exists" = "指定の場所は既に存在します";

View File

@@ -53,10 +53,244 @@
"Autoplaying Next" = "Volgende automatisch afspelen";
"Cellular" = "Mobiele data";
"Add to Playlist" = "Toevoegen aan afspeellijst";
"Browsing" = "Zoeken";
"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" = "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";
"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." = "Stukken normaal in het begin van een video met een animatie, stil plaatje, of stukje van een andere video van dezelfde maker.";
"Discord Server" = "Discord Server";
"Enable logging" = "Loggen inschakelen";
"Interface" = "Interface";
/* SponsorBlock category name */
"Intro" = "Intro";
"LIVE" = "LIVE";
"Matrix Chat" = "Matrix Chat";
/* SponsorBlock category name */
"Outro" = "Outro";
"Proxy videos" = "Video's door proxyserver leiden";
"Reset" = "Herstellen";
"Search history is empty" = "Zoekgeschiedenis is leeg";
"Search..." = "Zoeken...";
"Sections" = "Secties";
"Seek gesture sensitivity" = "Zoek gebaar gevoeligheid";
"Seek gesture speed" = "Zoek gebaar snelheid";
"Seek with horizontal swipe on video" = "Scrollen met horizontale sleep op video";
"Select location closest to you:" = "Selecteer de dichtstbijzijnde locatie:";
/* SponsorBlock category name */
"Self-promotion" = "Zelfpromotie";
"Settings" = "Instellingen";
"Share %@ link" = "%@ link delen";
"Share %@ link with time" = "%@ link met tijd delen";
"Share..." = "Delen...";
/* Video duration filter in search */
"Short" = "Kort";
"Show account username" = "Gebruikersnaam van account laten zien";
"Show anonymous accounts" = "Anonieme accounts laten zien";
"Show channel name" = "Naam van kanaal laten zien";
"Show history" = "Geschiedenis laten zien";

View File

@@ -605,3 +605,28 @@
"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)";
"Import Settings..." = "Importuj Ustawienia…";
"Export Settings" = "Eksportuj Ustawienia";
"Export" = "Eksportuj";
"Accounts passwords (unencrypted)" = "Hasła kont (nieszyfrowane)";
"Other" = "Inne";
"Other data" = "Inne dane";
"Export..." = "Eksportuj…";
"Other data include last used playback preferences and listing options" = "Inne dane obejmują ostatnie preferencje odtwarzania i opcje listowania";
"Are you sure you want to export unencrypted passwords?" = "Czy na pewno eksportować nieszyfrowane hasła?";
"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" = "Nie dziel się z nikim tym plikiem albo możesz stracić dostęp do swoich kont. Jeśli nie wybierzesz eksportu haseł, zostaniesz o nie zapytany podczas importu";
"File information" = "Informacje o pliku";
"Build" = "Wersja";
"Platform" = "Platforma";
"Import" = "Importuj";
"Action button labels" = "Etykiety przycisków akcji";
"Icon only" = "Tylko ikony";
"Icon and text" = "Ikony i tekst";
"Custom Location already exists" = "Własna Lokalizacja już istnieje";
"Custom Location selected for import" = "Lokalizacja wybrana do zaimportowania";
"Custom Location not selected for import" = "Lokalizacji nie wybrano do zaimportowania";
"Password required to import" = "Hasło wymagane do zaimportowania";
"Password saved in import file" = "Hasło zapisane w importowanym pliku";
"Account already exists" = "Konto już istnieje";
"Export in progress..." = "Eksport w toku…";
"In progress..." = "W trakcie…";

View File

@@ -355,7 +355,7 @@
"Could not extract channel information" = "Não pôde extrair informação do canal";
"For custom locations you can configure Frontend URL in Locations settings" = "Para localizações personalizadas você pode configurar URL do frontend nas configurações de localização";
"Add Location" = "Adicionar Localização";
"Add Location..." = "Adicionar Localização";
"Add Location..." = "Adicionar Localização..";
"For videos which feature music as the primary content." = "Para vídeos que têm música como conteúdo principal.";
"Close video after playing last in the queue" = "Fechar vídeo depois de tocar o último na fila";
"Clear Search History" = "Limpar Histórico de Busca";
@@ -406,7 +406,7 @@
"Country" = "País";
"Clear All" = "Limpar Tudo";
"Clear All Recents" = "Limpar Todos os Recentes";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Lembretes explícitos de dar like, se inscrever ou interagir com eles em qualquer plataforma, paga ou grátis (p.ex. clique em um vídeo).";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Lembretes explícitos de dar like, se inscrever ou interagir com eles em qualquer plataforma, paga ou grátis (p.ex. clicar em um vídeo).\n";
"Duration" = "Duração";
"Edit Quality Profile" = "Editar Perfil de Qualidade";
"Discussions take place in Discord and Matrix. It's a good spot for general questions." = "Discussões acontecem no Discord e no Matrix. É um bom lugar para perguntas gerais.";
@@ -604,3 +604,28 @@
"No preview" = "Sem prévia";
"Open vertical chapters expanded" = "Abrir capítulos verticais expandidos";
"Chapters (if available)" = "Capítulos (se disponível)";
"Password required to import" = "Senha necessária para importar";
"Export Settings" = "Exportar Ajustes";
"Accounts passwords (unencrypted)" = "Senhas das contas (não encriptadas)";
"Other" = "Outro";
"Export" = "Exportar";
"Build" = "Compilação";
"Action button labels" = "Rótulos dos botões de ação";
"Icon and text" = "Ícone e texto";
"Password saved in import file" = "Senha salva em arquivo de importação";
"Export in progress..." = "Exportação em progresso…";
"In progress..." = "Em progresso…";
"Import Settings..." = "Importar Ajustes…";
"Other data" = "Outros dados";
"Other data include last used playback preferences and listing options" = "Outros dados incluem as preferências de playback usadas pela última vez e opções de listagem";
"Export..." = "Exportar…";
"Platform" = "Plataforma";
"Are you sure you want to export unencrypted passwords?" = "Tem certeza que deseja exportar senhas sem criptografia?";
"Icon only" = "Apenas ícone";
"Custom Location already exists" = "Localização Personalizada já existe";
"Custom Location selected for import" = "Localização Personalizada selecionada para importação";
"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" = "Não compartilhe este arquivo com ninguém, ou você poderá perder acesso às suas contas. Se você não selecionar a exportação de senhas, será perguntado por elas durante a importação";
"File information" = "Informação do arquivo";
"Import" = "Importar";
"Custom Location not selected for import" = "Localização Personalizada não selecionada para importação";
"Account already exists" = "Conta já existe";

View File

@@ -10,7 +10,7 @@
"Accounts are not supported for the application of this instance" = "Conturile nu sunt acceptate pentru aplicaţia acestei instanțe";
"%lld videos" = "%lld videoclipuri";
"Add Location" = "Adaugă locație";
"Add Location..." = "Adaugă locație...";
"Add Location..." = "Adaugă locație..";
"Add profile..." = "Adaugă profil...";
"Add to %@" = "Adaugă la %@";
"Add to Playlist" = "Adaugă la playlist";
@@ -62,7 +62,7 @@
"Edit" = "Editați";
"Edit Playlist" = "Editați Playlist";
"Enter fullscreen in landscape" = "Introduceți ecranul complet în peisaj";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Mementouri explicite de a aprecia, de a vă abona sau de a interacționa cu ele pe orice platformă (platforme) plătite sau gratuite (de exemplu, faceți clic pe un videoclip).";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Mementouri explicite de a aprecia, de a vă abona sau de a interacționa cu ele pe orice platformă (platforme) plătite sau gratuite (de exemplu, faceți clic pe un videoclip).\n";
"Find Other" = "Găsiți alte";
"Finding something to play..." = "Să găsești ceva de jucat...";
"For videos which feature music as the primary content." = "Pentru videoclipurile care includ muzica ca conținut principal.";
@@ -604,3 +604,28 @@
"Description preview" = "Descriere preview";
"No preview" = "Fără previzualizare";
"Chapters (if available)" = "Capitole (dacă există)";
"Password required to import" = "Parolă necesară pentru a importa";
"Import Settings..." = "Importă Setări...";
"Export Settings" = "Exportă Setări";
"Other" = "Alte";
"Other data" = "Alte date";
"Export..." = "Exportă…";
"Other data include last used playback preferences and listing options" = "Alte date includ ultimele preferințe de redare utilizate și opțiunile de listare";
"Are you sure you want to export unencrypted passwords?" = "Sigur doriți să exportați parole necriptate?";
"Export" = "Exportă";
"File information" = "Informații despre fișier";
"Build" = "Build";
"Platform" = "Platformă";
"Import" = "Importă";
"Action button labels" = "Etichete pentru butoanele de acțiune";
"Icon only" = "Doar pictogramă";
"Icon and text" = "Pictogramă și text";
"Custom Location already exists" = "Locația customizată există deja";
"Custom Location not selected for import" = "Locația customizată nu este selectată pentru importare";
"Account already exists" = "Există deja un cont";
"Password saved in import file" = "Parolă salvată în fișierul de import";
"Export in progress..." = "Export în curs...";
"In progress..." = "În curs…";
"Custom Location selected for import" = "Locație customizată selectată pentru importare";
"Accounts passwords (unencrypted)" = "Parolele conturilor (necriptate)";
"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" = "Nu partajați acest fișier cu nimeni, altfel puteți pierde accesul la conturile tale. Dacă nu selectați să exportați parolele, vi se va cere să le furnizați în timpul importului";

View File

@@ -267,28 +267,28 @@
"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" = "";
@@ -305,38 +305,38 @@
"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" = "Hesabın kullanıcı adını göster";
"Used to create links from videos, channels and playlists" = "";
"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" = "";
"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" = "Kısa";
"Home" = "";
"Home" = "Ana Sayfa";
"Remove Location" = "";
"Edit Favorites…" = "Favorileri düzenle…";
"Show Open Videos toolbar button" = "";
"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." = "";
"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" = "";
"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" = "";
"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" = "Oynatma istatistiklerini göster";
"Pages buttons" = "";
"Videos" = "";
"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..." = "%@ akışıılıyor...";
"Clear Queue before opening" = "";
"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" = "";
"Week" = "Hafta";
"Sidebar" = "Kenar çubuğu";
"Show only icons" = "";
"Current: %@\n%@" = "Şuan: %@\n%@";
"Show anonymous accounts" = "Anonim hesapları göster";
"Could not open playlist" = "";
"Could not open playlist" = "Çalma listesi açılamadı";
"Round corners" = "";
"URL" = "";
"URL" = "URL";
"Recents" = "";
"Show sidebar when space permits" = "Alan olduğu sürece kenar çubuğunu göster";
"System controls buttons" = "";
"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" = "";
"Seek gesture sensitivity" = "Kaydırma hassasiyeti";
"Show keywords" = "Anahtar kelimeleri göster";
"Wiki" = "";
"Username" = "";
"Could not extract video ID" = "";
"Wiki" = "Wiki";
"Username" = "Kullanıcı adı";
"Could not extract video ID" = "Video ID bilgisi alınamadı";
/* Player controls layout size */
"Smaller" = "Küçült";
"Sort" = "Sırala";
"This cannot be reverted" = "";
"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" = "Video uzunluğunu göster";
"Source" = "Kaynak";
"Welcome" = "";
"Wi-Fi" = "";
"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" = "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." = "";
"Unsubscribe" = "";
"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" = "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" = "";
@@ -480,22 +480,22 @@
"System controls show buttons for %@" = "";
"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" = "";

View File

@@ -90,7 +90,7 @@
"Delete" = "删除";
"Disabled" = "禁用";
"Discord Server" = "Discord 服务器";
"Discussions take place in Discord and Matrix. It's a good spot for general questions." = "讨论在 Discord 及 Matrix 中行,您可以在里面询问一些普通的问题。";
"Discussions take place in Discord and Matrix. It's a good spot for general questions." = "討論在 Discord 及 Matrix 中行,您可以在裡面詢問一些簡單的問題。";
"Don't use public locations" = "不要使用公开地址";
"Donations" = "捐赠";
"Done" = "完成";

View File

@@ -0,0 +1,631 @@
"Format" = "格式";
"Driver" = "驅動";
"Audio" = "音效";
"Show only icons" = "只顯示圖標";
"Center" = "正中";
"File" = "文件";
"Documents" = "文件";
"Video" = "視頻";
"Codec" = "編碼";
"Size" = "大小";
"Sample Rate" = "取樣率";
"Mark channel feed as watched" = "標記頻道為已觀看";
"Clear all" = "清除所有";
"\"%@\" will be irreversibly removed from this device." = "\"%@\" 將會於此裝置上被永久移除。";
"Music Mode" = "音樂模式";
"Close video" = "關閉視頻";
"Play next item" = "播放下一項目";
"Maximum width expanded" = "最大寬度已展開";
"Show unwatched feed badges" = "顯示未觀看的\"最新影片\"標誌";
"Gesture: fowards" = "手勢: 向前";
"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" = "已打開文件";
"Landscape left" = "橫屏左邊";
"Landscape right" = "橫屏右邊";
"Gesture settings control skipping interval for double tap gesture on left/right side of the player. Changing system controls settings requires restart." = "手勢設置控制玩家左右兩側雙擊手勢的跳過間隔。更改系統控制設置需要重新啓動。";
"Show scroll to top button in comments" = "在評論中顯示滾動到頂部按鈕";
"(watched and shorts hidden)" = "(隱藏已觀看及短片)";
"(watched hidden)" = "(隱藏已觀看)";
"Show video context menu options to force selected backend" = "顯示視頻內容目錄選項來強制已選取的後端";
"Other data" = "其他資料";
"Other data include last used playback preferences and listing options" = "其他資料包括上次的播放喜好和清單選項";
"File information" = "檔案資訊";
"Build" = "版本";
"Action button labels" = "動作按鈕標籤";
"Icon and text" = "圖示及文字";
"Password required to import" = "需要匯入的密碼";
"Edit" = "編輯";
"Enable Return YouTube Dislike" = "啟用YouTube 不喜歡回報";
"Enter fullscreen in landscape" = "橫屏下進入全屏";
"Error when accessing playlist" = "播放清單出錯";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "明確提醒在任何付費或免費平台上按讚、訂閱或與他們互動(例如點擊影片)。\n";
"For videos which feature music as the primary content." = "以音樂為主要內容的視頻。";
"I like this app!" = "我喜歡這app!";
"If you are interested what's coming in future updates, you can track project Milestones." = "如果您對將來的功能更新感興趣,您可以追蹤我們的專案里程碑。";
"Increase rate" = "增长率";
/* SponsorBlock category name */
"Intro" = "簡介";
"Issues Tracker" = "問題追蹤介面";
/* Selected video has just finished playing */
"Just watched" = "已觀看";
/* Player controls layout size */
"Large" = "大";
"Large layout is not suitable for all devices and using it may cause controls not to fit on the screen." = "大佈局並不適合所有設備,使用它可能導致控制按鈕在屏幕上並不貼合。";
"LIVE" = "直播";
"Low quality" = "低畫質";
"Low" = "低";
"Mark video as watched after playing" = "播放後標記為已觀看";
"MPV Documentation" = "MPV 文檔";
"Orientation" = "方向";
"Music" = "音樂";
"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." = "視頻宣傳產品或服務的一部分,與創作者沒有直接關係。創作者將以金錢或免費產品的形式獲得報酬或補償。";
"Remove from Playlist" = "從播放清單中移除";
"Replies" = "回覆";
"Reset" = "重設";
"Restart the app to apply the settings above." = "重啟app 以應用以上設置。";
"Sections" = "章節";
"Share..." = "分享...";
"Show account username" = "顯示帳戶名稱";
"Show channel name" = "顯示頻道名稱";
"This cannot be reverted. You might need to switch between views or restart the app to see changes." = "這不能復原。你可能需要轉換顯示或重啟app 才能顯示變更。";
/* Player controls layout size for TV */
"TV" = "電視";
"unknown" = "不明";
"Unsubscribe" = "取消訂閱";
"Typically near or at the end of the video when the credits pop up and/or endcards are shown." = "通常在視頻結束時或接近視頻結尾時,出現 Credits Pop Up 和結束卡片。";
"Upload date" = "上載日期";
"URL" = "網址";
"Used to create links from videos, channels and playlists" = "用於從視頻、頻道和播放清單創建連結";
/* Player controls layout size */
"Very Large" = "非常大";
"Videos" = "視頻";
/* Video sort order in search */
"Views" = "觀看次數";
"Watched" = "已觀看";
"No chapters information available" = "沒有章節資訊";
"Share Logs..." = "分享日誌…";
"Any format" = "任何格式";
"%@ formats" = "%@ 格式";
"Keep last played video in the queue after restart" = "重啟後保留最後播放視頻至隊列";
"Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "播放清單空白\n\n點擊並按住視頻然後\n\"加至播放清單\"";
"Could not refresh Subscriptions" = "無法更新訂閱列表";
"Could not load streams" = "無法加載視頻";
"Could not open video" = "無法開啟視頻";
"Channel could not be found" = "無法找到頻道";
"For custom locations you can configure Frontend URL in Locations settings" = "對於自定義站台,您可以在設置中配置前端 URL";
"This URL could not be opened" = "無法打開此URL";
"Could not open channel" = "無法打開頻道";
/* Selected video is being played */
"Watching now" = "觀看中";
/* Video date filter in search */
"Week" = "星期";
"Yattee" = "Yattee";
"Could not update your token." = "無法更新你的權仗(Token)。";
"Could not refresh Trending" = "無法更新趨勢";
"Could not extract channel information" = "無法獲取頻道資訊";
"Yattee %@ (build %@)" = "Yattee %@ (版本 %@)";
/* Video date filter in search */
"Year" = "年";
"You can find information about using Yattee in the Wiki pages." = "您可以在 GitHub 相關頁面中找到有關使用 Yattee 的信息。";
"You can use automatic profile selection based on current device status or switch it in video playback settings controls." = "您可以使用基於當前設備狀態的自動配置文件選擇,或在視頻播放設置控件中進行切換。";
"Could not extract playlist ID" = "無法提取播放清單ID";
"Could not load video" = "無法載入視頻";
"You need to create an instance and accounts\nto access %@ section" = "你需要建立站台及帳戶\n來存取 %@ 部分";
"You need to select an account\nto access %@ section" = "你需要選擇帳戶\n來存取 %@ 部分";
"If you want this app to be available in your language, join translation project." = "如果你想此app 以你的語言顯示,請加入翻譯專案。";
"Private" = "私人";
"Playback queue is empty" = "播放隊列空白";
"Playing Next" = "播放下一個";
"You can switch between profiles in playback settings controls." = "您可以在播放設置控件中切換配置文件。";
"Current Playlist" = "當前播放清單";
"Stream & Player" = "串流及播放器";
"Statistics" = "數據";
"Hardware decoder" = "硬體解碼";
"Stream FPS" = "串流 FPS";
"Cached time" = "緩存時間";
"Rate & Captions" = "評分及字幕";
"Dropped frames" = "損失幀數";
"Could not create share link" = "無法建立分享連結";
"%@ Channel" = "%@ 頻道";
"%@ Playlist" = "%@ 播放清單";
"%@ subscribers" = "%@ 訂閱者";
"Accounts" = "帳戶";
"Accounts are not supported for the application of this instance" = "本站並不支持帳戶";
"Add Account" = "新增帳戶";
"%lld videos" = "%lld 視頻";
"Add Account..." = "新增帳戶...";
"Add Location" = "新增站點";
"Add Location..." = "新增站點..";
"Add profile..." = "新增配置...";
"Add Quality Profile" = "新增畫質配置";
"Add to %@" = "添加到 %@";
"Add to Favorites" = "加至我的最愛";
"Add to Playlist" = "加至播放清單";
"Add to Playlist..." = "加至播放清單...";
"Advanced" = "高級";
/* Trending category, section containing all kinds of videos */
"All" = "所有";
"Always use AVPlayer for live videos" = "總是使用AVPlayer(直播)";
"Anonymous" = "匿名";
/* Video date filter in search
Video duration filter in search */
"Any" = "任何";
"Apply to all" = "套用至全部";
"Are you sure you want to unsubscribe from %@?" = "確定要取消訂閱 %@";
"Automatic" = "自動";
"Autoplaying Next" = "自動播放下一個";
"Backend" = "後台";
"Categories to Skip" = "要跳過的類別";
"Cellular" = "流動網絡";
"Chapters" = "章節";
"Charging" = "充電中";
"Clear" = "清除";
"Clear All" = "清除所有";
"Clear All Recents" = "清除所有最近";
"Clear History" = "清除記錄";
"Clear Search History" = "清除搜尋記錄";
"Clear Search History..." = "清除搜尋記錄...";
"Clear the queue" = "清除隊列";
"Close" = "關閉";
"Close PiP when player is opened" = "當播放器打開時,關閉 PiP";
"Close player when closing video" = "當關閉視頻時,關閉播放器";
"Close player when starting PiP" = "當啟動PiP時關閉播放器";
"Close Video" = "關閉視頻";
"Close video after playing last in the queue" = "播放完最後隊列後關閉視頻";
"Connected successfully (%@)" = "連接成功 (%@)";
"Connection failed" = "連接失敗";
"Contact" = "聯繫";
"Continue" = "繼續";
"Continue from %@" = "從 %@繼續";
"Contributing" = "貢獻";
"Controls" = "控制";
"Copy %@ link" = "複製 %@ 連結";
"Copy %@ link with time" = "複製 %@ 連結(含時間)";
"Could not load locations manifest" = "無法加載站台列表";
"Country Name or Code" = "國家名稱或代碼";
"Create Playlist" = "新建播放清單";
"Current: %@\n%@" = "現在: %@\n%@";
/* Locations settings, custom instance is selected as current */
"Custom" = "自定義";
"Custom Locations" = "自定義站台";
/* Video sort order in search */
"Date" = "日期";
"Decrease rate" = "下降率";
"Decreased opacity" = "減少透明度";
"Delete" = "刪除";
"Disabled" = "禁用";
"Discord Server" = "Discord 伺服器";
"Discussions take place in Discord and Matrix. It's a good spot for general questions." = "討論在 Discord 及 Matrix 中進行,您可以在裡面詢問一些簡單的問題。";
"Don't use public locations" = "不使用公開站台";
"Donations" = "捐贈";
"Done" = "完成";
"Duration" = "時長";
"Edit Playlist" = "編輯播放清單";
"Edit Quality Profile" = "編輯質量配置";
"Edit..." = "編輯...";
"Enable logging" = "啟用日誌";
"Error" = "錯誤";
"Favorites" = "我的最愛";
"Filter" = "篩選";
"Filter: active" = "篩選: 啟用";
"Find Other" = "搜尋其他";
"Finding something to play..." = "正在尋找視頻...";
"Formats will be selected in order as listed.\nHLS is an adaptive format (resolution setting does not apply)." = "格式將按列出的順序選擇。\nHLS是一種自適應格式解析度設定不適用。";
"Frontend URL" = "前端網址";
"Fullscreen size" = "全屏大小";
"Gaming" = "遊戲";
"Help" = "幫助";
"Hide sidebar" = "隱藏側邊欄";
"High" = "高";
"Highest" = "最高";
"Highest quality" = "最高畫質";
"History" = "歷史";
"Honor orientation lock" = "方向鎖";
/* Video date filter in search */
"Hour" = "小時";
"I am lost" = "我迷失了";
"I found a bug /" = "我發現bug";
"I have a feature request" = "我有一個功能需要";
"I want to ask a question" = "我想問問題";
"If you are reporting a bug, include all relevant details (especially: app version, used device and system version, steps to reproduce)." = "如果你要反饋一個程式錯誤請包括所有相關資料特別是App 版本,使用設備以及系統版本,重現步驟)。";
"Info" = "資訊";
"Instance of current account" = "此帳戶站台";
/* SponsorBlock category name */
"Interaction" = "交互";
"Interface" = "介面";
/* Loading stream OSD */
"Loading streams..." = "加載中...";
"Loading..." = "加載中...";
"Locations" = "地址";
"Lock portrait mode" = "鎖定直屏";
/* Video duration filter in search */
"Long" = "長";
"Lowest" = "最低";
"Mark as watched" = "標記為已觀看";
"Mark watched videos with" = "標記已觀看視頻";
"Matrix Channel" = "Matrix 頻道";
"Matrix Chat" = "Matrix 聊天";
/* Player controls layout size */
"Medium" = "中";
"Medium quality" = "中畫質";
"Milestones" = "里程碑";
/* Video date filter in search */
"Month" = "月";
"More info can be found in:" = "更多資訊可在:";
"Movies" = "電影";
"Name" = "名稱";
"New Playlist" = "新播放清單";
"Next" = "下一個";
"No description" = "無簡介";
"No Playlists" = "沒有播放清單";
"No results" = "沒有結果";
"Normal" = "正常";
"Not available" = "不可用";
"Not Playing" = "沒有播放";
"Nothing" = "沒有東西";
/* SponsorBlock category name */
"Offtopic in Music Videos" = "在音樂視頻中的無關內容";
"Only when signed in" = "僅當登錄後";
"Open \"Playlists\" tab to create new one" = "打開「播放清單」 頁面來創建新的";
"Open Settings" = "打開設置";
/* Loading stream OSD */
"Opening %@ stream..." = "正在打開 %@ ...";
"Opening audio stream..." = "正在打開音訊...";
/* SponsorBlock category name */
"Outro" = "結尾";
"Password" = "密碼";
"Pause" = "暫停";
"Pause when entering background" = "進入後台時暫停";
"Pause when player is closed" = "播放器關閉時暫停";
"Picture in Picture" = "畫中畫";
"Play" = "播放";
"Play All" = "全部播放";
"Play in PiP" = "在畫中畫播放";
"Play Last" = "播放最後";
"Play Music" = "播放音樂";
"Play Next" = "播放下一部";
"Play Now" = "即時播放";
"Playback" = "播放";
"Player" = "播放器";
"Playlist" = "播放清單";
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "播放清單 “%@” 將被删除。\n此操作不可恢復。";
"Playlists" = "播放清單";
"Popular" = "熱播";
"Preferred Formats" = "喜好格式";
"Profiles" = "配置";
"Promoting a product or service that is directly related to the creator themselves. This usually includes merchandise or promotion of monetized platforms." = "推廣與創作者本身直接相關的產品或服務。這通常包括商品或盈利平台的推廣。";
"Proxy videos" = "代理視頻";
"Public Locations" = "公共站台";
"Public Manifest" = "公共清單";
"Quality" = "畫質";
"Quality Profile" = "畫質配置";
"Queue" = "隊列";
"Queue is empty" = "隊列為空";
"Rate" = "速度";
/* Video sort order in search */
"Rating" = "評級";
"Recents" = "最近";
"Red" = "紅";
"Refresh" = "更新";
"Regular size" = "正常大小";
"Regular Size" = "正常大小";
"Related" = "相關";
/* Video sort order in search */
"Relevance" = "相關度";
"Remove" = "移除";
"Remove from Favorites" = "從我的最愛中移除";
"Remove from history" = "從歷史中移除";
"Remove from the queue" = "從隊列中移除";
"Reset search filters" = "重設搜尋篩選";
"Reset watched status when playing again" = "重新播放後重設播放狀態";
"Resolution" = "分辨率";
"Restart" = "重新啟動";
"Restart/Play next" = "重新播放/播放下一個";
"Restore default profiles..." = "重置默認配置文件...";
"Rotate to portrait when exiting fullscreen" = "退出全屏後旋轉為直屏";
"Round corners" = "圓角";
"Save" = "儲存";
"Save history of played videos" = "儲存已播放視頻記錄";
"Save history of searches, channels and playlists" = "儲存搜尋,頻道及播放清單記錄";
"Search" = "搜尋";
"Search history is empty" = "搜尋歷史為空";
"Search..." = "搜尋...";
"Seek gesture sensitivity" = "手勢靈敏度";
"Seek gesture speed" = "手勢速度";
"Seek with horizontal swipe on video" = "視頻水平滑動搜索";
"Segments typically found at the start of a video that include an animation, still frame or clip which are also seen in other videos by the same creator." = "通常在視頻開頭找到的片段,包括動畫、靜止幀或剪輯,這些片段也可以由同一創作者在其他視頻中看到。";
"Select location closest to you:" = "選取離你最近的站台:";
/* SponsorBlock category name */
"Self-promotion" = "自我推銷";
"Settings" = "設置";
"Share %@ link" = "分享 %@ 連結";
"Share %@ link with time" = "分享 %@ 連結(含時間)";
/* Video duration filter in search */
"Short" = "短";
"Show anonymous accounts" = "顯示匿名帳戶";
"Show history" = "顯示歷史";
"Show keywords" = "顯示關鍵字";
"Show playback statistics" = "顯示播放統計";
"Show progress of watching on thumbnails" = "縮圖顯示播放進度";
"Show sidebar when space permits" = "空間充裕時顯示側邊欄";
"Show video length" = "顯示視頻長度";
"Shuffle" = "隨機播放";
"Shuffle All" = "隨機播放全部";
"Sidebar" = "側邊欄";
"Sign In Required" = "需要登錄";
/* Player controls layout size */
"Small" = "小";
/* Player controls layout size */
"Smaller" = "更小";
"Sort" = "排序";
"Sort: %@" = "排序: %@";
"Source" = "源";
/* SponsorBlock category name */
"Sponsor" = "宣傳";
"SponsorBlock" = "SponsorBlock (跳過贊助廣告)";
"SponsorBlock API Instance" = "SponsorBlock API 站台";
"Subscribe" = "訂閱";
/* Subscriptions title */
"Subscriptions" = "訂閱";
"Switch to other public location" = "轉換至其他公共站台";
"Switch to public locations" = "轉換至公共站台";
"System controls buttons" = "系統控制鍵";
"System controls show buttons for %@" = "系統控制%@鍵";
"That's nice to hear. It is fun to deliver apps other people want to use. You can consider donating to the project or help by contributing to new features development." = "很高興聽到您這麼說。提供人們想要的應用程序是一件很有趣的事情。您可以考慮為項目捐款,或為新功能開發做出貢獻。";
"This cannot be reverted" = "這不能復原";
"This information will be processed only on your device and used to connect you to the server in the specified country." = "此信息將僅在您的設備上處理,並用於將您連接到指定國家/地區的服務器。";
"This will remove all your custom profiles and return their default values. This cannot be reverted." = "這將刪除所有自定義配置文件並還原為其默認值。此操作無法復原。";
"Thumbnails" = "縮圖";
/* Video date filter in search */
"Today" = "今日";
"Trending" = "趨勢";
"Username" = "用戶名稱";
/* Selected video was played on given date */
"Watched %@" = "已觀看 %@";
"Welcome" = "歡迎";
"Wi-Fi" = "無線網絡";
"Wiki" = "GitHub";
"You have no Playlists" = "你沒有播放清單";
"You have no playlists\n\nTap on \"New Playlist\" to create one" = "你沒有播放清單\n\n點擊\"新建播放清單\"建立";
"Public" = "公開";
"Unlisted" = "未列出";
"Now Playing" = "現正播放";
"Current Location" = "現在位置";
"Add Channels, Playlists and Searches to Favorites using" = "添加頻道、播放清單和搜尋到我的最愛";
"Make default" = "設為預設";
"Visibility" = "可見度";
"It can be changed later in settings. You can use your own locations too." = "稍後可以在設置中更改。你也可以使用自己的地址。";
"Press and hold remote button to open captions and quality menus" = "按住遙控按鈕打開字幕和畫質功能表";
"Comments are disabled" = "留言被關閉";
"No comments" = "沒有留言";
"Open logs in Finder" = "在Finder 中開啟日誌";
"Could not extract SID from received cookies: %@" = "無法從Cookies 提取SID: %@";
"Could not refresh Popular" = "無法更新熱播";
"Could not open playlist" = "無法打開播放清單";
"Could not extract video ID" = "無法提取視頻ID";
"This video could not be opened" = "這視頻無法打開";
"No locations available at the moment" = "現時沒有可用地址";
"Could not refresh Playlists" = "無法更新播放清單";
"Translations" = "翻譯";
"No documents" = "沒有文件";
"Recent Documents" = "最近文件";
"Home" = "主頁";
"Share files from Finder on a Mac\nor iTunes on Windows" = "在 Windows iTunes 或 Mac 上\n共享 Finder 中的文件";
"Show Home" = "顯示主頁";
"Show Open Videos quick actions" = "顯示打開視頻快速操作";
"Recent History" = "最近歷史";
"Show Favorites" = "顯示我的最愛";
"Inspector visibility" = "檢查器可見度";
"Edit Favorites…" = "編輯我的最愛…";
"Show Open Videos toolbar button" = "顯示打開視頻工具按鈕";
"Buttons labels" = "按鈕標簽";
"Files" = "文件";
"Show Documents" = "顯示文件";
"Pages toolbar position" = "頁面工具列位置";
"Video Details" = "視頻詳情";
"Show Inspector" = "顯示檢查器";
"Reload manifest" = "重新載入清單";
"Clear Queue before opening" = "開啟前清除隊列";
"Open" = "打開";
"Video actions buttons" = "視頻操作按鈕";
"Pages buttons" = "頁面按鈕";
"URL to Open" = "要打開的URL";
"Enter link to open" = "輸入要打開的連結";
"Could not open Files" = "無法打開文件";
"Paste" = "貼上";
"Open Videos" = "打開視頻";
"Enter links to open, one per line" = "輸入需要打開的連結,每行一個";
"Playback Mode" = "播放模式";
"Add" = "添加";
"Hide" = "隱藏";
"Always" = "總是";
"Only for local files and URLs" = "僅針對本地文件以及連結";
"Right" = "右";
"Channels" = "頻道";
"Open Files" = "打開文件";
"Share" = "分享";
"Show icons and text when space permits" = "在空間允許時顯示圖標和文字";
"Left" = "左";
"FPS" = "FPS";
"Address" = "地址";
"Remove…" = "移除…";
"Show sidebar" = "顯示側邊欄";
"Locations Manifest" = "地址清單";
"Remove Location" = "移除地址";
"Open Video" = "打開視頻";
"Default Profile" = "預設配置";
"Playback history is empty" = "播放記錄空白";
"Copy%@link" = "複製%@連結";
"Share%@link" = "分享%@連結";
"Are you sure you want to remove this document?" = "你確定要移除文件?";
"Could not delete document" = "無法刪除文件";
"Live Streams" = "直播";
"Shorts" = "短片";
"Channel" = "頻道";
"Mark channel feed as unwatched" = "標記頻道為未觀看";
"Could not find any links to open in your clipboard" = "無法在你的剪輯版中找到任何可以打開的連結";
"Actions buttons" = "動作按鈕";
"Are you sure you want to remove %@ location?" = "你確定想要刪除 %@ 地址?";
"Verified" = "已認證";
"Open expanded" = "展開";
"Short videos: visible" = "短片: 可見";
"Player Bar" = "播放器控制條";
"Short videos: hidden" = "短片: 隱藏";
"Play all unwatched" = "播放所有未觀看";
"Double tap gesture" = "雙擊手勢";
"Tap and hold channel thumbnail to open context menu with more actions" = "點擊並按住頻道縮圖以打開包含更多操作的選單";
"Always show controls buttons" = "總是顯示控制按鈕";
"Single tap gesture" = "單擊手勢";
"Seeking" = "搜索中";
"Right click channel thumbnail to open context menu with more actions" = "右鍵單擊頻道縮圖以打開具有更多操作的選單";
"Controls Buttons" = "控制按鈕";
"System controls" = "系統控制";
"Controls button: backwards" = "控制按鈕: 向後";
"Controls button: forwards" = "控制按鈕: 向前";
"Gesture: backwards" = "手勢: 向後";
"Hide player" = "隱藏播放器";
"Gesture settings control skipping interval for double click on left/right side of the player. Changing system controls settings requires restart." = "手勢設置控制雙擊播放器左/右的跳躍間隔。更改系統控制設置需要重新啓動。";
"Lock orientation" = "鎖定方向";
"Cache" = "緩存";
"Actions Buttons" = "動作按鈕";
"Total size: %@" = "總大小: %@";
"Open channels with description expanded" = "打開頻道(含描述展開)";
"Subscribe/Unsubscribe" = "訂閱/取消訂閱";
"Show cache status" = "顯示緩存狀態";
"Maximum feed items" = "最大\"最新影片\"數目";
"Open channel" = "打開頻道";
"Inspector" = "檢查器";
"Open video description expanded" = "打開視頻描述";
"Mark all as unwatched" = "標記所有為未觀看";
"Mark all as watched" = "標記所有為已觀看";
"Playback Settings" = "播放設定";
"Replay" = "重播";
"Fullscreen" = "全屏幕";
"Lock" = "鎖定";
"Description" = "描述";
"Autoplay next" = "自動播放下一個";
"Stream" = "串流";
"Are you sure you want to clear cache?" = "你確定要清除緩存嗎?";
"Show Next in Queue" = "在隊列中顯示下一個";
"Show toggle watch status button" = "顯示切換觀看狀態按鈕";
"Next in Queue" = "隊列中下一個";
"List" = "列表";
"Cells" = "網格";
"Toggle size" = "替換大小";
"Toggle player" = "替換播放器";
"Do nothing" = "不做";
"Feed" = "最新影片";
"Queue - shuffled" = "隊列 - 隨機";
"Loop one" = "單個循環";
"File Extension" = "副檔名";
"Opening file..." = "正在打開文件...";
"Public account" = "公共帳戶";
"Enter account credentials to connect..." = "輸入帳號密碼來連接...";
"Enter location address to connect..." = "輸入站台地址來連接...";
"Seek" = "搜索";
"Your Accounts" = "你的帳戶";
"Browse without account" = "匿名瀏覽";
"Close video and player on end" = "播放結束時關閉視頻及播放器";
"Use system controls with AVPlayer" = "在AVPlayer 時使用系統控制按鈕";
"Rotate when entering fullscreen on landscape video" = "觀看橫向全屏視頻時旋轉";
"Available" = "可用";
"Home Settings" = "主頁設置";
"Watched: hidden" = "已觀看: 隱藏";
"No rotation" = "不要旋轉";
"Startup section" = "啟動部分";
"Watched: visible" = "已觀看: 可見";
"No videos to show" = "沒有視頻顯示";
"(shorts hidden)" = "(隱藏短片)";
"Disable filters" = "禁用過濾";
"Limit" = "上限";
"Are you sure you want to remove %@ from Favorites?" = "你確定要從我的最愛中刪除 %@ 嗎?";
"Keep channels with unwatched videos on top of subscriptions list" = "保留頻道內未觀看視頻在訂閱列表頂端";
"Play Now in MPV" = "在MPV 中播放";
"Play Now in AVPlayer" = "在AVPlayer 中播放";
"Show channel avatars in videos lists" = "在視頻列表中顯示頻道頭像";
"Show channel avatars in channels lists" = "在頻道列表中顯示頻道頭像";
"Podcasts" = "播客";
"Releases" = "發布";
"Add %@" = "添加 %@";
"Description preview" = "描述預覽";
"No preview" = "沒有預覽";
"Open vertical chapters expanded" = "展開垂直章節";
"Chapters (if available)" = "章節(如有)";
"Import Settings..." = "匯入設定...";
"Export Settings" = "匯出設定";
"Accounts passwords (unencrypted)" = "帳戶密碼 (非加密)";
"Other" = "其他";
"Export..." = "匯出…";
"Are you sure you want to export unencrypted passwords?" = "你確定要匯出未加密的密碼?";
"Icon only" = "僅圖示";
"Export" = "匯出";
"Import" = "匯入";
"Platform" = "平台";
"Custom Location already exists" = "自定義站台已存在";
"Custom Location selected for import" = "選擇導入的自定義站台";
"Custom Location not selected for import" = "未選擇導入的自定義站台";
"Account already exists" = "帳戶已存在";
"Password saved in import file" = "密碼已儲存在匯入文件";
"Export in progress..." = "匯出中...";
"In progress..." = "進行中…";
" subscribers" = " 訂閱者";
"10 seconds forwards/backwards" = "前放/回放10秒";
"Are you sure you want to clear history of watched videos?" = "確定要清除播放歷史?";
"Are you sure you want to clear search history?" = "確定要清除搜尋歷史?";
"Are you sure you want to delete playlist?" = "確定要刪除播放清單?";
"Are you sure you want to restore default quality profiles?" = "確定要回復預設質量配置?";
"Badge" = "標記";
"Badge & Decreased opacity" = "標記及降低透明度";
"Badge color" = "標記顏色";
"Based on system color scheme" = "根據系統配置";
"Battery" = "電池";
"Blue" = "藍";
"Browsing" = "瀏覽";
"Buffering stream..." = "緩衝中...";
"Bugs and great feature ideas can be sent to the GitHub issues tracker. " = "程式錯誤及出色的功能構思也可以在 GitHub 問題追蹤介面提出。 ";
"Cancel" = "取消";
"Button" = "按鈕";
"Captions" = "字幕";
"Category" = "類別";
"Close PiP and open player when application enters foreground" = "當應用程式進入前台時,關閉 PiP 並打開播放器";
"Close PiP when starting playing other video" = "當播放其他視頻時,關閉 PiP";
"Comments" = "留言";
"Country" = "國家";
"When partially watched video is played" = "播放未完全觀看視頻時";
"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" = "請勿與任何人共用此文件,否則您可能會失去對帳戶的存取權限。如果您不選擇匯出密碼,系統將要求您在匯入過程中提供密碼";

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
{
"originHash" : "9899ef48b3ee49eae175e25421b8330438e40c30a266d96473b299a6ab7c4188",
"pins" : [
{
"identity" : "activelabel.swift",
@@ -14,8 +15,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/Alamofire/Alamofire.git",
"state" : {
"revision" : "3dc6a42c7727c49bf26508e29b0a0b35f9c7e1ad",
"version" : "5.8.1"
"revision" : "f455c2975872ccd2d9c81594c658af65716e9b9a",
"version" : "5.9.1"
}
},
{
@@ -24,7 +25,7 @@
"location" : "https://github.com/hyperoslo/Cache.git",
"state" : {
"branch" : "master",
"revision" : "d048bf404a5c8362c6cf840c2096d5777975cd27"
"revision" : "f44a8f6b5ec27730198725ccc542fef0d1cc6b3d"
}
},
{
@@ -59,7 +60,6 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/cxfksword/MPVKit.git",
"state" : {
"branch" : "main",
"revision" : "dca1e345a26d09a3d621d7656a94e6427f3f7b83"
}
},
@@ -86,8 +86,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/ashleymills/Reachability.swift",
"state" : {
"revision" : "c01bbdf2d633cf049ae1ed1a68a2020a8bda32e2",
"version" : "5.1.0"
"revision" : "7b7018a69c84ea94ac2a38dff626e16ea81d1db9",
"version" : "5.2.1"
}
},
{
@@ -105,7 +105,7 @@
"location" : "https://github.com/SDWebImage/SDWebImage",
"state" : {
"branch" : "master",
"revision" : "fbe79221b146aa6647dceb5a5c75873a48b69519"
"revision" : "f6afa0132961d593f07970d84e2d8b588c29ea04"
}
},
{
@@ -122,8 +122,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImageSwiftUI.git",
"state" : {
"revision" : "aee64ef39b570c44ccf0f884c440fc6494a23c76",
"version" : "2.2.5"
"revision" : "53573d6dd017e354c0e7d8f1c86b77ef1383c996",
"version" : "2.2.7"
}
},
{
@@ -131,8 +131,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImageWebPCoder.git",
"state" : {
"revision" : "db4603921b31a6ce0f8c26d36d6a3fffc2dba481",
"version" : "0.14.2"
"revision" : "8a33fb3ca75a01267f775f891f7d61f675e95072",
"version" : "0.14.5"
}
},
{
@@ -176,8 +176,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/SwiftyJSON/SwiftyJSON.git",
"state" : {
"revision" : "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07",
"version" : "5.0.1"
"revision" : "af76cf3ef710b6ca5f8c05f3a31307d44a3c5828",
"version" : "5.0.2"
}
}
],

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

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