Compare commits

..

78 Commits

Author SHA1 Message Date
AAGaming 064c897c86 Merge branch 'main' into aa/type-cleanup-py 2023-10-22 19:58:05 -04:00
marios 2f46e0dc3e Update lint.yml 2023-10-20 17:10:33 +03:00
marios e363c677a0 Update edit-check.yml 2023-10-20 17:10:19 +03:00
dependabot[bot] f94a1f97df Bump @babel/traverse from 7.22.5 to 7.23.2 in /frontend (#550) 2023-10-20 13:58:00 +00:00
marios8543 e0592f0959 fix uninstall bug 2023-10-17 18:59:13 +03:00
marios8543 0736a6f995 fix bad type on store.tsx 2023-10-17 16:53:13 +03:00
marios d454a404a0 Merge branch 'main' into aa/type-cleanup-py 2023-10-17 16:50:13 +03:00
marios8543 ffbc79d919 fix bad type on store.tsx 2023-10-17 16:46:53 +03:00
Party Wumpus a2312256b3 fix typo
this is what i get for commiting to main 😔
2023-10-17 16:42:39 +03:00
Party Wumpus 2cdb49168c fix logical error when no store was set 2023-10-17 16:42:39 +03:00
marios8543 944e0e6e07 Fix decky_plugin on windows CI 2023-10-17 16:39:45 +03:00
Party Wumpus f53a3f383d fix typo
this is what i get for commiting to main 😔
2023-10-17 13:52:11 +01:00
Party Wumpus 407e647993 fix logical error when no store was set 2023-10-17 13:44:44 +01:00
marios8543 4a17474133 fix decky_plugin path in pyinstaller 2023-10-11 23:46:26 +03:00
AAGaming ade7cb7640 fix paths 2023-09-30 13:15:35 -04:00
AAGaming e8cbeb1805 oops 2023-09-30 12:46:48 -04:00
AAGaming 4b89fc1f9d fix broken import 2023-09-30 12:43:35 -04:00
AAGaming 00e10be93f fix ci (hopefully, because act wont work) 2023-09-30 12:42:02 -04:00
AAGaming b7043655b3 speed up stupid make 2023-09-30 12:36:17 -04:00
WerWolv 75ae7dfe69 Moved locales folder and requirements.txt 2023-09-26 14:58:27 +02:00
WerWolv a0d50baaca Moved main.py 2023-09-26 14:55:09 +02:00
WerWolv 0c2079fa85 Moved backend entirely into the backend folder 2023-09-26 14:54:52 +02:00
AAGaming 3960d28b06 with, not env 2023-09-25 13:37:28 -04:00
AAGaming 2d68809c1b run lint and typecheck on PRs 2023-09-25 13:37:15 -04:00
AAGaming b81c41f667 remove quotes on some types 2023-09-25 13:28:15 -04:00
AAGaming e22cc6269d make ci title consistent 2023-09-25 13:24:31 -04:00
AAGaming 300885f724 move type checking to other workflow, fix TS errors, add TSC checking 2023-09-25 13:23:38 -04:00
AAGaming 5838ddca56 add pyright ci 2023-09-25 13:09:33 -04:00
AAGaming f6401f4995 move to module imports 2023-09-25 13:06:46 -04:00
marios8543 75fbc7524f type hints on main,plugin,updater,utilites.localsocket 2023-09-25 11:27:36 -04:00
AAGaming ecc5f5c2fa begin adding static types to backend code 2023-09-25 11:27:36 -04:00
AAGaming ae399b8c0e remove useless main.py imports 2023-09-25 11:27:36 -04:00
jurassicplayer 22d579512d Preserve plugin order when reinstalling/updating (#530) 2023-08-28 07:00:37 -07:00
Marco Rodolfi caf4d75a06 Fix for SELinux handling logic (#529)
* Fix for SELinux handling logic

The old procedure was crashing with signal 9 SIGKILL, this should fix that problem
2023-08-26 19:00:02 +02:00
Marco Rodolfi a43e4328df Rollback to Python 3.10.6 for possible regression 2023-08-25 19:59:56 +02:00
TrainDoctor 0ede024771 Update README.md 2023-08-25 10:36:55 -07:00
dependabot[bot] 193f97d9fe Bump certifi from 2022.12.7 to 2023.7.22 (#526)
Bumps [certifi](https://github.com/certifi/python-certifi) from 2022.12.7 to 2023.7.22.
- [Commits](https://github.com/certifi/python-certifi/compare/2022.12.07...2023.07.22)

---
updated-dependencies:
- dependency-name: certifi
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-16 22:07:13 -07:00
WerWolvTranslationBot 38c96ea96a Translations update from Weblate (#520)
Co-authored-by: pontifex91 <pontifexrus@gmail.com>
Co-authored-by: Sean <zhangshuyan@fuji.waseda.jp>
Co-authored-by: Apostolos Grammatopoulos <greatapo@gmail.com>
2023-08-11 21:15:32 -07:00
suchmememanyskill dd130dbbd7 Only keep up to 5 recent logs of runs of plugins (#525) 2023-08-11 23:02:30 +01:00
suchmememanyskill 9233495cac Split windows workflow (#524)
* Split win actions workflow

* Create console-less win build
2023-08-10 14:46:48 +02:00
AAGaming e4001966e8 fix dumb error in plugin install if the hash doesn't match 2023-08-05 17:04:15 -04:00
WerWolvTranslationBot c52f1cd038 Translations update from Weblate (#501)
* Added translation using Weblate (Polish)

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Translated using Weblate (Czech)

Currently translated at 100.0% (135 of 135 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/cs/

* Translated using Weblate (Polish)

Currently translated at 99.2% (134 of 135 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/pl/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (135 of 135 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/zh_Hans/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (135 of 135 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/zh_Hant/

* Translated using Weblate (Polish)

Currently translated at 100.0% (135 of 135 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/pl/

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Translated using Weblate (Polish)

Currently translated at 100.0% (135 of 135 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/pl/

* Translated using Weblate (Polish)

Currently translated at 100.0% (135 of 135 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/pl/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (135 of 135 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/pt_BR/

* Translated using Weblate (Italian)

Currently translated at 100.0% (137 of 137 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/it/

* Translated using Weblate (Polish)

Currently translated at 100.0% (137 of 137 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/pl/

* Translated using Weblate (Korean)

Currently translated at 100.0% (137 of 137 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/ko/

* Added translation using Weblate (Finnish)

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Translated using Weblate (Italian)

Currently translated at 100.0% (139 of 139 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/it/

* Translated using Weblate (Polish)

Currently translated at 100.0% (139 of 139 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/pl/

* Translated using Weblate (Finnish)

Currently translated at 30.9% (43 of 139 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/fi/

* Translated using Weblate (Finnish)

Currently translated at 100.0% (139 of 139 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/fi/

* Translated using Weblate (Korean)

Currently translated at 100.0% (139 of 139 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/ko/

* Translated using Weblate (Czech)

Currently translated at 100.0% (139 of 139 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/cs/

---------

Co-authored-by: Eryk Pawlikowski <eryk5188@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Meiton <michal.salati@gmail.com>
Co-authored-by: david082321 <david082321@yahoo.com.tw>
Co-authored-by: re.sub(r'p', 'l', 'capslock') <admin@calslock.net>
Co-authored-by: Paulo Victor de Lima Sfair Alvares <pvsfair@gmail.com>
Co-authored-by: Marco Rodolfi <marco.rodolfi@tuta.io>
Co-authored-by: Sungjoon Moon <sumoon@seoulsaram.org>
Co-authored-by: Vinski Lång <vinski.lang@gmail.com>
Co-authored-by: Vinski Lång <53524661+Vizitys@users.noreply.github.com>
2023-07-30 10:49:25 +02:00
Party Wumpus 2ba9bce3de Make the updater work properly on SELinux (#518)
* Add DECKY_SELINUX env var

* if on selinux make binary executable with chcon

* No need to recursively change one file
2023-07-29 09:05:39 +01:00
Marco Rodolfi d4a76da78c [Need Testing] Actually fix sqlite 3 issues (#515)
* Properly fix sqlite issues

* Revert python downgrade

* Horrible hack to update SQLite to the latest version in the Ubuntu VMs

* Cleanup build script

* Fix yaml formatting

* Fix typos

* Use sudo for installing binary

* Fix library path

* Wrong naming

* Wrong name again

* Small stylisting fixes

* Missed a space
2023-07-27 18:47:46 +02:00
Beebles c7e4eb1b3f Add Custom TitleView (#512)
* feat(titleView): Add Custom TitleView support

* fix: wrap TitleView in Focusable

* fix: remove root div on TitleView
2023-07-27 14:58:21 +01:00
Marco Rodolfi 5460f95eac Latest builds of Python 3.10 already uses newer version of the SQLite library
According to the changelog, the latest version that uses > 3.37 is Python 3.10.9, so switch back to an older version of it until Ubuntu pick up more recent versions of SQLite
2023-07-27 09:50:22 +02:00
Marco Rodolfi 3ae4ceb431 Switch back to Python 3.10 in order to avoid library dependency hell 2023-07-27 09:38:32 +02:00
Marco Rodolfi 7a725935fc Slightly downgrade to 22.10 2023-07-27 09:33:14 +02:00
Marco Rodolfi 9437d7ed99 Bump it again to 23.04 2023-07-27 09:29:45 +02:00
Marco Rodolfi 34cf24f7c0 Update ubuntu image to fix sqlite missing function 2023-07-27 09:20:40 +02:00
Marco Rodolfi 5a9959f70f Properly fix sqlite issues (#514) 2023-07-26 14:54:21 -07:00
Party Wumpus 96069d3299 change issue dicord link to decky.xyz/discord 2023-07-26 14:10:28 +01:00
Party Wumpus b4c90683aa typo in bug report template 2023-07-26 14:09:05 +01:00
Marco Rodolfi 6993516ccb Bugfix: Unable to load _sqlite3 on main SteamOS (#507)
* Update to latest python

I have odd behaviour with importing sqlite3, which is failing to do. I have no clue why, so I'm trying to update Python to the latest stable to check if it's a Python bug.

* Update aiohttp for python compatibility

* Sligtly lower aiohttp version

* Update pyinstaller to latest stable version

It was failing to build a working executable with the latest python runtime.
2023-07-23 19:30:54 -07:00
Party Wumpus 37c1a0e964 Ignore chmod if decky is not run as root (#510)
* Ignore chmod if decky is not run as root

* I can't read

* i managed to make a mistake on 2/3 lines i edited....

* add warning on startup

* logger.warn is depreciated

* Update localplatformlinux.py
2023-07-21 23:00:08 +01:00
EMERALD 6d086fb5d5 Add testing store info to browse tab (#504) 2023-07-12 16:52:32 -07:00
fero 7c805e9b80 Add descriptions to Decky titleview DialogButtons (#502)
* chore: add onOKActionDescriptions to decky titleview

This is in preparation for beebles' custom titleview. Since plugins may reuse the same icons in their custom titleviews, it will be a good practice to disambiguate their meanings. In the Steam UI, any icon button has a matching description.

* chore: implement it using the translation framework

---------

Co-authored-by: Marco Rodolfi <marco.rodolfi@tuta.io>
2023-07-09 09:43:00 +01:00
WerWolvTranslationBot 6b3f9e4a9e Translations update from Weblate (#500)
* Added translation using Weblate (Polish)

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Translated using Weblate (Czech)

Currently translated at 100.0% (135 of 135 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/cs/

* Translated using Weblate (Polish)

Currently translated at 99.2% (134 of 135 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/pl/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (135 of 135 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/zh_Hans/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (135 of 135 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/zh_Hant/

* Translated using Weblate (Polish)

Currently translated at 100.0% (135 of 135 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/pl/

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

---------

Co-authored-by: Eryk Pawlikowski <eryk5188@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Meiton <michal.salati@gmail.com>
Co-authored-by: david082321 <david082321@yahoo.com.tw>
2023-07-04 19:46:51 +02:00
AAGaming dea08868d3 fix router hook recursively wrapping routes when patched multiple times 2023-07-03 23:57:53 -04:00
WerWolvTranslationBot 365866c35f Translations update from Weblate (#497)
* Translated using Weblate (Italian)

Currently translated at 100.0% (135 of 135 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/it/

* Added translation using Weblate (Bulgarian)

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (135 of 135 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/bg/

* Translated using Weblate (Russian)

Currently translated at 100.0% (135 of 135 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/ru/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (135 of 135 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/zh_Hans/

* Translated using Weblate (Korean)

Currently translated at 100.0% (135 of 135 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/ko/

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

---------

Co-authored-by: Marco Rodolfi <marco.rodolfi@tuta.io>
Co-authored-by: Lyubomir Vasilev <lyubomirv@gmx.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Andrew <www.andru90@gmail.com>
Co-authored-by: Sean <zhangshuyan@fuji.waseda.jp>
Co-authored-by: Sungjoon Moon <sumoon@seoulsaram.org>
2023-07-01 14:07:33 +02:00
suchmememanyskill 3d6d69568d Make sure settings/data/logs gets created as user (#499) 2023-07-01 11:46:58 +01:00
WerWolvTranslationBot 62a2107c06 Translations update from Weblate (#494)
* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (135 of 135 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/zh_Hans/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (135 of 135 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/pt_BR/

* Translated using Weblate (Italian)

Currently translated at 100.0% (138 of 138 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/it/

---------

Co-authored-by: Sean <zhangshuyan@fuji.waseda.jp>
Co-authored-by: Paulo Victor de Lima Sfair Alvares <pvsfair@gmail.com>
Co-authored-by: Marco Rodolfi <marco.rodolfi@tuta.io>
2023-06-26 09:11:42 +02:00
Marco Rodolfi cf2f419942 Translated using Weblate (Italian)
Currently translated at 100.0% (138 of 138 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/it/
2023-06-26 09:00:36 +02:00
Paulo Victor de Lima Sfair Alvares f657529ab5 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (135 of 135 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/pt_BR/
2023-06-26 08:59:18 +02:00
Sean 8eae2d60e5 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (135 of 135 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/zh_Hans/
2023-06-26 08:59:18 +02:00
Marco Rodolfi 3e64e53cd7 feat: Added detailed message for permission error and clean up english language from unused strings. 2023-06-26 08:53:41 +02:00
Jonas Dellinger ef9afa8cbc Add notification settings, which allows muting decky/plugin toast notifications (#479)
* Add notification settings, which allows muting decky/plugin toast notifications

* Fix typos
2023-06-24 12:59:39 +02:00
Marco Rodolfi 143461d597 chore: clean up unused parameters 2023-06-22 17:32:20 +02:00
Marco Rodolfi ae887e10d6 chore: adding parameters to file picker in logical order 2023-06-22 17:25:38 +02:00
WerWolvTranslationBot 186c70591a Translations update from Weblate (#489)
* Added translation using Weblate (Dutch)

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Translated using Weblate (English)

Currently translated at 100.0% (134 of 134 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/en/

* Translated using Weblate (Italian)

Currently translated at 100.0% (134 of 134 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/it/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 99.2% (133 of 134 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/zh_Hans/

* Translated using Weblate (Korean)

Currently translated at 100.0% (134 of 134 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/ko/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (134 of 134 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/nl/

* Translated using Weblate (Korean)

Currently translated at 100.0% (134 of 134 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/ko/

---------

Co-authored-by: cardiognostix <wardeh@dds.nl>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Marco Rodolfi <marco.rodolfi@tuta.io>
Co-authored-by: Sean <zhangshuyan@fuji.waseda.jp>
Co-authored-by: Sungjoon Moon <sumoon@seoulsaram.org>
2023-06-22 13:48:24 +02:00
Marco Rodolfi 777e7893e3 fix: handle missing backend extension list 2023-06-22 13:16:37 +02:00
Marco Rodolfi b2a1b172e2 Fix: export V2 to plugins 2023-06-22 12:54:41 +02:00
Marco Rodolfi fc72ac5c63 fix: cleanup code 2023-06-22 12:16:23 +02:00
Marco Rodolfi f1576c7798 Chore: Better logical order for file picker v2 function call 2023-06-22 11:58:00 +02:00
Marco Rodolfi 388526d02d Fix: add an API compatibility layer for the old file picker and change the new implementation as V2 2023-06-22 11:37:45 +02:00
Marco Rodolfi cb65fb4b11 Fix: wrong condition on show select folder 2023-06-22 10:45:17 +02:00
Marco Rodolfi b82c9cf6e6 Chore: clean up testing values pt 2 2023-06-21 18:37:19 +02:00
Marco Rodolfi 04fff476d5 Cleaning up debug values in developer installer 2023-06-21 07:33:38 +02:00
66 changed files with 2982 additions and 1037 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ body:
- label: I have searched existing issues
- label: This issue is not a duplicate of an existing one
- label: I have checked the [common issues section in the readme file](https://github.com/SteamDeckHomebrew/decky-loader#-common-issues)
- label: I have attached logs to this bug report (failure to include logs will mean your issue will not be responded too).
- label: I have attached logs to this bug report (failure to include logs will mean your issue may not be responded to).
- type: textarea
attributes:
+1 -1
View File
@@ -1,5 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Steam Deck Homebrew Discord Server
url: https://discord.gg/ZU74G2NJzk
url: https://decky.xyz/discord
about: Please ask and answer questions here.
+59
View File
@@ -0,0 +1,59 @@
name: Builder Win
on:
push:
pull_request:
permissions:
contents: write
jobs:
build-win:
name: Build PluginLoader for Win
runs-on: windows-2022
steps:
- name: Checkout 🧰
uses: actions/checkout@v3
- name: Set up NodeJS 18 💎
uses: actions/setup-node@v3
with:
node-version: 18
- name: Set up Python 3.11.4 🐍
uses: actions/setup-python@v4
with:
python-version: "3.11.4"
- name: Install Python dependencies ⬇️
working-directory: ./backend
run: |
python -m pip install --upgrade pip
pip install pyinstaller==5.13.0
pip install -r requirements.txt
- name: Install JS dependencies ⬇️
working-directory: ./frontend
run: |
npm i -g pnpm
pnpm i --frozen-lockfile
- name: Build JS Frontend 🛠️
working-directory: ./frontend
run: pnpm run build
- name: Build Python Backend 🛠️
run: pyinstaller --noconfirm --onefile --name "PluginLoader" --add-data "./backend/static;/static" --add-data "./backend/locales;/locales" --add-data "./backend/src/legacy;/src/legacy" --add-data "./plugin/*;/" --hidden-import=sqlite3 ./backend/main.py
- name: Build Python Backend (noconsole) 🛠️
run: pyinstaller --noconfirm --noconsole --onefile --name "PluginLoader_noconsole" --add-data "./backend/static;/static" --add-data "./backend/locales;/locales" --add-data "./backend/src/legacy;/src/legacy" --add-data "./plugin/*;/" --hidden-import=sqlite3 ./backend/main.py
- name: Upload package artifact ⬆️
uses: actions/upload-artifact@v3
with:
name: PluginLoader Win
path: |
./dist/PluginLoader.exe
./dist/PluginLoader_noconsole.exe
+35 -60
View File
@@ -31,7 +31,7 @@ permissions:
jobs:
build:
name: Build PluginLoader
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- name: Print input
@@ -47,65 +47,33 @@ jobs:
with:
node-version: 18
- name: Set up Python 3.10.2 🐍
- name: Set up Python 3.10.6 🐍
uses: actions/setup-python@v4
with:
python-version: "3.10.2"
python-version: "3.10.6"
- name: Upgrade SQLite 3 binary version to 3.42.0 🧑‍💻
run: >
cd /tmp &&
wget "https://www.sqlite.org/2023/sqlite-autoconf-3420000.tar.gz" &&
tar -xvzf sqlite-autoconf-3420000.tar.gz &&
cd /tmp/sqlite-autoconf-3420000 &&
./configure --prefix=/usr --disable-static CFLAGS="-g" CPPFLAGS="$CPPFLAGS -DSQLITE_ENABLE_COLUMN_METADATA=1 \
-DSQLITE_ENABLE_UNLOCK_NOTIFY -DSQLITE_ENABLE_DBSTAT_VTAB=1 -DSQLITE_ENABLE_FTS3_TOKENIZER=1 \
-DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_SECURE_DELETE -DSQLITE_ENABLE_STMTVTAB -DSQLITE_MAX_VARIABLE_NUMBER=250000 \
-DSQLITE_MAX_EXPR_DEPTH=10000 -DSQLITE_ENABLE_MATH_FUNCTIONS" &&
make -j$(nproc) &&
sudo make install &&
sudo cp /usr/lib/libsqlite3.so /usr/lib/x86_64-linux-gnu/ &&
sudo cp /usr/lib/libsqlite3.so.0 /usr/lib/x86_64-linux-gnu/ &&
sudo cp /usr/lib/libsqlite3.so.0.8.6 /usr/lib/x86_64-linux-gnu/ &&
rm -r /tmp/sqlite-autoconf-3420000
- name: Install Python dependencies ⬇️
working-directory: ./backend
run: |
python -m pip install --upgrade pip
pip install pyinstaller==5.5
[ -f requirements.txt ] && pip install -r requirements.txt
- name: Install JS dependencies ⬇️
working-directory: ./frontend
run: |
npm i -g pnpm
pnpm i --frozen-lockfile
- name: Build JS Frontend 🛠️
working-directory: ./frontend
run: pnpm run build
- name: Build Python Backend 🛠️
run: pyinstaller --noconfirm --onefile --name "PluginLoader" --add-data ./backend/static:/static --add-data ./backend/locales:/locales --add-data ./backend/legacy:/legacy --add-data ./plugin:/plugin ./backend/*.py
- name: Upload package artifact ⬆️
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v3
with:
name: PluginLoader
path: ./dist/PluginLoader
- name: Download package artifact locally
if: ${{ env.ACT }}
uses: actions/upload-artifact@v3
with:
path: ./dist/PluginLoader
build-win:
name: Build PluginLoader for Win
runs-on: windows-2022
steps:
- name: Checkout 🧰
uses: actions/checkout@v3
- name: Set up NodeJS 18 💎
uses: actions/setup-node@v3
with:
node-version: 18
- name: Set up Python 3.10.2 🐍
uses: actions/setup-python@v4
with:
python-version: "3.10.2"
- name: Install Python dependencies ⬇️
run: |
python -m pip install --upgrade pip
pip install pyinstaller==5.5
pip install pyinstaller==5.13.0
pip install -r requirements.txt
- name: Install JS dependencies ⬇️
@@ -119,13 +87,20 @@ jobs:
run: pnpm run build
- name: Build Python Backend 🛠️
run: pyinstaller --noconfirm --onefile --name "PluginLoader" --add-data "./backend/static;/static" --add-data "./backend/locales;/locales" --add-data "./backend/legacy;/legacy" --add-data "./plugin;/plugin" ./backend/main.py
run: pyinstaller --noconfirm --onefile --name "PluginLoader" --add-data ./backend/static:/static --add-data ./backend/locales:/locales --add-data ./backend/src/legacy:/src/legacy --add-data ./plugin/*:/ --hidden-import=sqlite3 ./backend/main.py
- name: Upload package artifact ⬆️
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v3
with:
name: PluginLoader Win
path: ./dist/PluginLoader.exe
name: PluginLoader
path: ./dist/PluginLoader
- name: Download package artifact locally
if: ${{ env.ACT }}
uses: actions/upload-artifact@v3
with:
path: ./dist/PluginLoader
release:
name: Release stable version of the package
@@ -153,7 +128,7 @@ jobs:
- name: Get latest release
uses: rez0n/actions-github-release@main
id: latest_release
env:
with:
token: ${{ secrets.GITHUB_TOKEN }}
repository: "SteamDeckHomebrew/decky-loader"
type: "nodraft"
@@ -232,7 +207,7 @@ jobs:
- name: Get latest release
uses: rez0n/actions-github-release@main
id: latest_release
env:
with:
token: ${{ secrets.GITHUB_TOKEN }}
repository: "SteamDeckHomebrew/decky-loader"
type: "nodraft"
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Get changed files
id: changed-files
+11 -5
View File
@@ -2,6 +2,7 @@ name: Lint
on:
push:
pull_request:
jobs:
lint:
@@ -9,9 +10,14 @@ jobs:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2 # Check out the repository first.
- name: Run prettier (JavaScript & TypeScript)
- uses: actions/checkout@v3 # Check out the repository first.
- name: Install TypeScript dependencies
working-directory: frontend
run: |
pushd frontend
npm install
npm run lint
npm i -g pnpm
pnpm i --frozen-lockfile
- name: Run prettier (TypeScript)
working-directory: frontend
run: pnpm run lint
+36
View File
@@ -0,0 +1,36 @@
name: Type Check
on:
push:
pull_request:
jobs:
typecheck:
name: Run type checkers
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2 # Check out the repository first.
- name: Install Python dependencies
working-directory: backend
run: |
python -m pip install --upgrade pip
[ -f requirements.txt ] && pip install -r requirements.txt
- name: Install TypeScript dependencies
working-directory: frontend
run: |
npm i -g pnpm
pnpm i --frozen-lockfile
- name: Run pyright (Python)
uses: jakebailey/pyright-action@v1
with:
python-version: "3.10.6"
no-comments: true
working-directory: backend
- name: Run tsc (TypeScript)
working-directory: frontend
run: $(pnpm bin)/tsc --noEmit
+1
View File
@@ -35,6 +35,7 @@ For more information about Decky Loader as well as documentation and development
### 🤔 Common Issues
- Syncthing may use port 8080 on Steam Deck, which Decky Loader needs to function. If you are using Syncthing as a service, please change its port to something else.
- 8384 is the recommended port for Syncthing.
- If you are using any software that uses port 1337 or 8080, please change its port to something else or uninstall it.
- Sometimes Decky will disappear on SteamOS updates. This can easily be fixed by just re-running the installer and installing the stable branch again. If this doesn't work, try installing the prerelease instead. If that doesn't work, then [check the existing issues](https://github.com/SteamDeckHomebrew/decky-loader/issues) and if there isn't one then you can [file a new issue](https://github.com/SteamDeckHomebrew/decky-loader/issues/new?assignees=&labels=bug&template=bug_report.yml&title=%5BBUG%5D+%3Ctitle%3E).
+252
View File
@@ -0,0 +1,252 @@
{
"BranchSelect": {
"update_channel": {
"stable": "Стабилен",
"testing": "Тестване",
"label": "Канал за обновления",
"prerelease": "Предварителни издания"
}
},
"Developer": {
"5secreload": "Презареждане след 5 секунди",
"disabling": "Изключване на React DevTools",
"enabling": "Включване на React DevTools"
},
"DropdownMultiselect": {
"button": {
"back": "Назад"
}
},
"FilePickerError": {
"errors": {
"unknown": "Възникна неизвестна грешка. Грешката в суров вид е: {{raw_error}}",
"file_not_found": "Посоченият път е неправилен. Проверете го и го въведете правилно.",
"perm_denied": "Нямате достъп до посочената папка. Проверете дали потребителят (deck на Steam Deck) има съответните правомощия за достъп до посочената папка/файл."
}
},
"FilePickerIndex": {
"file": {
"select": "Избиране на този файл"
},
"files": {
"all_files": "Всички файлове",
"file_type": "Файлов тип",
"show_hidden": "Показване на скритите файлове"
},
"filter": {
"created_asce": "Дата на създаване (първо най-старите)",
"created_desc": "Дата на създаване (първо най-новите)",
"modified_asce": "Дата на промяна (първо най-старите)",
"modified_desc": "Дата на промяна (първо най-новите)",
"name_asce": "Я-А",
"name_desc": "А-Я",
"size_asce": "Размер (първо най-малките)",
"size_desc": "Размер (първо най-големите)"
},
"folder": {
"label": "Папка",
"show_more": "Показване на още файлове",
"select": "Използване на тази папка"
}
},
"MultiplePluginsInstallModal": {
"description": {
"install": "Инсталиране на {{name}} {{version}}",
"reinstall": "Преинсталиране на {{name}} {{version}}",
"update": "Обновяване на {{name}} до {{version}}"
},
"ok_button": {
"idle": "Потвърждаване",
"loading": "В процес на работа"
},
"title": {
"mixed_one": "Промяна на {{count}} добавка",
"mixed_other": "Промяна на {{count}} добавки",
"update_one": "Обновяване на 1 добавка",
"update_other": "Обновяване на {{count}} добавки",
"install_one": "Инсталиране на 1 добавка",
"install_other": "Инсталиране на {{count}} добавки",
"reinstall_one": "Преинсталиране на 1 добавка",
"reinstall_other": "Преинсталиране на {{count}} добавки"
},
"confirm": "Наистина ли искате да направите следните промени?"
},
"PluginCard": {
"plugin_full_access": "Тази добавка има пълен достъп до Вашия Steam Deck.",
"plugin_install": "Инсталиране",
"plugin_no_desc": "Няма описание.",
"plugin_version_label": "Версия на добавката"
},
"PluginInstallModal": {
"install": {
"button_idle": "Инсталиране",
"desc": "Наистина ли искате да инсталирате {{artifact}} {{version}}?",
"title": "Инсталиране на {{artifact}}",
"button_processing": "Инсталиране"
},
"reinstall": {
"button_idle": "Преинсталиране",
"button_processing": "Преинсталиране",
"desc": "Наистина ли искате да преинсталирате {{artifact}} {{version}}?",
"title": "Преинсталиране на {{artifact}}"
},
"update": {
"button_idle": "Обновяване",
"title": "Обновяване на {{artifact}}",
"button_processing": "Обновяване",
"desc": "Наистина ли искате да обновите {{artifact}} {{version}}?"
},
"no_hash": "Тази добавка няма хеш. Инсталирате я на свой собствен риск."
},
"PluginListIndex": {
"hide": "Бърз достъп: Скриване",
"no_plugin": "Няма инсталирани добавки!",
"plugin_actions": "Действия с добавката",
"reinstall": "Преинсталиране",
"uninstall": "Деинсталиране",
"update_to": "Обновяване до {{name}}",
"reload": "Презареждане",
"show": "Бърз достъп: Показване",
"update_all_one": "Обновяване на 1 добавка",
"update_all_other": "Обновяване на {{count}} добавки"
},
"PluginListLabel": {
"hidden": "Скрито от менюто за бърз достъп"
},
"PluginLoader": {
"decky_title": "Decky",
"error": "Грешка",
"plugin_load_error": {
"message": "Грешка при зареждането на добавката {{name}}",
"toast": "Грешка при зареждането на {{name}}"
},
"plugin_uninstall": {
"button": "Деинсталиране",
"desc": "Наистина ли искате да деинсталирате {{name}}?",
"title": "Деинсталиране на {{name}}"
},
"plugin_update_one": "Има налично обновление за 1 добавка!",
"plugin_update_other": "Има налични обновления за {{count}} добавки!",
"decky_update_available": "Има налично обновление до {{tag_name}}!",
"plugin_error_uninstall": "Зареждането на {{name}} предизвика грешка, както се вижда по-горе. Това обикновено означава, че добавката изисква обновяване на новата версия на SteamUI. Проверете дали има обновление или изберете да я премахнете в настройките на Decky, в раздела с добавките."
},
"RemoteDebugging": {
"remote_cef": {
"desc": "Разрешаване на достъп без удостоверяване до дебъгера на CEF на всеки от Вашата мрежа",
"label": "Разрешаване на отдалеченото дебъгване на CEF"
}
},
"SettingsDeveloperIndex": {
"cef_console": {
"button": "Отваряне на конзолата",
"label": "Конзола на CEF",
"desc": "Отваря конзолата на CEF. Това има смисъл единствено за дебъгване. Нещата тук може да са опасни и трябва да бъдат използвани само ако Вие сте разработчик на добавка, или получавате насоки от такъв."
},
"header": "Други",
"react_devtools": {
"ip_label": "IP",
"label": "Включване на React DevTools",
"desc": "Включва свързването към компютър, на който работи React DevTools. Промяната на тази настройка ще презареди Steam. Задайте IP адреса преди да включите това."
},
"third_party_plugins": {
"button_install": "Инсталиране",
"button_zip": "Разглеждане",
"header": "Добавки от външен източник",
"label_desc": "Адрес",
"label_zip": "Инсталиране на добавка от файл ZIP",
"label_url": "Инсталиране на добавка от адрес в Интернет"
},
"valve_internal": {
"desc2": "Не пипайте нищо в това меню, освен ако не знаете какво правите.",
"label": "Включване на вътрешното меню на Valve",
"desc1": "Включва вътрешното меню за разработчици на Valve."
}
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Версия на Decky",
"header": "Относно"
},
"developer_mode": {
"label": "Режим за разработчици"
},
"notifications": {
"decky_updates_label": "Има налично обновление на Decky",
"header": "Известия",
"plugin_updates_label": "Има налични обновления на добавките"
},
"other": {
"header": "Други"
},
"updates": {
"header": "Обновления"
},
"beta": {
"header": "Участие в бета-версии"
}
},
"SettingsIndex": {
"developer_title": "Разработчик",
"general_title": "Общи",
"plugins_title": "Добавки"
},
"Store": {
"store_contrib": {
"label": "Допринасяне",
"desc": "Ако искате да допринесете към магазина за добавки на Decky, разгледайте хранилището SteamDeckHomebrew/decky-plugin-template в GitHub. Може да намерите информация относно разработката и разпространението във файла README."
},
"store_filter": {
"label": "Филтър",
"label_def": "Всички"
},
"store_search": {
"label": "Търсене"
},
"store_sort": {
"label": "Подредба",
"label_def": "Последно обновление (първо най-новите)"
},
"store_source": {
"label": "Изходен код",
"desc": "Целият изходен код е наличен в хранилището SteamDeckHomebrew/decky-plugin-database в GitHub."
},
"store_tabs": {
"about": "Относно",
"alph_asce": "По азбучен ред (Я -> А)",
"alph_desc": "По азбучен ред (А -> Я)",
"title": "Разглеждане"
},
"store_testing_cta": "Помислете дали искате да тествате новите добавки, за да помогнете на екипа на Decky Loader!"
},
"StoreSelect": {
"custom_store": {
"label": "Персонализиран магазин",
"url_label": "Адрес"
},
"store_channel": {
"custom": "Персонализиран",
"default": "По подразбиране",
"label": "Канал за магазина",
"testing": "Тестване"
}
},
"Updater": {
"decky_updates": "Обновления на Decky",
"patch_notes_desc": "Бележки за промените",
"updates": {
"check_button": "Проверка за обновления",
"checking": "Проверяване",
"cur_version": "Текуща версия: {{ver}}",
"label": "Обновления",
"lat_version": "Използвате най-новата версия: {{ver}}",
"reloading": "Презареждане",
"updating": "Обновяване",
"install_button": "Инсталиране на обновлението"
},
"no_patch_notes_desc": "няма бележки за промените в тази версия"
},
"PluginView": {
"hidden_one": "1 добавка е скрита от този списък",
"hidden_other": "{{count}} добавки са скрити от този списък"
}
}
+48 -9
View File
@@ -14,7 +14,27 @@
},
"FilePickerIndex": {
"folder": {
"select": "Použít tuto složku"
"select": "Použít tuto složku",
"label": "Složka",
"show_more": "Zobrazit více souborů"
},
"filter": {
"created_asce": "Vytvořeno (Nejstarší)",
"created_desc": "Vytvořeno (Nejnovější)",
"modified_asce": "Upraveno (Nejstarší)",
"modified_desc": "Upraveno (Nejnovější)",
"name_asce": "Z-A",
"name_desc": "A-Z",
"size_asce": "Velikost (Nejmenší)",
"size_desc": "Velikost (Největší)"
},
"files": {
"show_hidden": "Zobrazit skryté soubory",
"all_files": "Všechny soubory",
"file_type": "Typ souboru"
},
"file": {
"select": "Vybrat tento soubor"
}
},
"PluginView": {
@@ -54,7 +74,7 @@
},
"MultiplePluginsInstallModal": {
"title": {
"mixed_one": "Upravit 1 plugin",
"mixed_one": "Upravit {{count}} plugin",
"mixed_few": "Upravit {{count}} pluginů",
"mixed_other": "Upravit {{count}} pluginů",
"reinstall_one": "Přeinstalovat 1 plugin",
@@ -129,10 +149,6 @@
"label_url": "Instalovat plugin z URL",
"label_zip": "Instalovat plugin ze ZIP souboru"
},
"toast_zip": {
"body": "Instalace selhala! Podporovány jsou pouze soubory ZIP.",
"title": "Decky"
},
"valve_internal": {
"desc1": "Zapíná interní vývojářské menu Valve.",
"desc2": "Nedotýkejte se ničeho v této nabídce, pokud nevíte, co děláte.",
@@ -154,7 +170,6 @@
"header": "Účast v betě"
},
"developer_mode": {
"desc": "Zapíná vývojářské nastavení Decky.",
"label": "Vývojářský režim"
},
"other": {
@@ -162,12 +177,16 @@
},
"updates": {
"header": "Aktualizace"
},
"notifications": {
"decky_updates_label": "Dostupná aktualizace Decky",
"header": "Notifikace",
"plugin_updates_label": "Dostupná aktualizace pluginu"
}
},
"SettingsIndex": {
"developer_title": "Vývojář",
"general_title": "Obecné",
"navbar_settings": "Nastavení Decky",
"plugins_title": "Pluginy"
},
"Store": {
@@ -196,7 +215,11 @@
"alph_desc": "Abecedně (A do Z)",
"title": "Procházet"
},
"store_testing_cta": "Zvažte prosím testování nových pluginů, pomůžete tím týmu Decky Loader!"
"store_testing_cta": "Zvažte prosím testování nových pluginů, pomůžete tím týmu Decky Loader!",
"store_testing_warning": {
"desc": "Tento kanál obchodu můžete použít k testování nejnovějších verzí pluginů. Nezapomeňte zanechat zpětnou vazbu na GitHubu, aby bylo možné plugin aktualizovat pro všechny uživatele.",
"label": "Vítejte na testovacím kanálu obchodu"
}
},
"StoreSelect": {
"custom_store": {
@@ -224,5 +247,21 @@
"decky_updates": "Aktualizace Decky",
"patch_notes_desc": "Poznámky k verzi",
"no_patch_notes_desc": "žádné poznámky pro tuto verzi"
},
"DropdownMultiselect": {
"button": {
"back": "Zpět"
}
},
"FilePickerError": {
"errors": {
"file_not_found": "Zadaná cesta není platná. Zkontrolujte ji a zadejte znovu správně.",
"unknown": "Nastala neznámá chyba. Nezpracovaná chyba je: {{raw_error}}",
"perm_denied": "Nemáte přístup k zadanému adresáři. Zkontrolujte, zda jako uživatel (deck na Steam Decku) máte odpovídající oprávnění pro přístup k dané složce/souboru."
}
},
"TitleView": {
"settings_desc": "Otevřít nastavení Decky",
"decky_store_desc": "Otevřít obchod Decky"
}
}
+1 -7
View File
@@ -92,10 +92,6 @@
"button_install": "Installieren",
"label_url": "Installiere Erweiterung via URL"
},
"toast_zip": {
"body": "Installation fehlgeschlagen! Nur ZIP Datein werden unterstützt.",
"title": "Decky"
},
"valve_internal": {
"desc2": "Fasse in diesem Menü nichts an, es sei denn, du weißt was du tust.",
"label": "Aktiviere Valve-internes Menü",
@@ -111,7 +107,6 @@
"header": "Beta Teilnahme"
},
"developer_mode": {
"desc": "Aktiviere Deckys Entwickleroptionen.",
"label": "Entwickleroptionen"
},
"other": {
@@ -124,8 +119,7 @@
"SettingsIndex": {
"developer_title": "Entwickler",
"general_title": "Allgemein",
"plugins_title": "Erweiterungen",
"navbar_settings": "Decky Einstellungen"
"plugins_title": "Erweiterungen"
},
"Store": {
"store_contrib": {
+88 -28
View File
@@ -13,15 +13,17 @@
"label_url": "Εγκατάσταση επέκτασης απο URL",
"label_zip": "Εγκατάσταση επέκτασης από αρχείο ZIP"
},
"toast_zip": {
"title": "Decky",
"body": "Η εγκατάσταση απέτυχε. Μόνο αρχεία ZIP επιτρέπονται."
},
"valve_internal": {
"desc1": "Ενεργοποιεί το μενού προγραμματιστή της Valve.",
"desc2": "Μην αγγίξετε τίποτα σε αυτό το μενού εκτός και αν ξέρετε τι κάνει.",
"label": "Ενεργοποιήση εσωτερικού μενού Valve"
}
},
"cef_console": {
"button": "Άνοιγμα Κονσόλας",
"desc": "Ανοίγει την Κονσόλα CEF. Χρήσιμο μόνο για εντοπισμό σφαλμάτων. Τα πράγματα εδώ είναι δυνητικά επικίνδυνα και θα πρέπει να χρησιμοποιηθεί μόνο εάν είστε προγραμματιστής επεκτάσεων, ή κατευθυνθήκατε εδώ από έναν προγραμματιστή.",
"label": "Κονσόλα CEF"
},
"header": "Άλλα"
},
"BranchSelect": {
"update_channel": {
@@ -33,8 +35,8 @@
},
"Developer": {
"5secreload": "Γίνεται επαναφόρτωση σε 5 δευτερόλεπτα",
"disabling": "Γίνεται απενεργοποίηση",
"enabling": "Γίνεται ενεργοποίηση"
"disabling": "Γίνεται απενεργοποίηση των React DevTools",
"enabling": "Γίνεται ενεργοποίηση των React DevTools"
},
"PluginCard": {
"plugin_no_desc": "Δεν υπάρχει περιγραφή.",
@@ -70,14 +72,16 @@
"reload": "Επαναφόρτωση",
"uninstall": "Απεγκατάσταση",
"update_to": "Ενημέρωση σε {{name}}",
"update_all_one": "",
"update_all_other": ""
"update_all_one": "Ενημέρωση 1 επέκτασης",
"update_all_other": "Ενημέρωση {{count}} επεκτάσεων",
"show": "Γρήγορη πρόσβαση: Εμφάνιση",
"hide": "Γρήγορη πρόσβαση: Απόκρυψη"
},
"PluginLoader": {
"decky_title": "Decky",
"decky_update_available": "Ενημέρωση σε {{tag_name}} διαθέσιμη!",
"error": "Σφάλμα",
"plugin_error_uninstall": "Πηγαίντε στο <0></0> στο μενού του Decky για να απεγκαταστήσετε αυτή την επέκταση.",
"plugin_error_uninstall": "Η φόρτωση του {{name}} προκάλεσε το παραπάνω σφάλμα. Αυτό συνήθως σημαίνει ότι η επέκταση απαιτεί ενημέρωση για τη νέα έκδοση του SteamUI. Ελέγξτε εάν υπάρχει ενημέρωση ή αξιολογήστε την απεγκαταστήσετε της επέκτασης στις ρυθμίσεις του Decky, στην ενότητα Επεκτάσεις.",
"plugin_load_error": {
"message": "Σφάλμα στη φόρτωση της επέκτασης {{name}}",
"toast": "Σφάλμα φόρτωσης {{name}}"
@@ -87,8 +91,8 @@
"desc": "Σίγουρα θέλετε να απεγκαταστήσετε το {{name}};",
"title": "Απεγκατάσταση {{name}}"
},
"plugin_update_one": "",
"plugin_update_other": ""
"plugin_update_one": "Διαθέσιμη ενημέρωση για 1 επέκταση!",
"plugin_update_other": "Διαθέσιμες ενημερώσεις για {{count}} επεκτάσεις!"
},
"RemoteDebugging": {
"remote_cef": {
@@ -102,7 +106,6 @@
"header": "Σχετικά"
},
"developer_mode": {
"desc": "Ενεργοποιεί το μενού προγραμματιστή του Decky.",
"label": "Λειτουργία προγραμματιστή"
},
"other": {
@@ -113,18 +116,22 @@
},
"beta": {
"header": "Συμμετοχή στη Beta"
},
"notifications": {
"decky_updates_label": "Διαθέσιμη ενημέρωση του Decky",
"header": "Ειδοποιήσεις",
"plugin_updates_label": "Διαθέσιμες ενημερώσεις επεκτάσεων"
}
},
"SettingsIndex": {
"plugins_title": "Επεκτάσεις",
"developer_title": "Προγραμματιστής",
"general_title": "Γενικά",
"navbar_settings": "Ρυθμίσεις Decky"
"general_title": "Γενικά"
},
"Store": {
"store_contrib": {
"label": "Συνεισφέροντας",
"desc": "Αν θέλετε να συνεισφέρετε στο κατάστημα επεκτάσεων του Decky, τσεκάρετε το SteamDeckHomebrew/decky-plugin-template repository στο GitHub. Πληροφοριές σχετικά με τη δημιουργία και τη διανομή επεκτάσεων είναι διαθέσιμες στο README."
"desc": "Αν θέλετε να συνεισφέρετε στο κατάστημα επεκτάσεων του Decky, τσεκάρετε το SteamDeckHomebrew/decky-plugin-template repository στο GitHub. Πληροφορίες σχετικά με τη δημιουργία και τη διανομή επεκτάσεων είναι διαθέσιμες στο README."
},
"store_filter": {
"label": "Φίλτρο",
@@ -147,7 +154,11 @@
"alph_desc": "Αλφαβητικά (Α σε Ζ)",
"title": "Περιήγηση"
},
"store_testing_cta": "Παρακαλώ σκεφτείτε να τεστάρετε νέες επεκτάσεις για να βοηθήσετε την ομάδα του Decky Loader!"
"store_testing_cta": "Παρακαλώ σκεφτείτε να τεστάρετε νέες επεκτάσεις για να βοηθήσετε την ομάδα του Decky Loader!",
"store_testing_warning": {
"desc": "Μπορείτε να χρησιμοποιήσετε αυτό το κανάλι του καταστήματος για να δοκιμάσετε τις νεότερες εκδόσεις των επεκτάσεων. Φροντίστε να αφήσετε σχόλια στο GitHub, ώστε να βοηθήσετε στην ενημέρωση της εκάστοτε επέκταση για όλους τους χρήστες.",
"label": "Καλώς ήρθατε στο Δοκιμαστικό Κανάλι τους Καταστήματος"
}
},
"StoreSelect": {
"custom_store": {
@@ -178,23 +189,72 @@
},
"FilePickerIndex": {
"folder": {
"select": "Χρησιμοποιήστε αυτό το φάκελο"
"select": "Χρησιμοποιήστε αυτό το φάκελο",
"label": "Φάκελος",
"show_more": "Εμφάνιση περισσότερων αρχείων"
},
"filter": {
"modified_asce": "Τροποποιήθηκε (Παλαιότερο)",
"modified_desc": "Τροποποιήθηκε (Νεότερο)",
"created_desc": "Δημιουργήθηκε (Νεότερο)",
"name_asce": "Z-A",
"name_desc": "A-Z",
"created_asce": "Δημιουργήθηκε (Παλαιότερο)",
"size_asce": "Μέγεθος (Μικρότερο)",
"size_desc": "Μέγεθος (Μεγαλύτερο)"
},
"file": {
"select": "Επιλογή αυτού του αρχείου"
},
"files": {
"show_hidden": "Εμφάνιση Κρυφών Αρχείων",
"all_files": "Όλα Τα Αρχεία",
"file_type": "Τύπος Αρχείου"
}
},
"PluginView": {
"hidden_one": "",
"hidden_other": ""
"hidden_one": "1 επέκταση είναι κρυμμένη σε αυτήν τη λίστα",
"hidden_other": "{{count}} επεκτάσεις είναι κρυμμένες σε αυτήν τη λίστα"
},
"MultiplePluginsInstallModal": {
"title": {
"mixed_one": "",
"mixed_other": "",
"update_one": "",
"update_other": "",
"reinstall_one": "",
"reinstall_other": "",
"install_one": "",
"install_other": ""
"mixed_one": "Τροποποίηση 1 επέκτασης",
"mixed_other": "Τροποποίηση {{count}} επεκτάσεων",
"update_one": "Ενημέρωση 1 επέκτασης",
"update_other": "Ενημέρωση {{count}} επεκτάσεων",
"reinstall_one": "Επανεγκατάσταση 1 επέκτασης",
"reinstall_other": "Επανεγκατάσταση {{count}} επεκτάσεων",
"install_one": "Εγκατάσταση 1 επέκτασης",
"install_other": "Εγκατάσταση {{count}} επεκτάσεων"
},
"confirm": "Είστε βέβαιοι ότι θέλετε να κάνετε τις ακόλουθες τροποποιήσεις;",
"description": {
"reinstall": "Επανεγκατάσταση {{name}} {{version}}",
"update": "Ενημέρωση {{name}} to {{version}}",
"install": "Εγκατάσταση {{name}} {{version}}"
},
"ok_button": {
"idle": "Επιβεβαίωση",
"loading": "Φόρτωση"
}
},
"PluginListLabel": {
"hidden": "Κρυφό στο μενού γρήγορης πρόσβασης"
},
"TitleView": {
"settings_desc": "Άνοιγμα Ρυθμίσεων Decky",
"decky_store_desc": "Άνοιγμα Καταστήματος Decky"
},
"DropdownMultiselect": {
"button": {
"back": "Πίσω"
}
},
"FilePickerError": {
"errors": {
"file_not_found": "Η καθορισμένη διαδρομή δεν είναι έγκυρη. Παρακαλούμε ελέγξτε τη και εισάγετέ τη ξανά σωστά.",
"perm_denied": "Δεν έχετε πρόσβαση στην καθορισμένη διαδρομή. Ελέγξτε εάν ο χρήστης σας (deck στο Steam Deck) έχει τα αντίστοιχα δικαιώματα πρόσβασης στον καθορισμένο φάκελο/αρχείο.",
"unknown": "Παρουσιάστηκε άγνωστο σφάλμα. Το σφάλμα είναι: {{raw_error}}"
}
}
}
+257 -246
View File
@@ -1,249 +1,260 @@
{
"BranchSelect": {
"update_channel": {
"label": "Update Channel",
"prerelease": "Prerelease",
"stable": "Stable",
"testing": "Testing"
}
},
"Developer": {
"5secreload": "Reloading in 5 seconds",
"disabling": "Disabling React DevTools",
"enabling": "Enabling React DevTools"
},
"DropdownMultiselect": {
"button": {
"back": "Back"
}
},
"FilePickerError": {
"errors": {
"file_not_found": "The path specified is not valid. Please check it and reenter it correctly.",
"unknown": "An unknown error occurred. The raw error is: {{raw_error}}"
}
},
"FilePickerIndex": {
"files": {
"all_files": "All Files",
"file_type": "File Type",
"show_hidden": "Show Hidden Files"
},
"filter": {
"created_asce": "Created (Oldest)",
"created_desc": "Created (Newest)",
"modified_asce": "Modified (Oldest)",
"modified_desc": "Modified (Newest)",
"name_asce": "Z-A",
"name_desc": "A-Z",
"size_asce": "Size (Smallest)",
"size_desc": "Size (Largest)"
},
"folder": {
"label": "Folder",
"select": "Use this folder",
"show_more": "Show more files"
}
},
"PluginView": {
"hidden_one": "1 plugin is hidden from this list",
"hidden_other": "{{count}} plugins are hidden from this list"
},
"PluginListLabel": {
"hidden": "Hidden from the quick access menu"
},
"PluginCard": {
"plugin_full_access": "This plugin has full access to your Steam Deck.",
"plugin_install": "Install",
"plugin_no_desc": "No description provided.",
"plugin_version_label": "Plugin Version"
},
"PluginInstallModal": {
"install": {
"button_idle": "Install",
"button_processing": "Installing",
"desc": "Are you sure you want to install {{artifact}} {{version}}?",
"title": "Install {{artifact}}"
},
"no_hash": "This plugin does not have a hash, you are installing it at your own risk.",
"reinstall": {
"button_idle": "Reinstall",
"button_processing": "Reinstalling",
"desc": "Are you sure you want to reinstall {{artifact}} {{version}}?",
"title": "Reinstall {{artifact}}"
},
"update": {
"button_idle": "Update",
"button_processing": "Updating",
"desc": "Are you sure you want to update {{artifact}} {{version}}?",
"title": "Update {{artifact}}"
}
},
"MultiplePluginsInstallModal": {
"title": {
"mixed_one": "Modify {{count}} plugin",
"mixed_other": "Modify {{count}} plugins",
"update_one": "Update 1 plugin",
"update_other": "Update {{count}} plugins",
"reinstall_one": "Reinstall 1 plugin",
"reinstall_other": "Reinstall {{count}} plugins",
"install_one": "Install 1 plugin",
"install_other": "Install {{count}} plugins"
},
"ok_button": {
"idle": "Confirm",
"loading": "Working"
},
"confirm": "Are you sure you want to make the following modifications?",
"description": {
"install": "Install {{name}} {{version}}",
"update": "Update {{name}} to {{version}}",
"reinstall": "Reinstall {{name}} {{version}}"
}
},
"PluginListIndex": {
"no_plugin": "No plugins installed!",
"plugin_actions": "Plugin Actions",
"reinstall": "Reinstall",
"reload": "Reload",
"uninstall": "Uninstall",
"update_to": "Update to {{name}}",
"show": "Quick access: Show",
"hide": "Quick access: Hide",
"update_all_one": "Update 1 plugin",
"update_all_other": "Update {{count}} plugins"
},
"PluginLoader": {
"decky_title": "Decky",
"decky_update_available": "Update to {{tag_name}} available!",
"error": "Error",
"plugin_error_uninstall": "Loading {{name}} caused an exception as shown above. This usually means that the plugin requires an update for the new version of SteamUI. Check if an update is present or evaluate its removal in the Decky settings, in the Plugins section.",
"plugin_load_error": {
"message": "Error loading plugin {{name}}",
"toast": "Error loading {{name}}"
},
"plugin_uninstall": {
"button": "Uninstall",
"desc": "Are you sure you want to uninstall {{name}}?",
"title": "Uninstall {{name}}"
},
"plugin_update_one": "Updates available for 1 plugin!",
"plugin_update_other": "Updates available for {{count}} plugins!"
},
"RemoteDebugging": {
"remote_cef": {
"desc": "Allow unauthenticated access to the CEF debugger to anyone in your network",
"label": "Allow Remote CEF Debugging"
}
},
"SettingsDeveloperIndex": {
"cef_console": {
"button": "Open Console",
"desc": "Opens the CEF Console. Only useful for debugging purposes. Stuff here is potentially dangerous and should only be used if you are a plugin dev, or are directed here by one.",
"label": "CEF Console"
},
"header": "Other",
"react_devtools": {
"desc": "Enables connection to a computer running React DevTools. Changing this setting will reload Steam. Set the IP address before enabling.",
"ip_label": "IP",
"label": "Enable React DevTools"
},
"third_party_plugins": {
"button_install": "Install",
"button_zip": "Browse",
"header": "Third-Party Plugins",
"label_desc": "URL",
"label_url": "Install Plugin from URL",
"label_zip": "Install Plugin from ZIP File"
},
"toast_zip": {
"body": "Installation failed! Only ZIP files are supported.",
"title": "Decky"
},
"valve_internal": {
"desc1": "Enables the Valve internal developer menu.",
"desc2": "Do not touch anything in this menu unless you know what it does.",
"label": "Enable Valve Internal"
}
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Decky Version",
"header": "About"
},
"beta": {
"header": "Beta participation"
},
"developer_mode": {
"desc": "Enables Decky's developer settings.",
"label": "Developer mode"
},
"other": {
"header": "Other"
},
"updates": {
"header": "Updates"
}
},
"SettingsIndex": {
"developer_title": "Developer",
"general_title": "General",
"navbar_settings": "Decky Settings",
"plugins_title": "Plugins"
},
"Store": {
"store_contrib": {
"desc": "If you would like to contribute to the Decky Plugin Store, check the SteamDeckHomebrew/decky-plugin-template repository on GitHub. Information on development and distribution is available in the README.",
"label": "Contributing"
},
"store_filter": {
"label": "Filter",
"label_def": "All"
},
"store_search": {
"label": "Search"
},
"store_sort": {
"label": "Sort",
"label_def": "Last Updated (Newest)"
},
"store_source": {
"desc": "All plugin source code is available on SteamDeckHomebrew/decky-plugin-database repository on GitHub.",
"label": "Source Code"
},
"store_tabs": {
"about": "About",
"alph_asce": "Alphabetical (Z to A)",
"alph_desc": "Alphabetical (A to Z)",
"title": "Browse"
},
"store_testing_cta": "Please consider testing new plugins to help the Decky Loader team!"
},
"StoreSelect": {
"custom_store": {
"label": "Custom Store",
"url_label": "URL"
},
"store_channel": {
"custom": "Custom",
"default": "Default",
"label": "Store Channel",
"testing": "Testing"
}
},
"Updater": {
"decky_updates": "Decky Updates",
"no_patch_notes_desc": "no patch notes for this version",
"patch_notes_desc": "Patch Notes",
"updates": {
"check_button": "Check For Updates",
"checking": "Checking",
"cur_version": "Current version: {{ver}}",
"install_button": "Install Update",
"label": "Updates",
"lat_version": "Up to date: running {{ver}}",
"reloading": "Reloading",
"updating": "Updating"
}
"BranchSelect": {
"update_channel": {
"label": "Update Channel",
"prerelease": "Prerelease",
"stable": "Stable",
"testing": "Testing"
}
},
"Developer": {
"5secreload": "Reloading in 5 seconds",
"disabling": "Disabling React DevTools",
"enabling": "Enabling React DevTools"
},
"DropdownMultiselect": {
"button": {
"back": "Back"
}
},
"FilePickerError": {
"errors": {
"file_not_found": "The path specified is not valid. Please check it and reenter it correctly.",
"perm_denied": "You do not have access to the specified directory. Please check if your user (deck on Steam Deck) has the corresponding permission to access the specified folder/file.",
"unknown": "An unknown error occurred. The raw error is: {{raw_error}}"
}
},
"FilePickerIndex": {
"file": {
"select": "Select this file"
},
"files": {
"all_files": "All Files",
"file_type": "File Type",
"show_hidden": "Show Hidden Files"
},
"filter": {
"created_asce": "Created (Oldest)",
"created_desc": "Created (Newest)",
"modified_asce": "Modified (Oldest)",
"modified_desc": "Modified (Newest)",
"name_asce": "Z-A",
"name_desc": "A-Z",
"size_asce": "Size (Smallest)",
"size_desc": "Size (Largest)"
},
"folder": {
"label": "Folder",
"select": "Use this folder",
"show_more": "Show more files"
}
},
"MultiplePluginsInstallModal": {
"confirm": "Are you sure you want to make the following modifications?",
"description": {
"install": "Install {{name}} {{version}}",
"reinstall": "Reinstall {{name}} {{version}}",
"update": "Update {{name}} to {{version}}"
},
"ok_button": {
"idle": "Confirm",
"loading": "Working"
},
"title": {
"install_one": "Install 1 plugin",
"install_other": "Install {{count}} plugins",
"mixed_one": "Modify {{count}} plugin",
"mixed_other": "Modify {{count}} plugins",
"reinstall_one": "Reinstall 1 plugin",
"reinstall_other": "Reinstall {{count}} plugins",
"update_one": "Update 1 plugin",
"update_other": "Update {{count}} plugins"
}
},
"PluginCard": {
"plugin_full_access": "This plugin has full access to your Steam Deck.",
"plugin_install": "Install",
"plugin_no_desc": "No description provided.",
"plugin_version_label": "Plugin Version"
},
"PluginInstallModal": {
"install": {
"button_idle": "Install",
"button_processing": "Installing",
"desc": "Are you sure you want to install {{artifact}} {{version}}?",
"title": "Install {{artifact}}"
},
"no_hash": "This plugin does not have a hash, you are installing it at your own risk.",
"reinstall": {
"button_idle": "Reinstall",
"button_processing": "Reinstalling",
"desc": "Are you sure you want to reinstall {{artifact}} {{version}}?",
"title": "Reinstall {{artifact}}"
},
"update": {
"button_idle": "Update",
"button_processing": "Updating",
"desc": "Are you sure you want to update {{artifact}} {{version}}?",
"title": "Update {{artifact}}"
}
},
"PluginListIndex": {
"hide": "Quick access: Hide",
"no_plugin": "No plugins installed!",
"plugin_actions": "Plugin Actions",
"reinstall": "Reinstall",
"reload": "Reload",
"show": "Quick access: Show",
"uninstall": "Uninstall",
"update_all_one": "Update 1 plugin",
"update_all_other": "Update {{count}} plugins",
"update_to": "Update to {{name}}"
},
"PluginListLabel": {
"hidden": "Hidden from the quick access menu"
},
"PluginLoader": {
"decky_title": "Decky",
"decky_update_available": "Update to {{tag_name}} available!",
"error": "Error",
"plugin_error_uninstall": "Loading {{name}} caused an exception as shown above. This usually means that the plugin requires an update for the new version of SteamUI. Check if an update is present or evaluate its removal in the Decky settings, in the Plugins section.",
"plugin_load_error": {
"message": "Error loading plugin {{name}}",
"toast": "Error loading {{name}}"
},
"plugin_uninstall": {
"button": "Uninstall",
"desc": "Are you sure you want to uninstall {{name}}?",
"title": "Uninstall {{name}}"
},
"plugin_update_one": "Updates available for 1 plugin!",
"plugin_update_other": "Updates available for {{count}} plugins!"
},
"PluginView": {
"hidden_one": "1 plugin is hidden from this list",
"hidden_other": "{{count}} plugins are hidden from this list"
},
"RemoteDebugging": {
"remote_cef": {
"desc": "Allow unauthenticated access to the CEF debugger to anyone in your network",
"label": "Allow Remote CEF Debugging"
}
},
"SettingsDeveloperIndex": {
"cef_console": {
"button": "Open Console",
"desc": "Opens the CEF Console. Only useful for debugging purposes. Stuff here is potentially dangerous and should only be used if you are a plugin dev, or are directed here by one.",
"label": "CEF Console"
},
"header": "Other",
"react_devtools": {
"desc": "Enables connection to a computer running React DevTools. Changing this setting will reload Steam. Set the IP address before enabling.",
"ip_label": "IP",
"label": "Enable React DevTools"
},
"third_party_plugins": {
"button_install": "Install",
"button_zip": "Browse",
"header": "Third-Party Plugins",
"label_desc": "URL",
"label_url": "Install Plugin from URL",
"label_zip": "Install Plugin from ZIP File"
},
"valve_internal": {
"desc1": "Enables the Valve internal developer menu.",
"desc2": "Do not touch anything in this menu unless you know what it does.",
"label": "Enable Valve Internal"
}
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Decky Version",
"header": "About"
},
"beta": {
"header": "Beta participation"
},
"developer_mode": {
"label": "Developer mode"
},
"notifications": {
"decky_updates_label": "Decky update available",
"header": "Notifications",
"plugin_updates_label": "Plugin updates available"
},
"other": {
"header": "Other"
},
"updates": {
"header": "Updates"
}
},
"SettingsIndex": {
"developer_title": "Developer",
"general_title": "General",
"plugins_title": "Plugins"
},
"Store": {
"store_contrib": {
"desc": "If you would like to contribute to the Decky Plugin Store, check the SteamDeckHomebrew/decky-plugin-template repository on GitHub. Information on development and distribution is available in the README.",
"label": "Contributing"
},
"store_filter": {
"label": "Filter",
"label_def": "All"
},
"store_search": {
"label": "Search"
},
"store_sort": {
"label": "Sort",
"label_def": "Last Updated (Newest)"
},
"store_source": {
"desc": "All plugin source code is available on SteamDeckHomebrew/decky-plugin-database repository on GitHub.",
"label": "Source Code"
},
"store_tabs": {
"about": "About",
"alph_asce": "Alphabetical (Z to A)",
"alph_desc": "Alphabetical (A to Z)",
"title": "Browse"
},
"store_testing_cta": "Please consider testing new plugins to help the Decky Loader team!",
"store_testing_warning": {
"desc": "You can use this store channel to test bleeding-edge plugin versions. Be sure to leave feedback on GitHub so the plugin can be updated for all users.",
"label": "Welcome to the Testing Store Channel"
}
},
"StoreSelect": {
"custom_store": {
"label": "Custom Store",
"url_label": "URL"
},
"store_channel": {
"custom": "Custom",
"default": "Default",
"label": "Store Channel",
"testing": "Testing"
}
},
"TitleView": {
"decky_store_desc": "Open Decky Store",
"settings_desc": "Open Decky Settings"
},
"Updater": {
"decky_updates": "Decky Updates",
"no_patch_notes_desc": "no patch notes for this version",
"patch_notes_desc": "Patch Notes",
"updates": {
"check_button": "Check For Updates",
"checking": "Checking",
"cur_version": "Current version: {{ver}}",
"install_button": "Install Update",
"label": "Updates",
"lat_version": "Up to date: running {{ver}}",
"reloading": "Reloading",
"updating": "Updating"
}
}
}
+1 -7
View File
@@ -13,10 +13,6 @@
"label": "Activar menú interno de Valve",
"desc1": "Activa el menú interno de desarrollo de Valve."
},
"toast_zip": {
"title": "Decky",
"body": "¡Ha fallado la instalación! Solo se permiten archivos ZIP."
},
"cef_console": {
"button": "Abrir consola",
"label": "Consola CEF",
@@ -118,8 +114,7 @@
"decky_version": "Versión de Decky"
},
"developer_mode": {
"label": "Modo desarrollador",
"desc": "Activa los ajustes de desarrollador de Decky."
"label": "Modo desarrollador"
},
"beta": {
"header": "Participación en la beta"
@@ -131,7 +126,6 @@
"SettingsIndex": {
"developer_title": "Desarrollador",
"general_title": "General",
"navbar_settings": "Ajustes de Decky",
"plugins_title": "Plugins"
},
"Store": {
+260
View File
@@ -0,0 +1,260 @@
{
"BranchSelect": {
"update_channel": {
"prerelease": "Esijulkaisu",
"testing": "Testiversio",
"stable": "Vakaa versio",
"label": "Päivityskanava"
}
},
"Developer": {
"5secreload": "Uudelleenladataan 5 sekunin kuluttua",
"disabling": "Poistetaan React DevTools käytöstä",
"enabling": "Otetaan React DevTools käyttöön"
},
"FilePickerError": {
"errors": {
"perm_denied": "Sinulla ei ole käyttöoikeutta määritettyyn hakemistoon. Tarkista, onko käyttäjälläsi (käyttäjä 'deck' Steam Deckillä) vastaavat oikeudet käyttää määritettyä kansiota/tiedostoa.",
"unknown": "Tapahtui tuntematon virhe. Raaka virhe on: {{raw_error}}",
"file_not_found": "Määritetty polku ei kelpaa. Tarkista se ja kirjoita se uudelleen oikein."
}
},
"FilePickerIndex": {
"file": {
"select": "Valitse tämä tiedosto"
},
"files": {
"all_files": "Kaikki tiedostot",
"file_type": "Tiedostotyyppi",
"show_hidden": "Näytä piilotetut tiedostot"
},
"filter": {
"created_desc": "Luotu (uusin ensin)",
"modified_asce": "Muokattu (vanhin)",
"modified_desc": "Muokattu (uusin)",
"name_asce": "Z-A",
"name_desc": "A-Z",
"size_asce": "Koko (pienin ensin)",
"size_desc": "Koko (suurin ensin)",
"created_asce": "Luotu (vanhin ensin)"
},
"folder": {
"label": "Kansio",
"select": "Käytä tätä kansiota",
"show_more": "Näytä lisää tiedostoja"
}
},
"MultiplePluginsInstallModal": {
"confirm": "Haluatko varmasti tehdä seuraavat muutokset?",
"description": {
"reinstall": "Uudelleenasenna {{name}} {{version}}",
"update": "Päivitä {{name}} versioon {{version}}",
"install": "Asenna {{name}} {{version}}"
},
"ok_button": {
"idle": "Vahvista",
"loading": "Ladataan"
},
"title": {
"install_one": "Asenna yksi laajennus",
"install_other": "Asenna {{count}} laajennusta",
"update_one": "Päivitä yksi laajennus",
"update_other": "Päivitä {{count}} laajennusta",
"mixed_one": "Muuta yhtä laajennusta",
"mixed_other": "Muuta {{count}} laajennusta",
"reinstall_one": "Uudelleenasenna yksi laajennus",
"reinstall_other": "Uudelleenasenna {{count}} laajennusta"
}
},
"PluginCard": {
"plugin_install": "Asenna",
"plugin_no_desc": "Ei kuvausta.",
"plugin_version_label": "Laajennuksen versio",
"plugin_full_access": "Tällä laajennuksella on täysi pääsy Steam Deckkiisi."
},
"PluginInstallModal": {
"install": {
"button_idle": "Asenna",
"button_processing": "Asennetaan",
"desc": "Haluatko varmasti asentaa {{artifact}} {{version}}?",
"title": "Asenna {{artifact}}"
},
"no_hash": "Tällä laajennuksella ei ole hashia, asennat sen omalla vastuullasi.",
"reinstall": {
"button_idle": "Uudelleenasenna",
"button_processing": "Uudelleenasennetaan",
"desc": "Haluatko varmasti uudelleenasentaa {{artifact}} {{version}}?",
"title": "Uudelleenasenna {{artifact}}"
},
"update": {
"button_idle": "Päivitä",
"button_processing": "Päivitetään",
"desc": "Haluatko varmasti päivittää {{artifact}} {{version}}?",
"title": "Päivitä {{artifact}}"
}
},
"DropdownMultiselect": {
"button": {
"back": "Takaisin"
}
},
"PluginListIndex": {
"no_plugin": "Ei asennettuja laajennuksia!",
"plugin_actions": "Laajennustoiminnot",
"reinstall": "Uudelleenasenna",
"reload": "Lataa uudelleen",
"uninstall": "Poista asennus",
"update_all_one": "Päivitä yksi laajennus",
"update_all_other": "Päivitä {{count}} laajennusta",
"update_to": "Päivitä versioon {{name}}",
"hide": "Pikavalikko: Piilota",
"show": "Pikavalikko: Näytä"
},
"PluginListLabel": {
"hidden": "Piilotettu pikavalikosta"
},
"PluginLoader": {
"decky_title": "Decky",
"decky_update_available": "Päivitys versioon {{tag_name}} on saatavilla!",
"error": "Virhe",
"plugin_load_error": {
"message": "Virhe ladattaessa {{name}}-laajennusta",
"toast": "Virhe ladattaessa {{name}}"
},
"plugin_uninstall": {
"button": "Poista asennus",
"desc": "Haluatko varmasti poistaa {{name}} asennuksen?",
"title": "Poista {{name}}"
},
"plugin_update_one": "Päivityksiä saatavilla yhdelle laajennukselle!",
"plugin_update_other": "Päivityksiä saatavilla {{count}} laajennukselle!",
"plugin_error_uninstall": "{{name}} lataaminen aiheutti yllä olevan poikkeuksen. Tämä tarkoittaa yleensä sitä, että laajennus vaatii päivityksen uudelle SteamUI-versiolle. Tarkista, onko päivitystä saatavilla, tai harkitse laajennuksen poistoa Decky-asetuksista, laajennukset-osiosta."
},
"RemoteDebugging": {
"remote_cef": {
"desc": "Salli todentamaton pääsy CEF-debuggeriin kenelle tahansa verkossasi",
"label": "Salli CEF-etädebugaus"
}
},
"SettingsDeveloperIndex": {
"cef_console": {
"button": "Avaa konsoli",
"desc": "Avaa CEF-konsolin. Hyödyllinen vain debugaustarkoituksiin. Täällä olevat jutut ovat mahdollisesti vaarallisia, ja niitä tulisi käyttää vain, jos olet laajennuksen kehittäjä tai jos kehittäjä on ohjannut sinut tänne.",
"label": "CEF-konsoli"
},
"header": "Muu",
"react_devtools": {
"desc": "Mahdollistaa yhteyden tietokoneeseen, jossa on käytössä React DevTools. Tämän asetuksen muuttaminen lataa Steamin uudelleen. Aseta IP-osoite ennen käyttöönottoa.",
"ip_label": "IP-osoite",
"label": "Ota React DevTools käyttöön"
},
"third_party_plugins": {
"button_install": "Asenna",
"button_zip": "Selaa",
"header": "Kolmannen osapuolen laajennukset",
"label_desc": "URL-osoite",
"label_zip": "Asenna laajennus ZIP-tiedostosta",
"label_url": "Asenna laajennus URL-osoitteesta"
},
"valve_internal": {
"desc2": "Älä koske mihinkään tässä valikossa, ellet tiedä mitä se tekee.",
"label": "Ota Valve Internal käyttöön",
"desc1": "Ottaa käyttöön Valven sisäisen kehittäjävalikon."
}
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Decky-versio",
"header": "Tietoja"
},
"beta": {
"header": "Beta-osallistuminen"
},
"developer_mode": {
"label": "Kehittäjätila"
},
"notifications": {
"decky_updates_label": "Decky-päivitys saatavilla",
"header": "Ilmoitukset",
"plugin_updates_label": "Laajennuspäivityksiä saatavilla"
},
"other": {
"header": "Muu"
},
"updates": {
"header": "Päivitykset"
}
},
"SettingsIndex": {
"developer_title": "Kehittäjä",
"general_title": "Yleinen",
"plugins_title": "Laajennukset"
},
"Store": {
"store_contrib": {
"label": "Osallistuminen",
"desc": "Mikäli haluat julkaista Decky Plugin Storeen, tarkista GitHubin SteamDeckHomebrew/decky-plugin-template -esimerkkitietovarasto. Tietoa kehityksestä ja jakelusta löytyy README:stä."
},
"store_filter": {
"label": "Suodin",
"label_def": "Kaikki"
},
"store_search": {
"label": "Hae"
},
"store_sort": {
"label": "Järjestä",
"label_def": "Viimeksi päivitetty (uusin ensin)"
},
"store_source": {
"desc": "Kaikken laajennusten lähdekoodit ovat saatavilla SteamDeckHomebrew/decky-plugin-database -arkistosta GitHubissa.",
"label": "Lähdekoodi"
},
"store_tabs": {
"about": "Tietoja",
"alph_asce": "Aakkosjärjestyksessä (ZA)",
"alph_desc": "Aakkosjärjestyksessä (AZ)",
"title": "Selaa"
},
"store_testing_cta": "Harkitse uusien lisäosien testaamista auttaaksesi Decky Loader -tiimiä!",
"store_testing_warning": {
"label": "Tervetuloa testausmyymälä-kanavalle",
"desc": "Voit käyttää tätä myymäläkanavaa testataksesi uusimpia laajennusversioita. Muista jättää palautetta GitHubissa, jotta laajennus voidaan päivittää kaikille käyttäjille."
}
},
"StoreSelect": {
"custom_store": {
"label": "Mukautettu myymälä",
"url_label": "URL-osoite"
},
"store_channel": {
"custom": "Mukautettu",
"default": "Oletus",
"label": "Myymäläkanava",
"testing": "Testaus"
}
},
"TitleView": {
"decky_store_desc": "Avaa Decky-myymälä",
"settings_desc": "Avaa Decky-asetukset"
},
"Updater": {
"decky_updates": "Decky-päivitykset",
"no_patch_notes_desc": "tälle versiolle ei ole korjausmerkintöjä",
"patch_notes_desc": "Korjausmerkinnät",
"updates": {
"check_button": "Tarkista päivitykset",
"checking": "Tarkistetaan",
"cur_version": "Nykyinen versio: {{ver}}",
"install_button": "Asenna päivitys",
"label": "Päivitykset",
"lat_version": "Ajan tasalla: versio {{ver}}",
"reloading": "Uudelleenladataan",
"updating": "Päivitetään"
}
},
"PluginView": {
"hidden_one": "Yksi laajennus on piilotettu tästä luettelosta",
"hidden_other": "{{count}} laajennusta on piilotettu tästä luettelosta"
}
}
-6
View File
@@ -13,10 +13,6 @@
"label_url": "Installer le plugin à partir d'un URL",
"label_zip": "Installer le plugin à partir d'un fichier ZIP"
},
"toast_zip": {
"body": "Échec de l'installation! Seuls les fichiers ZIP sont pris en charge.",
"title": "Decky"
},
"valve_internal": {
"desc1": "Active le menu développeur interne de Valve.",
"desc2": "Ne touchez à rien dans ce menu à moins que vous ne sachiez ce qu'il fait.",
@@ -139,7 +135,6 @@
"header": "Participation à la Bêta"
},
"developer_mode": {
"desc": "Active les paramètres de développeur de Decky.",
"label": "Mode développeur"
},
"other": {
@@ -152,7 +147,6 @@
"SettingsIndex": {
"developer_title": "Développeur",
"general_title": "Général",
"navbar_settings": "Paramètres de Decky",
"plugins_title": "Plugins"
},
"Store": {
+60 -49
View File
@@ -20,10 +20,14 @@
"FilePickerError": {
"errors": {
"file_not_found": "Il percorso specificato non è valido. Controllalo e prova a reinserirlo di nuovo.",
"unknown": "È avvenuto un'errore sconosciuto. L'errore segnalato è {{raw_error}}"
"unknown": "È avvenuto un'errore sconosciuto. L'errore segnalato è {{raw_error}}",
"perm_denied": "Il tuo utente non ha accesso alla directory specificata. Verifica se l'utente corrente (è deck su Steam Deck di default) ha i permessi corrispondenti per accedere alla cartella/file desiderato."
}
},
"FilePickerIndex": {
"file": {
"select": "Seleziona questo file"
},
"files": {
"all_files": "Tutti i file",
"file_type": "Tipo di file",
@@ -45,6 +49,32 @@
"show_more": "Mostra più file"
}
},
"MultiplePluginsInstallModal": {
"confirm": "Sei sicuro di voler effettuare le modifiche seguenti?",
"description": {
"install": "Installa {{name}} {{version}}",
"reinstall": "Reinstalla {{name}} {{version}}",
"update": "Aggiorna {{name}} alla versione {{version}}"
},
"ok_button": {
"idle": "Conferma",
"loading": "Elaboro"
},
"title": {
"install_one": "Installa un plugin",
"install_many": "Installa {{count}} plugins",
"install_other": "Installa {{count}} plugins",
"mixed_one": "Modifica un plugin",
"mixed_many": "Modifica {{count}} plugins",
"mixed_other": "Modifica {{count}} plugins",
"reinstall_one": "Reinstalla un plugin",
"reinstall_many": "Reinstalla {{count}} plugins",
"reinstall_other": "Reinstalla {{count}} plugins",
"update_one": "Aggiorna un plugin",
"update_many": "Aggiorna {{count}} plugins",
"update_other": "Aggiorna {{count}} plugins"
}
},
"PluginCard": {
"plugin_full_access": "Questo plugin ha accesso completo al tuo Steam Deck.",
"plugin_install": "Installa",
@@ -73,17 +103,20 @@
}
},
"PluginListIndex": {
"hide": "Accesso rapido: Nascondi",
"no_plugin": "Nessun plugin installato!",
"plugin_actions": "Operazioni sui plugins",
"reinstall": "Reinstalla",
"reload": "Ricarica",
"show": "Accesso rapido: Mostra",
"uninstall": "Rimuovi",
"update_to": "Aggiorna a {{name}}",
"update_all_one": "Aggiorna un plugin",
"update_all_many": "Aggiorna {{count}} plugins",
"update_all_other": "Aggiorna {{count}} plugins",
"show": "Accesso rapido: Mostra",
"hide": "Accesso rapido: Nascondi"
"update_to": "Aggiorna a {{name}}"
},
"PluginListLabel": {
"hidden": "Nascosto dal menu di accesso rapido"
},
"PluginLoader": {
"decky_title": "Decky",
@@ -103,6 +136,11 @@
"plugin_update_many": "Aggiornamenti disponibili per {{count}} plugins!",
"plugin_update_other": "Aggiornamenti disponibili per {{count}} plugins!"
},
"PluginView": {
"hidden_one": "Un plugin è nascosto dalla lista",
"hidden_many": "Sono nascosti {{count}} plugin dalla lista",
"hidden_other": "Sono nascosti {{count}} plugin dalla lista"
},
"RemoteDebugging": {
"remote_cef": {
"desc": "Permetti l'accesso non autenticato al debugger di CEF da tutti gli indirizzi sulla tua rete locale",
@@ -110,6 +148,11 @@
}
},
"SettingsDeveloperIndex": {
"cef_console": {
"button": "Apri la console",
"desc": "Apri la console di CEF. Utile solamente per ragioni di debug. Questa opzione deve essere usata solo se sei uno sviluppatore di plugin o se uno di questi ti ha chiesto di farlo, visto che questa feature potrebbe essere potenzialmente pericolosa.",
"label": "Console CEF"
},
"header": "Altro",
"react_devtools": {
"desc": "Abilita la connessione ad un computer che esegue i DevTools di React. Steam verrà ricaricato se lo stato cambia. Imposta il tuo indirizzo IP prima di abilitarlo.",
@@ -124,19 +167,10 @@
"label_url": "Installa plugin da un'indirizzo web",
"label_zip": "Installa plugin da un file ZIP"
},
"toast_zip": {
"body": "Installazione non riuscita! Solo supportati solo file ZIP.",
"title": "Decky"
},
"valve_internal": {
"desc1": "Abilita il menu di sviluppo interno di Valve.",
"desc2": "Non toccare nulla in questo menu se non sai quello che fa.",
"label": "Abilita Menu Sviluppatore"
},
"cef_console": {
"label": "Console CEF",
"button": "Apri la console",
"desc": "Apri la console di CEF. Utile solamente per ragioni di debug. Questa opzione deve essere usata solo se sei uno sviluppatore di plugin o se uno di questi ti ha chiesto di farlo, visto che questa feature potrebbe essere potenzialmente pericolosa."
}
},
"SettingsGeneralIndex": {
@@ -148,7 +182,6 @@
"header": "Partecipazione alla beta"
},
"developer_mode": {
"desc": "Abilità le impostazioni di sviluppo di Decky.",
"label": "Modalità sviluppatore"
},
"other": {
@@ -156,12 +189,16 @@
},
"updates": {
"header": "Aggiornamenti"
},
"notifications": {
"header": "Notifiche",
"decky_updates_label": "Aggiornamenti di Decky",
"plugin_updates_label": "Aggiornamenti dei plugins"
}
},
"SettingsIndex": {
"developer_title": "Sviluppatore",
"general_title": "Generali",
"navbar_settings": "Impostazioni Decky",
"plugins_title": "Plugins"
},
"Store": {
@@ -190,7 +227,11 @@
"alph_desc": "Alfabetico (A a Z)",
"title": "Sfoglia"
},
"store_testing_cta": "Valuta la possibilità di testare nuovi plugin per aiutare il team di Decky Loader!"
"store_testing_cta": "Valuta la possibilità di testare nuovi plugin per aiutare il team di Decky Loader!",
"store_testing_warning": {
"label": "Benvenuto nel Negozio di Test dei Plugins",
"desc": "Puoi usare questo canale del negozio per testare versioni di plugin sperimentali. Assicurati di lasciare un feedback su Github dopo averlo testato in modo che il plugin possa essere promosso a stabile per tutti gli altri utenti o per permettere allo sviluppatore di plugin di correggere eventuali errori."
}
},
"StoreSelect": {
"custom_store": {
@@ -219,38 +260,8 @@
"updating": "Aggiornando"
}
},
"MultiplePluginsInstallModal": {
"title": {
"mixed_one": "Modifica un plugin",
"mixed_many": "Modifica {{count}} plugins",
"mixed_other": "Modifica {{count}} plugins",
"update_one": "Aggiorna un plugin",
"update_many": "Aggiorna {{count}} plugins",
"update_other": "Aggiorna {{count}} plugins",
"reinstall_one": "Reinstalla un plugin",
"reinstall_many": "Reinstalla {{count}} plugins",
"reinstall_other": "Reinstalla {{count}} plugins",
"install_one": "Installa un plugin",
"install_many": "Installa {{count}} plugins",
"install_other": "Installa {{count}} plugins"
},
"confirm": "Sei sicuro di voler effettuare le modifiche seguenti?",
"ok_button": {
"idle": "Conferma",
"loading": "Elaboro"
},
"description": {
"install": "Installa {{name}} {{version}}",
"update": "Aggiorna {{name}} alla versione {{version}}",
"reinstall": "Reinstalla {{name}} {{version}}"
}
},
"PluginView": {
"hidden_one": "Un plugin è nascosto dalla lista",
"hidden_many": "Sono nascosti {{count}} plugin dalla lista",
"hidden_other": "Sono nascosti {{count}} plugin dalla lista"
},
"PluginListLabel": {
"hidden": "Nascosti dal menu di accesso rapido"
"TitleView": {
"settings_desc": "Apri le impostazioni di Decky",
"decky_store_desc": "Apri lo store di Decky"
}
}
+52 -13
View File
@@ -2,9 +2,9 @@
"BranchSelect": {
"update_channel": {
"label": "업데이트 배포 채널",
"stable": "안정",
"testing": "시험판",
"prerelease": "사전 출시"
"stable": "안정",
"testing": "테스트",
"prerelease": "사전 출시"
}
},
"Developer": {
@@ -14,7 +14,27 @@
},
"FilePickerIndex": {
"folder": {
"select": "이 폴더 사용"
"select": "이 폴더 사용",
"label": "폴더",
"show_more": "더 많은 파일 표시"
},
"filter": {
"created_asce": "만든 날짜 (오름차순)",
"modified_asce": "수정한 날짜 (오름차순)",
"created_desc": "만든 날짜 (내림차 순)",
"name_asce": "Z-A",
"name_desc": "A-Z",
"size_asce": "크기 (오름차순)",
"modified_desc": "수정한 날짜 (내림차순)",
"size_desc": "크기 (내림차순)"
},
"files": {
"all_files": "모든 파일",
"show_hidden": "숨김 파일 표시",
"file_type": "파일 형식"
},
"file": {
"select": "이 파일 선택"
}
},
"PluginView": {
@@ -93,7 +113,7 @@
"title": "{{name}} 설치 제거"
},
"plugin_update_other": "플러그인 {{count}}개를 업데이트 할 수 있습니다!",
"plugin_error_uninstall": "{{name}} 플러그인을 불러올 때 위와 같은 예외가 발생했습니다. 이는 보통 SteamUI 최신 버전에 맞는 플러그인 업데이트가 필요할 때 발생합니다. Decky 설정의 플러그인 항목에서 업데이트가 있는지 확인하거나 설치 제거를 시도 해 보세요."
"plugin_error_uninstall": "{{name}} 플러그인을 불러올 때 위와 같은 예외가 발생했습니다. 이는 보통 SteamUI 최신 버전에 맞는 플러그인 업데이트가 필요할 때 발생합니다. Decky 설정의 플러그인 섹션에서 업데이트가 있는지 확인하거나 설치 제거를 시도 해 보세요."
},
"RemoteDebugging": {
"remote_cef": {
@@ -121,10 +141,6 @@
"label_url": "URL에서 플러그인 설치",
"label_zip": "ZIP 파일에서 플러그인 설치"
},
"toast_zip": {
"body": "설치를 실패했습니다! ZIP 파일만이 지원됩니다.",
"title": "Decky"
},
"valve_internal": {
"desc1": "Valve 내부 개발자 메뉴를 활성화합니다.",
"label": "Valve 내부 개발자 메뉴 활성화",
@@ -140,7 +156,6 @@
"header": "베타 참가"
},
"developer_mode": {
"desc": "Decky의 개발자 설정을 활성화합니다.",
"label": "개발자 모드"
},
"other": {
@@ -148,12 +163,16 @@
},
"updates": {
"header": "업데이트"
},
"notifications": {
"header": "알림",
"plugin_updates_label": "플러그인 업데이트 가능",
"decky_updates_label": "Decky 업데이트 가능"
}
},
"SettingsIndex": {
"developer_title": "개발자",
"general_title": "일반",
"navbar_settings": "Decky 설정",
"plugins_title": "플러그인"
},
"Store": {
@@ -182,7 +201,11 @@
"alph_desc": "알파벳순 (A-Z)",
"title": "검색"
},
"store_testing_cta": "새로운 플러그인을 테스트하여 Decky Loader 팀을 도와주세요!"
"store_testing_cta": "새로운 플러그인을 테스트하여 Decky Loader 팀을 도와주세요!",
"store_testing_warning": {
"desc": "이 스토어 채널을 사용하여 가장 최신 버전의 플러그인을 테스트할 수 있습니다. GitHub에 피드백을 남겨서 모든 사용자가 업데이트 할 수 있게 해주세요.",
"label": "테스트 스토어 채널에 오신 것을 환영합니다"
}
},
"StoreSelect": {
"custom_store": {
@@ -193,7 +216,7 @@
"custom": "사용자 지정",
"label": "스토어 배포 채널",
"default": "기본",
"testing": "시험"
"testing": "테스트"
}
},
"Updater": {
@@ -210,5 +233,21 @@
"reloading": "다시 로드 중",
"updating": "업데이트 중"
}
},
"FilePickerError": {
"errors": {
"file_not_found": "지정된 경로가 잘못되었습니다. 확인 후에 다시 입력해 주세요.",
"unknown": "알 수 없는 오류가 발생했습니다. Raw 오류: {{raw_error}}",
"perm_denied": "선택한 경로에 접근 할 수 없습니다. 선택한 폴더/파일 접근 권한이 유저(Steam Deck의 deck 유저)에 맞게 올바르게 설정 되었는지 확인하세요."
}
},
"DropdownMultiselect": {
"button": {
"back": "뒤로"
}
},
"TitleView": {
"settings_desc": "Decky 설정 열기",
"decky_store_desc": "Decky 스토어 열기"
}
}
+243
View File
@@ -0,0 +1,243 @@
{
"BranchSelect": {
"update_channel": {
"prerelease": "Vooruitgave",
"stable": "Stabiel",
"label": "Update Kanaal",
"testing": "Test"
}
},
"Developer": {
"5secreload": "Herlaad in 5 seconden",
"disabling": "Uitschakelen React DevTools",
"enabling": "Inschakelen React DevTools"
},
"DropdownMultiselect": {
"button": {
"back": "Terug"
}
},
"FilePickerError": {
"errors": {
"unknown": "Een onbekende fout is opgetreden. De ruwe fout is: {{raw_error}}",
"file_not_found": "Het opgegeven pad is niet geldig. Controleer het en voer het opnieuw correct in."
}
},
"FilePickerIndex": {
"files": {
"all_files": "Alle bestanden",
"file_type": "Bestandstype",
"show_hidden": "Toon verborgen bestanden"
},
"filter": {
"created_desc": "Gecreëerd ( Nieuwste)",
"modified_asce": "Veranderd (Oudste)",
"modified_desc": "Veranderd (Nieuwste)",
"name_asce": "Z-A",
"name_desc": "A-Z",
"size_asce": "Grootte (Kleinste)",
"size_desc": "Grootte (Grootste)",
"created_asce": "Gecreëerd (Oudste)"
},
"folder": {
"label": "Map",
"select": "Gebruik deze map",
"show_more": "Toon meer bestanden"
}
},
"PluginView": {
"hidden_one": "1 plug-in is verborgen in deze lijst",
"hidden_other": "{{count}} plug-ins zijn verborgen in deze lijst"
},
"PluginListLabel": {
"hidden": "Verborgen in het snelmenu"
},
"PluginCard": {
"plugin_install": "Installeren",
"plugin_no_desc": "Geen beschrijving gegeven.",
"plugin_version_label": "Plugin Versie",
"plugin_full_access": "Deze plug-in heeft volledige toegang tot je Steam Deck."
},
"PluginInstallModal": {
"install": {
"button_idle": "Installeren",
"button_processing": "Bezig met installeren",
"title": "Installeer {{artifact}}",
"desc": "Weet je zeker dat je {{artifact}} {{version}} wilt installeren?"
},
"no_hash": "Deze plug-in heeft geen hash, u installeert deze op eigen risico.",
"reinstall": {
"button_idle": "Herinstalleren",
"button_processing": "Bezig te herinstalleren",
"desc": "Weet je zeker dat je {{artifact}} {{version}} opnieuw wilt installeren?",
"title": "Installeer {{artifact}} opnieuw"
},
"update": {
"button_idle": "Update",
"button_processing": "Bezig met updaten",
"title": "{{artifact}} bijwerken",
"desc": "Weet je zeker dat je {{artifact}} {{version}} wilt updaten?"
}
},
"MultiplePluginsInstallModal": {
"title": {
"mixed_one": "Wijzig {{count}} plug-in",
"mixed_other": "Pas {{count}} plug-ins aan",
"update_one": "1 plugin bijwerken",
"update_other": "{{count}} plug-ins bijwerken",
"install_one": "Installeer 1 plugin",
"install_other": "Installeer {{count}} plugins",
"reinstall_one": "1 plugin opnieuw installeren",
"reinstall_other": "{{count}} plugins opnieuw installeren"
},
"ok_button": {
"idle": "Bevestigen",
"loading": "Werkend"
},
"confirm": "Weet u zeker dat u de volgende wijzigingen wilt aanbrengen?",
"description": {
"install": "Installeer {{name}} {{version}}",
"update": "Update {{name}} naar {{version}}",
"reinstall": "Installeer opnieuw {{name}} {{version}}"
}
},
"PluginListIndex": {
"no_plugin": "Geen plugins geïnstalleerd!",
"plugin_actions": "Plugin Acties",
"reload": "Herladen",
"uninstall": "Verwijderen",
"update_to": "Update naar {{name}}",
"hide": "Snelle toegang: Verberg",
"update_all_one": "Update 1 plugin",
"update_all_other": "Update {{count}} plugins",
"reinstall": "Opnieuw installeren",
"show": "Snelle toegang: Toon"
},
"PluginLoader": {
"decky_title": "Decky",
"error": "Fout",
"plugin_load_error": {
"message": "Fout bij het laden van plugin {{name}}",
"toast": "Fout bij het laden van {{name}}"
},
"plugin_uninstall": {
"button": "Verwijderen",
"desc": "Weet je zeker dat je {{name}} wilt verwijderen?",
"title": "Verwijder {{name}}"
},
"plugin_update_one": "Updates beschikbaar voor 1 plugin!",
"plugin_update_other": "Updates beschikbaar voor {{count}} plugins!",
"decky_update_available": "Update naar {{tag_name}} beschikbaar!",
"plugin_error_uninstall": "Het laden van {{name}} veroorzaakte een uitzondering zoals hierboven weergegeven. Dit betekent meestal dat de plug-in een update vereist voor de nieuwe versie van SteamUI. Controleer of er een update aanwezig is of evalueer de verwijdering ervan in de Decky-instellingen, in het gedeelte Plug-ins."
},
"RemoteDebugging": {
"remote_cef": {
"desc": "Sta ongeauthenticeerde toegang tot de CEF-foutopsporing toe aan iedereen in uw netwerk",
"label": "Externe CEF-foutopsporing toestaan"
}
},
"SettingsDeveloperIndex": {
"cef_console": {
"button": "Console openen",
"label": "CEF Bedieningsscherm",
"desc": "Opent de CEF-console. Alleen nuttig voor foutopsporingsdoeleinden. Dingen hier zijn potentieel gevaarlijk en mogen alleen worden gebruikt als u een ontwikkelaar van plug-ins bent, of hier door een ontwikkelaar naartoe wordt geleid."
},
"header": "Andere",
"react_devtools": {
"ip_label": "IP",
"label": "Aanzetten React DevTools",
"desc": "Maakt verbinding met een computer met React DevTools mogelijk. Als je deze instelling wijzigt, wordt Steam opnieuw geladen. Stel het IP-adres in voordat u het inschakelt."
},
"third_party_plugins": {
"header": "Plug-ins van derden",
"label_desc": "URL",
"label_url": "Installeer Plugin van URL",
"label_zip": "Installeer Plugin van Zip bestand",
"button_install": "Installeren",
"button_zip": "Bladeren"
},
"valve_internal": {
"desc1": "Schakelt het interne ontwikkelaarsmenu van Valve in.",
"desc2": "Raak niets in dit menu aan tenzij u weet wat het doet.",
"label": "Valve Internal inschakelen"
}
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Decky versie",
"header": "Over"
},
"beta": {
"header": "Beta deelname"
},
"developer_mode": {
"label": "Ontwikkelaars modus"
},
"other": {
"header": "Overige"
},
"updates": {
"header": "Nieuwe Versies"
}
},
"SettingsIndex": {
"developer_title": "Ontwikkelaar",
"general_title": "Algemeen",
"plugins_title": "Plugins"
},
"Store": {
"store_filter": {
"label": "Filter",
"label_def": "Alles"
},
"store_search": {
"label": "Zoek"
},
"store_sort": {
"label": "Sorteren",
"label_def": "Laatste Geupdate (Nieuwste)"
},
"store_source": {
"label": "Bron Code",
"desc": "Alle broncode van de plug-in is beschikbaar in de SteamDeckHomebrew/decky-plugin-database-repository op GitHub."
},
"store_tabs": {
"about": "Over",
"alph_asce": "Alfabetisch (Z naar A)",
"alph_desc": "Alfabetisch (A naar Z)",
"title": "Bladeren"
},
"store_testing_cta": "Overweeg alsjeblieft om nieuwe plug-ins te testen om het Decky Loader-team te helpen!",
"store_contrib": {
"desc": "Als je wilt bijdragen aan de Decky Plugin winkel, kijk dan in de SteamDeckHomebrew/decky-plugin-template repository op GitHub. Informatie over ontwikkeling en distributie is beschikbaar in de README.",
"label": "Bijdragende"
}
},
"StoreSelect": {
"custom_store": {
"label": "Aangepassingen winkel",
"url_label": "URL"
},
"store_channel": {
"custom": "Aanpassingen",
"default": "Standaard",
"label": "Winkel Kanaal",
"testing": "Testen"
}
},
"Updater": {
"patch_notes_desc": "Correctie opmerkingen",
"updates": {
"check_button": "Controleer op updates",
"checking": "Controleren",
"cur_version": "Huidige versie: {{ver}}",
"install_button": "Installeer Update",
"label": "Update",
"lat_version": "Up-to-date: loopt {{ver}}",
"reloading": "Herstarten",
"updating": "Aan het updaten"
},
"decky_updates": "Decky Nieuwe Versies",
"no_patch_notes_desc": "geen correctie-opmerkingen voor deze versie"
}
}
+267
View File
@@ -0,0 +1,267 @@
{
"BranchSelect": {
"update_channel": {
"testing": "Testowy",
"label": "Kanał aktualizacji",
"stable": "Stabilny",
"prerelease": "Przedpremierowy"
}
},
"Developer": {
"enabling": "Włączanie React DevTools",
"5secreload": "Ponowne załadowanie za 5 sekund",
"disabling": "Wyłączanie React DevTools"
},
"DropdownMultiselect": {
"button": {
"back": "Powrót"
}
},
"FilePickerError": {
"errors": {
"perm_denied": "Nie masz dostępu do podanego katalogu. Sprawdź, czy twój użytkownik (deck na Steam Deck) ma odpowiednie uprawnienia dostępu do określonego katalogu/pliku.",
"unknown": "Wystąpił nieznany błąd. Surowy błąd to {{raw_error}}",
"file_not_found": "Podana ścieżka jest nieprawidłowa. Sprawdź ją i wprowadź ponownie poprawnie."
}
},
"FilePickerIndex": {
"file": {
"select": "Wybierz ten plik"
},
"files": {
"all_files": "Wszystkie pliki",
"file_type": "Typ pliku",
"show_hidden": "Pokaż ukryte pliki"
},
"filter": {
"created_asce": "Utworzono (najstarszy)",
"created_desc": "Utworzono (najnowszy)",
"modified_asce": "Zmodyfikowany (najstarszy)",
"modified_desc": "Zmodyfikowany (najnowszy)",
"name_asce": "Z-A",
"name_desc": "A-Z",
"size_asce": "Rozmiar (najmniejszy)",
"size_desc": "Rozmiar (największy)"
},
"folder": {
"label": "Katalog",
"select": "Użyj tego katalogu",
"show_more": "Pokaż więcej plików"
}
},
"MultiplePluginsInstallModal": {
"title": {
"mixed_one": "Zmodyfikuj {{count}} plugin",
"mixed_few": "Zmodyfikuj {{count}} pluginy",
"mixed_many": "Zmodyfikuj {{count}} pluginów",
"reinstall_one": "Reinstaluj 1 plugin",
"reinstall_few": "Reinstaluj {{count}} pluginy",
"reinstall_many": "Reinstaluj {{count}} pluginów",
"install_one": "Zainstaluj 1 plugin",
"install_few": "Zainstaluj {{count}} pluginy",
"install_many": "Zainstaluj {{count}} pluginów",
"update_one": "Zaktualizuj 1 plugin",
"update_few": "Zaktualizuj {{count}} pluginy",
"update_many": "Zaktualizuj {{count}} pluginów"
},
"confirm": "Czy na pewno chcesz wprowadzić następujące modyfikacje?",
"description": {
"install": "Zainstaluj {{name}} {{version}}",
"reinstall": "Reinstaluj {{name}} {{version}}",
"update": "Zaktualizuj {{name}} do {{version}}"
},
"ok_button": {
"idle": "Potwierdź",
"loading": "W toku"
}
},
"PluginCard": {
"plugin_install": "Zainstaluj",
"plugin_no_desc": "Brak opisu.",
"plugin_version_label": "Wersja pluginu",
"plugin_full_access": "Ten plugin ma pełny dostęp do twojego Steam Decka."
},
"PluginInstallModal": {
"install": {
"button_idle": "Zainstaluj",
"button_processing": "Instalowanie",
"desc": "Czy na pewno chcesz zainstalować {{artifact}} {{version}}?",
"title": "Zainstaluj {{artifact}}"
},
"reinstall": {
"button_idle": "Reinstaluj",
"button_processing": "Reinstalowanie",
"desc": "Czy na pewno chcesz ponownie zainstalować {{artifact}} {{version}}?",
"title": "Reinstaluj {{artifact}}"
},
"update": {
"button_idle": "Aktualizacja",
"button_processing": "Aktualizowanie",
"desc": "Czy na pewno chcesz zaktualizować {{artifact}} {{version}}?",
"title": "Zaktualizuj {{artifact}}"
},
"no_hash": "Ten plugin nie ma hasha, instalujesz go na własne ryzyko."
},
"PluginListIndex": {
"hide": "Szybki dostęp: Ukryj",
"no_plugin": "Brak zainstalowanych pluginów!",
"reload": "Załaduj ponownie",
"update_all_one": "Zaktualizuj 1 plugin",
"update_all_few": "Zaktualizuj {{count}} pluginy",
"update_all_many": "Zaktualizuj {{count}} pluginów",
"plugin_actions": "Akcje pluginów",
"reinstall": "Reinstalacja",
"show": "Szybki dostęp: Pokaż",
"uninstall": "Odinstaluj",
"update_to": "Zaktualizuj do {{name}}"
},
"PluginLoader": {
"decky_title": "Decky",
"decky_update_available": "Dostępna aktualizacja do {{tag_name}}!",
"error": "Błąd",
"plugin_error_uninstall": "Ładowanie {{name}} spowodowało wyjątek, jak pokazano powyżej. Zwykle oznacza to, że plugin wymaga aktualizacji do nowej wersji SteamUI. Sprawdź, czy aktualizacja jest obecna lub rozważ usunięcie go w ustawieniach Decky, w sekcji Pluginy.",
"plugin_load_error": {
"message": "Błąd ładowania plugin {{name}}",
"toast": "Błąd ładowania {{name}}"
},
"plugin_uninstall": {
"button": "Odinstaluj",
"title": "Odinstaluj {{name}}",
"desc": "Czy na pewno chcesz odinstalować {{name}}?"
},
"plugin_update_one": "Aktualizacje dostępne dla 1 pluginu!",
"plugin_update_few": "Aktualizacje dostępne dla {{count}} pluginów!",
"plugin_update_many": "Aktualizacje dostępne dla {{count}} pluginów!"
},
"PluginListLabel": {
"hidden": "Ukryty w menu szybkiego dostępu"
},
"PluginView": {
"hidden_one": "1 plugin jest ukryty na tej liście",
"hidden_few": "{{count}} pluginy jest ukryty na tej liście",
"hidden_many": "{{count}} pluginów jest ukryty na tej liście"
},
"RemoteDebugging": {
"remote_cef": {
"desc": "Zezwalaj na nieuwierzytelniony dostęp do debugera CEF wszystkim osobom w Twojej sieci",
"label": "Zezwól na zdalne debugowanie CEF"
}
},
"SettingsDeveloperIndex": {
"cef_console": {
"button": "Otwórz konsolę",
"desc": "Otwiera konsolę CEF. Przydatne tylko do celów debugowania. Rzeczy tutaj są potencjalnie niebezpieczne i powinny być używane tylko wtedy, gdy jesteś twórcą wtyczek lub zostałeś tu przez kogoś skierowany.",
"label": "Konsola CEF"
},
"header": "Inne",
"react_devtools": {
"desc": "Umożliwia połączenie z komputerem z uruchomionym React DevTools. Zmiana tego ustawienia spowoduje ponowne załadowanie Steam. Ustaw adres IP przed włączeniem.",
"ip_label": "IP",
"label": "Włącz React DevTools"
},
"third_party_plugins": {
"button_install": "Zainstaluj",
"button_zip": "Przeglądaj",
"header": "Pluginy zewnętrzne",
"label_desc": "URL",
"label_url": "Zainstaluj plugin z adresu URL",
"label_zip": "Zainstaluj plugin z pliku ZIP"
},
"valve_internal": {
"desc1": "Włącza wewnętrzne menu programisty Valve.",
"desc2": "Nie dotykaj niczego w tym menu, chyba że wiesz, co robi.",
"label": "Włącz Valve Internal"
}
},
"SettingsGeneralIndex": {
"notifications": {
"decky_updates_label": "Dostępna aktualizacja Decky",
"header": "Powiadomienia",
"plugin_updates_label": "Dostępne aktualizacje pluginów"
},
"other": {
"header": "Inne"
},
"updates": {
"header": "Aktualizacje"
},
"about": {
"header": "Informacje",
"decky_version": "Wersja Decky"
},
"beta": {
"header": "Udział w becie"
},
"developer_mode": {
"label": "Tryb dewelopera"
}
},
"SettingsIndex": {
"developer_title": "Deweloper",
"general_title": "Ogólne",
"plugins_title": "Pluginy"
},
"Store": {
"store_contrib": {
"desc": "Jeśli chcesz przyczynić się do rozwoju Decky Plugin Store, sprawdź repozytorium SteamDeckHomebrew/decky-plugin-template na GitHub. Informacje na temat rozwoju i dystrybucji są dostępne w pliku README.",
"label": "Współtworzenie"
},
"store_filter": {
"label": "Filtr",
"label_def": "Wszystko"
},
"store_search": {
"label": "Szukaj"
},
"store_sort": {
"label": "Sortowanie",
"label_def": "Ostatnia aktualizacja (najnowsza)"
},
"store_source": {
"desc": "Cały kod źródłowy pluginów jest dostępny w repozytorium SteamDeckHomebrew/decky-plugin-database na GitHub.",
"label": "Kod źródłowy"
},
"store_tabs": {
"alph_asce": "Alfabetycznie (od Z do A)",
"alph_desc": "Alfabetycznie (od A do Z)",
"title": "Przeglądaj",
"about": "Informacje"
},
"store_testing_cta": "Rozważ przetestowanie nowych pluginów, aby pomóc zespołowi Decky Loader!",
"store_testing_warning": {
"label": "Witamy w Testowym Kanale Sklepu",
"desc": "Możesz użyć tego kanału sklepu do testowania najnowszych wersji pluginów. Pamiętaj, aby zostawić opinię na GitHub, aby plugin mogła zostać zaktualizowana dla wszystkich użytkowników."
}
},
"StoreSelect": {
"custom_store": {
"label": "Niestandardowy sklep",
"url_label": "URL"
},
"store_channel": {
"custom": "Niestandardowy",
"default": "Domyślny",
"label": "Kanał sklepu",
"testing": "Testowy"
}
},
"Updater": {
"decky_updates": "Aktualizacje Decky",
"no_patch_notes_desc": "Brak informacji o poprawkach dla tej wersji",
"patch_notes_desc": "Opis zmian",
"updates": {
"check_button": "Sprawdź aktualizacje",
"checking": "Sprawdzanie",
"cur_version": "Aktualna wersja: {{ver}}",
"install_button": "Zainstaluj aktualizację",
"label": "Aktualizacje",
"lat_version": "Aktualizacje zainstalowane. Aktualna wersja: {{ver}}",
"reloading": "Ponowne ładowanie",
"updating": "Aktualizowanie"
}
},
"TitleView": {
"settings_desc": "Otwórz ustawienia Decky",
"decky_store_desc": "Otwórz sklep Decky"
}
}
+41 -10
View File
@@ -14,7 +14,27 @@
},
"FilePickerIndex": {
"folder": {
"select": "Use esta pasta"
"select": "Use esta pasta",
"label": "Pasta",
"show_more": "Mostrar mais arquivos"
},
"files": {
"show_hidden": "Mostrar Arquivos Ocultos",
"all_files": "Todos os arquivos",
"file_type": "Formato de arquivo"
},
"filter": {
"created_asce": "Criado (Mais antigo)",
"created_desc": "Criado (Mais recente)",
"modified_asce": "Alterado (Mais antigo)",
"name_asce": "Z-A",
"name_desc": "A-Z",
"size_asce": "Tamanho (Menor)",
"size_desc": "Tamanho (Maior)",
"modified_desc": "Alterado (Mais recente)"
},
"file": {
"select": "Selecione este arquivo"
}
},
"PluginListLabel": {
@@ -49,7 +69,7 @@
},
"MultiplePluginsInstallModal": {
"title": {
"mixed_one": "Modificar 1 plugin",
"mixed_one": "Modificar {{count}} plugin",
"mixed_many": "Modificar {{count}} plugins",
"mixed_other": "Modificar {{count}} plugins",
"update_one": "Atualizar 1 plugin",
@@ -100,7 +120,7 @@
},
"decky_update_available": "Atualização para {{tag_name}} disponível!",
"plugin_error_uninstall": "Um erro aconteceu ao carregar {{name}}, como mostrado acima. Isso normalmente significa que o plugin precisa de uma atualização para a nova versão do SteamUI. Confira se existe uma atualização ou avalie a remoção do plugin nas configurações do Decky, na sessão de plugins.",
"plugin_update_one": "Atualizações disponível para 1 plugin!",
"plugin_update_one": "Atualização disponível para 1 plugin!",
"plugin_update_many": "Atualizações disponíveis para {{count}} plugins!",
"plugin_update_other": "Atualizações disponíveis para {{count}} plugins!"
},
@@ -130,10 +150,6 @@
"label_zip": "Instalar Plugin a partir de um arquivo ZIP",
"label_desc": "URL"
},
"toast_zip": {
"title": "Decky",
"body": "Falha na instalação! Somente arquivos ZIP são suportados."
},
"valve_internal": {
"desc1": "Habilita o menu interno de desenvolvedor da Valve.",
"desc2": "Não toque em nada neste menu, a não ser que você saiba o que está fazendo.",
@@ -146,7 +162,6 @@
"header": "Sobre"
},
"developer_mode": {
"desc": "Habilita as configurações de desenvolvedor do Decky.",
"label": "Modo Deselvolvedor"
},
"other": {
@@ -157,12 +172,16 @@
},
"beta": {
"header": "Participação no Beta"
},
"notifications": {
"decky_updates_label": "Atualização do Decky disponível",
"header": "Noificações",
"plugin_updates_label": "Atualizações de Plugin disponíveis"
}
},
"SettingsIndex": {
"developer_title": "Desenvolvedor",
"general_title": "Geral",
"navbar_settings": "Configurações do Decky",
"plugins_title": "Plugins"
},
"Store": {
@@ -182,7 +201,7 @@
"label_def": "Último atualizado (Mais recente)"
},
"store_source": {
"desc": "Todos os codigo fonte dos plugins estão disponíveis no repositório SteamDeckHomebrew/decky-plugin-database no GitHub.",
"desc": "Todos os códigos fonte dos plugins estão disponíveis no repositório SteamDeckHomebrew/decky-plugin-database no GitHub.",
"label": "Código Fonte"
},
"store_tabs": {
@@ -224,5 +243,17 @@
"hidden_one": "1 plugin está oculto nesta lista",
"hidden_many": "{{count}} plugins estão ocultos nesta lista",
"hidden_other": "{{count}} plugins estão ocultos nesta lista"
},
"DropdownMultiselect": {
"button": {
"back": "Voltar"
}
},
"FilePickerError": {
"errors": {
"file_not_found": "O caminho especificado não é válido. Por favor, confira e reinsira corretamente.",
"unknown": "Ocorreu um erro desconhecido. O erro completo é: {{raw_error}}",
"perm_denied": "Você não tem acesso à este diretório. Por favor, verifiquei se seu usuário (deck no Steam Deck) tem as permissões necessárias para acessar este arquivo/pasta."
}
}
}
+1 -7
View File
@@ -129,10 +129,6 @@
"label_url": "Instalar plugin a partir dum URL",
"label_zip": "Instalar plugin a partir dum ficheiro ZIP"
},
"toast_zip": {
"title": "Decky",
"body": "A instalação falhou! Só ficheiros ZIP são suportados."
},
"valve_internal": {
"label": "Activar menu interno da Valve",
"desc1": "Activa o menu interno de programador da Valve.",
@@ -154,8 +150,7 @@
"header": "Participação na versão Beta"
},
"developer_mode": {
"label": "Modo de programador",
"desc": "Activa as definições de programador do Decky."
"label": "Modo de programador"
},
"other": {
"header": "Outros"
@@ -167,7 +162,6 @@
"SettingsIndex": {
"developer_title": "Programador",
"general_title": "Geral",
"navbar_settings": "Definições do Decky",
"plugins_title": "Plugins"
},
"Store": {
+255 -21
View File
@@ -1,33 +1,267 @@
{
"MultiplePluginsInstallModal": {
"title": {
"update_one": "",
"update_few": "",
"update_many": "",
"reinstall_one": "",
"reinstall_few": "",
"reinstall_many": "",
"install_one": "",
"install_few": "",
"install_many": "",
"mixed_one": "",
"mixed_few": "",
"mixed_many": ""
"update_one": "Переустановить {{count}} плагин",
"update_few": "Переустановить {{count}} плагинов",
"update_many": "Переустановить {{count}} плагинов",
"reinstall_one": "Переустановить {{count}} плагин",
"reinstall_few": "Переустановить {{count}} плагинов",
"reinstall_many": "Переустановить {{count}} плагинов",
"install_one": "Установить {{count}} плагин",
"install_few": "Установить {{count}} плагинов",
"install_many": "Установить {{count}} плагинов",
"mixed_one": "Изменить {{count}} плагин",
"mixed_few": "Изменить {{count}} плагинов",
"mixed_many": "Изменить {{count}} плагинов"
},
"description": {
"install": "Установить {{name}} {{version}}",
"reinstall": "Переустановить {{name}} {{version}}",
"update": "Обновить с {{name}} на {{version}}"
},
"confirm": "Вы уверены, что хотите внести следующие изменения?",
"ok_button": {
"idle": "Подтвердить",
"loading": "В процессе"
}
},
"PluginListIndex": {
"update_all_one": "",
"update_all_few": "",
"update_all_many": ""
"update_all_one": "Обновить {{count}} плагин",
"update_all_few": "Обновить {{count}} плагинов",
"update_all_many": "Обновить {{count}} плагинов",
"hide": "Быстрый доступ: Скрыть",
"reload": "Перезагрузить",
"uninstall": "Удалить",
"update_to": "Обновить на {{name}}",
"show": "Быстрый доступ: Показать",
"plugin_actions": "Действия с плагинами",
"no_plugin": "Не установлено ни одного плагина!",
"reinstall": "Переустановить"
},
"PluginLoader": {
"plugin_update_one": "",
"plugin_update_few": "",
"plugin_update_many": ""
"plugin_update_one": "Обновления доступны для {{count}} плагина!",
"plugin_update_few": "Обновления доступны для {{count}} плагинов!",
"plugin_update_many": "Обновления доступны для {{count}} плагинов!",
"plugin_error_uninstall": "Загрузка {{name}} вызвала исключение, указанное выше. Обычно это означает, что плагин требует обновления для новой версии SteamUI. Проверьте наличие обновления или попробуйте его удалить в настройках Decky, в разделе Плагины.",
"plugin_load_error": {
"message": "Ошибка загрузки плагина {{name}}",
"toast": "Ошибка загрузки {{name}}"
},
"plugin_uninstall": {
"button": "Удалить",
"desc": "Вы уверены, что хотите удалить {{name}}?",
"title": "Удалить {{name}}"
},
"decky_title": "Decky",
"decky_update_available": "Доступно обновление на {{tag_name}}!",
"error": "Ошибка"
},
"PluginView": {
"hidden_one": "",
"hidden_few": "",
"hidden_many": ""
"hidden_one": "{{count}} плагин скрыт из списка",
"hidden_few": "{{count}} плагинов скрыт из списка",
"hidden_many": "{{count}} плагинов скрыт из списка"
},
"FilePickerIndex": {
"files": {
"show_hidden": "Показать скрытые файлы",
"all_files": "Все файлы",
"file_type": "Тип файла"
},
"filter": {
"created_asce": "Создан (самый старый)",
"modified_asce": "Модифицирован (самый новый)",
"modified_desc": "Модифицирован (самый старый)",
"size_asce": "Размер (самый малый)",
"size_desc": "Размер (самый большой)",
"name_asce": "Z-A",
"name_desc": "A-Z",
"created_desc": "Создан (самый новый)"
},
"folder": {
"label": "Папка",
"show_more": "Показать больше файлов",
"select": "Использовать этот каталог"
},
"file": {
"select": "Выберите этот файл"
}
},
"PluginCard": {
"plugin_install": "Установить",
"plugin_no_desc": "Нет описания.",
"plugin_version_label": "Версия плагина",
"plugin_full_access": "Этот плагин имеет полный доступ к вашему Steam Deck."
},
"PluginInstallModal": {
"install": {
"button_processing": "Установка",
"title": "Установить {{artifact}}",
"button_idle": "Установить",
"desc": "Вы уверены, что хотите установить {{artifact}} {{version}}?"
},
"no_hash": "У данного плагина отсутствует хэш, устанавливайте на свой страх и риск.",
"reinstall": {
"title": "Переустановить {{artifact}}",
"desc": "Вы уверены, что хотите переустановить {{artifact}} {{version}}?",
"button_idle": "Переустановить",
"button_processing": "Переустановка"
},
"update": {
"button_idle": "Обновить",
"button_processing": "Обновление",
"desc": "Вы уверены, что хотите обновить {{artifact}} {{version}}?",
"title": "Обновить {{artifact}}"
}
},
"PluginListLabel": {
"hidden": "Скрыто из меню быстрого доступа"
},
"RemoteDebugging": {
"remote_cef": {
"desc": "Разрешить неаутентифицированный доступ к отладчику CEF всем в вашей сети",
"label": "Разрешить удаленную отладку CEF"
}
},
"SettingsDeveloperIndex": {
"header": "Другое",
"third_party_plugins": {
"button_install": "Установить",
"label_zip": "Установить плагин из ZIP файла",
"label_url": "Установить плагин из URL",
"button_zip": "Обзор",
"header": "Сторонние плагины",
"label_desc": "Ссылка"
},
"react_devtools": {
"ip_label": "IP",
"desc": "Позволяет подключиться к компьютеру, на котором работает React DevTools. Изменение этого параметра приведет к перезагрузке Steam. Установите IP-адрес перед включением.",
"label": "Включить React DevTools"
},
"cef_console": {
"button": "Открыть консоль",
"desc": "Открывает консоль CEF. Полезно только для целей отладки. Настройки здесь потенциально опасны и должны использоваться только в том случае, если вы являетесь разработчиком плагинов или направленны сюда одним из них.",
"label": "CEF Консоль"
},
"valve_internal": {
"desc1": "Включает внутреннее меню разработчика Valve.",
"label": "Включить Valve Internal",
"desc2": "Ничего не трогайте в этом меню, если не знаете, что оно делает."
}
},
"SettingsGeneralIndex": {
"beta": {
"header": "Бета программа"
},
"developer_mode": {
"label": "Режим разработчика"
},
"other": {
"header": "Другое"
},
"about": {
"decky_version": "Версия Decky",
"header": "Информация"
},
"updates": {
"header": "Обновления"
},
"notifications": {
"decky_updates_label": "Обновление Decky доступно",
"header": "Уведомления",
"plugin_updates_label": "Доступны обновления плагинов"
}
},
"Store": {
"store_sort": {
"label": "Сортировка",
"label_def": "Последнее обновление(самые новые)"
},
"store_source": {
"label": "Исходный код",
"desc": "Весь исходный код плагина доступен в репозитории SteamDeckHomebrew/decky-plugin-database на GitHub."
},
"store_tabs": {
"about": "Информация",
"alph_desc": "По алфавиту (A - Z)",
"title": "Обзор",
"alph_asce": "По алфавиту (Z - A)"
},
"store_testing_cta": "Пожалуйста, рассмотрите возможность тестирования новых плагинов, чтобы помочь команде Decky Loader!",
"store_contrib": {
"desc": "Если вы хотите внести свой вклад в магазин плагинов Decky, проверьте репозиторий SteamDeckHomebrew/decky-plugin-template на GitHub. Информация о разработке и распространении доступна в README.",
"label": "Помощь проекту"
},
"store_filter": {
"label": "Фильтр",
"label_def": "Все"
},
"store_search": {
"label": "Поиск"
},
"store_testing_warning": {
"label": "Добро пожаловать в тестовый канал магазина",
"desc": "Вы можете использовать этот канал магазина для тестирования новейших версий плагинов. Не забудьте оставить отзыв на GitHub, чтобы плагин можно было обновить для всех пользователей."
}
},
"StoreSelect": {
"custom_store": {
"label": "Сторонний магазин",
"url_label": "URL"
},
"store_channel": {
"custom": "Сторонний",
"default": "По-умолчанию",
"label": "Канал магазина",
"testing": "Тестовый"
}
},
"Updater": {
"decky_updates": "Обновления Decky",
"no_patch_notes_desc": "нет примечаний к патчу для этой версии",
"updates": {
"check_button": "Проверить обновления",
"checking": "Проверка",
"cur_version": "Текущая версия: {{ver}}",
"updating": "Обновление",
"install_button": "Установить обновление",
"label": "Обновления",
"lat_version": "Обновлено: версия {{ver}}",
"reloading": "Перезагрузка"
},
"patch_notes_desc": "Примечания к патчу"
},
"FilePickerError": {
"errors": {
"perm_denied": "У вас нет доступа к указанному каталогу.. Пожалуйста, проверьте имеет ли пользователь (deck на Steam Deck) соответствующие права доступа к указанной папке/файлу.",
"file_not_found": "Указан недействительный путь. Пожалуйста, проверьте его и повторите ввод.",
"unknown": "Произошла неизвестная ошибка. Текст ошибки: {{raw_error}}"
}
},
"DropdownMultiselect": {
"button": {
"back": "Назад"
}
},
"BranchSelect": {
"update_channel": {
"prerelease": "Предрелиз",
"stable": "Стабильный",
"testing": "Тестовый",
"label": "Канал обновлений"
}
},
"Developer": {
"5secreload": "Перезагрузка через 5 секунд",
"disabling": "Выключение React DevTools",
"enabling": "Включение React DevTools"
},
"SettingsIndex": {
"developer_title": "Разработчик",
"general_title": "Общее",
"plugins_title": "Плагины"
},
"TitleView": {
"decky_store_desc": "Открыть магазин Decky",
"settings_desc": "Открыть настройки Decky"
}
}
+1 -5
View File
@@ -11,9 +11,6 @@
"label_desc": "URL",
"label_url": "Instalo Shtes Nga URL",
"label_zip": "Instalo Shtes Nga ZIP"
},
"toast_zip": {
"title": "Decky"
}
},
"BranchSelect": {
@@ -82,8 +79,7 @@
},
"SettingsIndex": {
"developer_title": "Zhvillues",
"general_title": "Gjeneral",
"navbar_settings": "Cilësimet Decky"
"general_title": "Gjeneral"
},
"Store": {
"store_sort": {
-6
View File
@@ -124,10 +124,6 @@
"label_zip": "Встановити плагін з ZIP-файлу",
"button_zip": "Огляд"
},
"toast_zip": {
"title": "Decky",
"body": "Помилка встановлення! Підтримуються лише ZIP-файли."
},
"valve_internal": {
"desc1": "Вмикає внутрішнє розробницьке меню Valve.",
"label": "Увімкнути Valve Internal",
@@ -143,7 +139,6 @@
"header": "Участь у Beta"
},
"developer_mode": {
"desc": "Вмикає розробницькі налаштування Decky.",
"label": "Розробницький режим"
},
"other": {
@@ -156,7 +151,6 @@
"SettingsIndex": {
"developer_title": "Розробник",
"general_title": "Загальне",
"navbar_settings": "Налаштування Decky",
"plugins_title": "Плагіни"
},
"Store": {
+51 -12
View File
@@ -14,7 +14,27 @@
},
"FilePickerIndex": {
"folder": {
"select": "使用这个文件夹"
"select": "使用这个文件夹",
"label": "文件夹",
"show_more": "显示更多文件"
},
"filter": {
"created_asce": "创建日期(最旧)",
"created_desc": "创建日期(最新)",
"modified_asce": "修改日期(最旧)",
"modified_desc": "修改日期(最新)",
"name_asce": "字母降序",
"name_desc": "字母升序",
"size_asce": "大小(最小)",
"size_desc": "大小(最大)"
},
"files": {
"all_files": "全部文件",
"file_type": "文件类型",
"show_hidden": "显示隐藏文件"
},
"file": {
"select": "选择此文件"
}
},
"PluginCard": {
@@ -52,8 +72,8 @@
"uninstall": "卸载",
"update_to": "更新 {{name}}",
"update_all_other": "更新 {{count}} 个插件",
"show": "快速访问菜单:可见",
"hide": "快速访问菜单:不可见"
"show": "快速访问菜单中显示",
"hide": "快速访问菜单中隐藏"
},
"PluginLoader": {
"decky_title": "Decky",
@@ -91,10 +111,6 @@
"label_url": "从 URL 安装插件",
"label_zip": "从 ZIP 压缩文件安装插件"
},
"toast_zip": {
"title": "Decky",
"body": "安装失败!只有 ZIP 格式的插件被支持。"
},
"valve_internal": {
"desc1": "启用 Valve 内部开发者菜单。",
"desc2": "除非你知道你在干什么,否则请不要修改此菜单中的任何内容。",
@@ -116,20 +132,23 @@
"header": "参与测试"
},
"developer_mode": {
"label": "开发者模式",
"desc": "启用 Decky 的开发者测试。"
"label": "开发者模式"
},
"other": {
"header": "其他"
},
"updates": {
"header": "更新"
},
"notifications": {
"header": "通知",
"decky_updates_label": "Decky 更新可用",
"plugin_updates_label": "插件更新可用"
}
},
"SettingsIndex": {
"developer_title": "开发者",
"general_title": "通用",
"navbar_settings": "Decky 设置",
"plugins_title": "插件"
},
"Store": {
@@ -158,7 +177,11 @@
"alph_desc": "字母排序 (A 到 Z)",
"title": "浏览"
},
"store_testing_cta": "请考虑测试新插件以帮助 Decky Loader 团队!"
"store_testing_cta": "请考虑测试新插件以帮助 Decky Loader 团队!",
"store_testing_warning": {
"desc": "你可以使用该商店频道以体验最新版本的插件。 请在插件 Github 页面留言以使插件可以正式面向所有用户。",
"label": "欢迎来到商店测试频道"
}
},
"StoreSelect": {
"store_channel": {
@@ -206,9 +229,25 @@
}
},
"PluginListLabel": {
"hidden": "快速访问菜单中不可见"
"hidden": "快速访问菜单中已隐藏"
},
"PluginView": {
"hidden_other": "此列表隐藏了 {{count}} 个插件"
},
"DropdownMultiselect": {
"button": {
"back": "返回"
}
},
"FilePickerError": {
"errors": {
"file_not_found": "指定路径无效。请检查并输入正确的路径。",
"unknown": "发生了一个未知错误。原始错误为:{{raw_error}}",
"perm_denied": "你没有访问特定目录的权限。请检查你的用户(Steam Deck 中的 deck 账户)有着相对应的权限以访问特定的文件夹或文件。"
}
},
"TitleView": {
"decky_store_desc": "打开 Decky 商店",
"settings_desc": "打开 Decky 设置"
}
}
+40 -9
View File
@@ -14,7 +14,27 @@
},
"FilePickerIndex": {
"folder": {
"select": "使用此資料夾"
"select": "使用此資料夾",
"show_more": "顯示更多檔案",
"label": "資料夾"
},
"filter": {
"modified_asce": "修改日期(舊到新)",
"created_desc": "建立日期(新到舊)",
"modified_desc": "修改日期(新到舊)",
"name_desc": "子母排序(A到Z",
"name_asce": "子母排序(Z到A",
"size_asce": "檔案大小(小到大)",
"size_desc": "檔案大小(大到小)",
"created_asce": "建立日期(舊到新)"
},
"file": {
"select": "選擇此檔案"
},
"files": {
"all_files": "所有檔案",
"file_type": "檔案類型",
"show_hidden": "顯示隱藏檔"
}
},
"PluginCard": {
@@ -86,10 +106,6 @@
"button_install": "安裝",
"header": "第三方外掛程式"
},
"toast_zip": {
"body": "安裝失敗!只支援 ZIP 檔案。",
"title": "Decky"
},
"valve_internal": {
"desc2": "除非您知道它的作用,否則不要碰這個選單中的任何東西。",
"desc1": "啟用 Valve 內建開發人員選單。",
@@ -104,7 +120,7 @@
"cef_console": {
"button": "開啟控制台",
"label": "CEF 控制台",
"desc": "開啟 CEF 控制台。僅用於偵錯。這裡的東西有潛在的風險,只有當是一個外掛程式開發者或者被外掛程式開發者引導到這裡時,才應該使用。"
"desc": "開啟 CEF 控制台。僅用於偵錯。這裡的東西有潛在的風險,只有當是一個外掛程式開發者或者被外掛程式開發者引導到這裡時,才應該使用。"
}
},
"SettingsGeneralIndex": {
@@ -116,20 +132,23 @@
"header": "參與測試"
},
"developer_mode": {
"label": "開發人員模式",
"desc": "啟用 Decky 的開發人員模式。"
"label": "開發人員模式"
},
"other": {
"header": "其他"
},
"updates": {
"header": "更新"
},
"notifications": {
"decky_updates_label": "Decky 可更新",
"header": "通知",
"plugin_updates_label": "外掛程式有更新"
}
},
"SettingsIndex": {
"developer_title": "開發人員",
"general_title": "一般",
"navbar_settings": "Decky 設定",
"plugins_title": "外掛程式"
},
"Store": {
@@ -210,5 +229,17 @@
"update": "更新 {{name}} 到 {{version}}",
"reinstall": "重新安裝 {{name}} {{version}}"
}
},
"FilePickerError": {
"errors": {
"perm_denied": "您沒有瀏覽此目錄的權限。請檢查您的使用者(Steam Deck 中的 deck 帳號)有權限瀏覽特定的資料夾或檔案。",
"unknown": "發生未知錯誤。錯誤詳細資料:{{raw_error}}",
"file_not_found": "指定路徑無效。請檢查並輸入正確路徑。"
}
},
"DropdownMultiselect": {
"button": {
"back": "返回"
}
}
}
+3 -189
View File
@@ -1,190 +1,4 @@
# Change PyInstaller files permissions
import sys
from localplatform import (chmod, chown, service_stop, service_start,
ON_WINDOWS, get_log_level, get_live_reload,
get_server_port, get_server_host, get_chown_plugin_path,
get_unprivileged_user, get_unprivileged_path,
get_privileged_path)
if hasattr(sys, '_MEIPASS'):
chmod(sys._MEIPASS, 755)
# Full imports
from asyncio import new_event_loop, set_event_loop, sleep
from json import dumps, loads
from logging import DEBUG, INFO, basicConfig, getLogger
from os import getenv, path
from traceback import format_exc
import multiprocessing
import aiohttp_cors
# Partial imports
from aiohttp import client_exceptions, WSMsgType
from aiohttp.web import Application, Response, get, run_app, static
from aiohttp_jinja2 import setup as jinja_setup
# local modules
from browser import PluginBrowser
from helpers import (REMOTE_DEBUGGER_UNIT, csrf_middleware, get_csrf_token,
mkdir_as_user, get_system_pythonpaths)
from injector import get_gamepadui_tab, Tab, get_tabs, close_old_tabs
from loader import Loader
from settings import SettingsManager
from updater import Updater
from utilities import Utilities
from customtypes import UserType
basicConfig(
level=get_log_level(),
format="[%(module)s][%(levelname)s]: %(message)s"
)
logger = getLogger("Main")
plugin_path = path.join(get_privileged_path(), "plugins")
def chown_plugin_dir():
if not path.exists(plugin_path): # For safety, create the folder before attempting to do anything with it
mkdir_as_user(plugin_path)
if not chown(plugin_path, UserType.HOST_USER) or not chmod(plugin_path, 555):
logger.error(f"chown/chmod exited with a non-zero exit code")
if get_chown_plugin_path() == True:
chown_plugin_dir()
class PluginManager:
def __init__(self, loop) -> None:
self.loop = loop
self.web_app = Application()
self.web_app.middlewares.append(csrf_middleware)
self.cors = aiohttp_cors.setup(self.web_app, defaults={
"https://steamloopback.host": aiohttp_cors.ResourceOptions(
expose_headers="*",
allow_headers="*",
allow_credentials=True
)
})
self.plugin_loader = Loader(self.web_app, plugin_path, self.loop, get_live_reload())
self.settings = SettingsManager("loader", path.join(get_privileged_path(), "settings"))
self.plugin_browser = PluginBrowser(plugin_path, self.plugin_loader.plugins, self.plugin_loader, self.settings)
self.utilities = Utilities(self)
self.updater = Updater(self)
jinja_setup(self.web_app)
async def startup(_):
if self.settings.getSetting("cef_forward", False):
self.loop.create_task(service_start(REMOTE_DEBUGGER_UNIT))
else:
self.loop.create_task(service_stop(REMOTE_DEBUGGER_UNIT))
self.loop.create_task(self.loader_reinjector())
self.loop.create_task(self.load_plugins())
self.web_app.on_startup.append(startup)
self.loop.set_exception_handler(self.exception_handler)
self.web_app.add_routes([get("/auth/token", self.get_auth_token)])
for route in list(self.web_app.router.routes()):
self.cors.add(route)
self.web_app.add_routes([static("/static", path.join(path.dirname(__file__), 'static'))])
self.web_app.add_routes([static("/legacy", path.join(path.dirname(__file__), 'legacy'))])
def exception_handler(self, loop, context):
if context["message"] == "Unclosed connection":
return
loop.default_exception_handler(context)
async def get_auth_token(self, request):
return Response(text=get_csrf_token())
async def load_plugins(self):
# await self.wait_for_server()
logger.debug("Loading plugins")
self.plugin_loader.import_plugins()
# await inject_to_tab("SP", "window.syncDeckyPlugins();")
if self.settings.getSetting("pluginOrder", None) == None:
self.settings.setSetting("pluginOrder", list(self.plugin_loader.plugins.keys()))
logger.debug("Did not find pluginOrder setting, set it to default")
async def loader_reinjector(self):
while True:
tab = None
nf = False
dc = False
while not tab:
try:
tab = await get_gamepadui_tab()
except (client_exceptions.ClientConnectorError, client_exceptions.ServerDisconnectedError):
if not dc:
logger.debug("Couldn't connect to debugger, waiting...")
dc = True
pass
except ValueError:
if not nf:
logger.debug("Couldn't find GamepadUI tab, waiting...")
nf = True
pass
if not tab:
await sleep(5)
await tab.open_websocket()
await tab.enable()
await self.inject_javascript(tab, True)
try:
async for msg in tab.listen_for_message():
# this gets spammed a lot
if msg.get("method", None) != "Page.navigatedWithinDocument":
logger.debug("Page event: " + str(msg.get("method", None)))
if msg.get("method", None) == "Page.domContentEventFired":
if not await tab.has_global_var("deckyHasLoaded", False):
await self.inject_javascript(tab)
if msg.get("method", None) == "Inspector.detached":
logger.info("CEF has requested that we detach.")
await tab.close_websocket()
break
# If this is a forceful disconnect the loop will just stop without any failure message. In this case, injector.py will handle this for us so we don't need to close the socket.
# This is because of https://github.com/aio-libs/aiohttp/blob/3ee7091b40a1bc58a8d7846e7878a77640e96996/aiohttp/client_ws.py#L321
logger.info("CEF has disconnected...")
# At this point the loop starts again and we connect to the freshly started Steam client once it is ready.
except Exception as e:
logger.error("Exception while reading page events " + format_exc())
await tab.close_websocket()
pass
# while True:
# await sleep(5)
# if not await tab.has_global_var("deckyHasLoaded", False):
# logger.info("Plugin loader isn't present in Steam anymore, reinjecting...")
# await self.inject_javascript(tab)
async def inject_javascript(self, tab: Tab, first=False, request=None):
logger.info("Loading Decky frontend!")
try:
if first:
if await tab.has_global_var("deckyHasLoaded", False):
await close_old_tabs()
await tab.evaluate_js("try{if (window.deckyHasLoaded){setTimeout(() => location.reload(), 100)}else{window.deckyHasLoaded = true;(async()=>{try{while(!window.SP_REACT){await new Promise(r => setTimeout(r, 10))};await import('http://localhost:1337/frontend/index.js')}catch(e){console.error(e)};})();}}catch(e){console.error(e)}", False, False, False)
except:
logger.info("Failed to inject JavaScript into tab\n" + format_exc())
pass
def run(self):
return run_app(self.web_app, host=get_server_host(), port=get_server_port(), loop=self.loop, access_log=None)
# This file is needed to make the relative imports in src/ work properly.
if __name__ == "__main__":
if ON_WINDOWS:
# Fix windows/flask not recognising that .js means 'application/javascript'
import mimetypes
mimetypes.add_type('application/javascript', '.js')
# Required for multiprocessing support in frozen files
multiprocessing.freeze_support()
# Append the loader's plugin path to the recognized python paths
sys.path.append(path.join(path.dirname(__file__), "plugin"))
# Append the system and user python paths
sys.path.extend(get_system_pythonpaths())
loop = new_event_loop()
set_event_loop(loop)
PluginManager(loop).run()
from src.main import main
main()
+3
View File
@@ -0,0 +1,3 @@
{
"strict": ["*"]
}
+5
View File
@@ -0,0 +1,5 @@
aiohttp==3.8.4
aiohttp-jinja2==1.5.1
aiohttp_cors==0.7.0
watchdog==2.1.7
certifi==2023.7.22
+48 -26
View File
@@ -4,53 +4,70 @@ import json
# from pprint import pformat
# Partial imports
from aiohttp import ClientSession, web
from asyncio import get_event_loop, sleep
from concurrent.futures import ProcessPoolExecutor
from aiohttp import ClientSession
from asyncio import sleep
from hashlib import sha256
from io import BytesIO
from logging import getLogger
from os import R_OK, W_OK, path, rename, listdir, access, mkdir
from os import R_OK, W_OK, path, listdir, access, mkdir
from shutil import rmtree
from time import time
from zipfile import ZipFile
from localplatform import chown, chmod
from enum import IntEnum
from typing import Dict, List, TypedDict
# Local modules
from helpers import get_ssl_context, download_remote_binary_to_path
from injector import get_gamepadui_tab
from .localplatform import chown, chmod
from .loader import Loader, Plugins
from .helpers import get_ssl_context, download_remote_binary_to_path
from .settings import SettingsManager
from .injector import get_gamepadui_tab
logger = getLogger("Browser")
class PluginInstallType(IntEnum):
INSTALL = 0
REINSTALL = 1
UPDATE = 2
class PluginInstallRequest(TypedDict):
name: str
artifact: str
version: str
hash: str
install_type: PluginInstallType
class PluginInstallContext:
def __init__(self, artifact, name, version, hash) -> None:
def __init__(self, artifact: str, name: str, version: str, hash: str) -> None:
self.artifact = artifact
self.name = name
self.version = version
self.hash = hash
class PluginBrowser:
def __init__(self, plugin_path, plugins, loader, settings) -> None:
def __init__(self, plugin_path: str, plugins: Plugins, loader: Loader, settings: SettingsManager) -> None:
self.plugin_path = plugin_path
self.plugins = plugins
self.loader = loader
self.settings = settings
self.install_requests = {}
self.install_requests: Dict[str, PluginInstallContext | List[PluginInstallContext]] = {}
def _unzip_to_plugin_dir(self, zip, name, hash):
def _unzip_to_plugin_dir(self, zip: BytesIO, name: str, hash: str):
zip_hash = sha256(zip.getbuffer()).hexdigest()
if hash and (zip_hash != hash):
return False
zip_file = ZipFile(zip)
zip_file.extractall(self.plugin_path)
plugin_dir = path.join(self.plugin_path, self.find_plugin_folder(name))
plugin_folder = self.find_plugin_folder(name)
assert plugin_folder is not None
plugin_dir = path.join(self.plugin_path, plugin_folder)
if not chown(plugin_dir) or not chmod(plugin_dir, 555):
logger.error(f"chown/chmod exited with a non-zero exit code")
return False
return True
async def _download_remote_binaries_for_plugin_with_name(self, pluginBasePath):
async def _download_remote_binaries_for_plugin_with_name(self, pluginBasePath: str):
rv = False
try:
packageJsonPath = path.join(pluginBasePath, 'package.json')
@@ -91,7 +108,7 @@ class PluginBrowser:
return rv
"""Return the filename (only) for the specified plugin"""
def find_plugin_folder(self, name):
def find_plugin_folder(self, name: str) -> str | None:
for folder in listdir(self.plugin_path):
try:
with open(path.join(self.plugin_path, folder, 'plugin.json'), "r", encoding="utf-8") as f:
@@ -102,11 +119,13 @@ class PluginBrowser:
except:
logger.debug(f"skipping {folder}")
async def uninstall_plugin(self, name):
async def uninstall_plugin(self, name: str):
if self.loader.watcher:
self.loader.watcher.disabled = True
tab = await get_gamepadui_tab()
plugin_dir = path.join(self.plugin_path, self.find_plugin_folder(name))
plugin_folder = self.find_plugin_folder(name)
assert plugin_folder is not None
plugin_dir = path.join(self.plugin_path, plugin_folder)
try:
logger.info("uninstalling " + name)
logger.info(" at dir " + plugin_dir)
@@ -133,12 +152,14 @@ class PluginBrowser:
if self.loader.watcher:
self.loader.watcher.disabled = False
async def _install(self, artifact, name, version, hash):
async def _install(self, artifact: str, name: str, version: str, hash: str):
# Will be set later in code
res_zip = None
# Check if plugin is installed
isInstalled = False
# Preserve plugin order before removing plugin (uninstall alters the order and removes the plugin from the list)
current_plugin_order = self.settings.getSetting("pluginOrder")[:]
if self.loader.watcher:
self.loader.watcher.disabled = True
try:
@@ -183,6 +204,7 @@ class PluginBrowser:
ret = self._unzip_to_plugin_dir(res_zip, name, hash)
if ret:
plugin_folder = self.find_plugin_folder(name)
assert plugin_folder is not None
plugin_dir = path.join(self.plugin_path, plugin_folder)
ret = await self._download_remote_binaries_for_plugin_with_name(plugin_dir)
if ret:
@@ -191,27 +213,27 @@ class PluginBrowser:
self.loader.plugins[name].stop()
self.loader.plugins.pop(name, None)
await sleep(1)
current_plugin_order = self.settings.getSetting("pluginOrder")
current_plugin_order.append(name)
if not isInstalled:
current_plugin_order = self.settings.getSetting("pluginOrder")
current_plugin_order.append(name)
self.settings.setSetting("pluginOrder", current_plugin_order)
logger.debug("Plugin %s was added to the pluginOrder setting", name)
self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder)
else:
logger.fatal(f"Failed Downloading Remote Binaries")
else:
self.log.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
logger.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
if self.loader.watcher:
self.loader.watcher.disabled = False
async def request_plugin_install(self, artifact, name, version, hash, install_type):
async def request_plugin_install(self, artifact: str, name: str, version: str, hash: str, install_type: PluginInstallType):
request_id = str(time())
self.install_requests[request_id] = PluginInstallContext(artifact, name, version, hash)
tab = await get_gamepadui_tab()
await tab.open_websocket()
await tab.evaluate_js(f"DeckyPluginLoader.addPluginInstallPrompt('{name}', '{version}', '{request_id}', '{hash}', {install_type})")
async def request_multiple_plugin_installs(self, requests):
async def request_multiple_plugin_installs(self, requests: List[PluginInstallRequest]):
request_id = str(time())
self.install_requests[request_id] = [PluginInstallContext(req['artifact'], req['name'], req['version'], req['hash']) for req in requests]
js_requests_parameter = ','.join([
@@ -222,17 +244,17 @@ class PluginBrowser:
await tab.open_websocket()
await tab.evaluate_js(f"DeckyPluginLoader.addMultiplePluginsInstallPrompt('{request_id}', [{js_requests_parameter}])")
async def confirm_plugin_install(self, request_id):
async def confirm_plugin_install(self, request_id: str):
requestOrRequests = self.install_requests.pop(request_id)
if isinstance(requestOrRequests, list):
[await self._install(req.artifact, req.name, req.version, req.hash) for req in requestOrRequests]
else:
await self._install(requestOrRequests.artifact, requestOrRequests.name, requestOrRequests.version, requestOrRequests.hash)
def cancel_plugin_install(self, request_id):
def cancel_plugin_install(self, request_id: str):
self.install_requests.pop(request_id)
def cleanup_plugin_settings(self, name):
def cleanup_plugin_settings(self, name: str):
"""Removes any settings related to a plugin. Propably called when a plugin is uninstalled.
Args:
+25 -34
View File
@@ -2,16 +2,16 @@ import re
import ssl
import uuid
import os
import sys
import subprocess
from hashlib import sha256
from io import BytesIO
import certifi
from aiohttp.web import Response, middleware
from aiohttp.web import Request, Response, middleware
from aiohttp.typedefs import Handler
from aiohttp import ClientSession
import localplatform
from customtypes import UserType
from . import localplatform
from .customtypes import UserType
from logging import getLogger
REMOTE_DEBUGGER_UNIT = "steam-web-debug-portforward.service"
@@ -31,17 +31,17 @@ def get_csrf_token():
return csrf_token
@middleware
async def csrf_middleware(request, handler):
async def csrf_middleware(request: Request, handler: Handler):
if str(request.method) == "OPTIONS" or request.headers.get('Authentication') == csrf_token or str(request.rel_url) == "/auth/token" or str(request.rel_url).startswith("/plugins/load_main/") or str(request.rel_url).startswith("/static/") or str(request.rel_url).startswith("/legacy/") or str(request.rel_url).startswith("/steam_resource/") or str(request.rel_url).startswith("/frontend/") or assets_regex.match(str(request.rel_url)) or frontend_regex.match(str(request.rel_url)):
return await handler(request)
return Response(text='Forbidden', status='403')
return Response(text='Forbidden', status=403)
# Get the default homebrew path unless a home_path is specified. home_path argument is deprecated
def get_homebrew_path(home_path = None) -> str:
def get_homebrew_path() -> str:
return localplatform.get_unprivileged_path()
# Recursively create path and chown as user
def mkdir_as_user(path):
def mkdir_as_user(path: str):
path = os.path.realpath(path)
os.makedirs(path, exist_ok=True)
localplatform.chown(path)
@@ -57,23 +57,18 @@ def get_loader_version() -> str:
# returns the appropriate system python paths
def get_system_pythonpaths() -> list[str]:
extra_args = {}
if localplatform.ON_LINUX:
# run as normal normal user to also include user python paths
extra_args["user"] = localplatform.localplatform._get_user_id()
extra_args["env"] = {}
try:
# run as normal normal user if on linux to also include user python paths
proc = subprocess.run(["python3" if localplatform.ON_LINUX else "python", "-c", "import sys; print('\\n'.join(x for x in sys.path if x))"],
capture_output=True, **extra_args)
# TODO make this less insane
capture_output=True, user=localplatform.localplatform._get_user_id() if localplatform.ON_LINUX else None, env={} if localplatform.ON_LINUX else None) # type: ignore
return [x.strip() for x in proc.stdout.decode().strip().split("\n")]
except Exception as e:
logger.warn(f"Failed to execute get_system_pythonpaths(): {str(e)}")
return []
# Download Remote Binaries to local Plugin
async def download_remote_binary_to_path(url, binHash, path) -> bool:
async def download_remote_binary_to_path(url: str, binHash: str, path: str) -> bool:
rv = False
try:
if os.access(os.path.dirname(path), os.W_OK):
@@ -110,46 +105,42 @@ def set_user_group() -> str:
# Get the user id hosting the plugin loader
def get_user_id() -> int:
return localplatform.localplatform._get_user_id()
return localplatform.localplatform._get_user_id() # pyright: ignore [reportPrivateUsage]
# Get the user hosting the plugin loader
def get_user() -> str:
return localplatform.localplatform._get_user()
return localplatform.localplatform._get_user() # pyright: ignore [reportPrivateUsage]
# Get the effective user id of the running process
def get_effective_user_id() -> int:
return localplatform.localplatform._get_effective_user_id()
return localplatform.localplatform._get_effective_user_id() # pyright: ignore [reportPrivateUsage]
# Get the effective user of the running process
def get_effective_user() -> str:
return localplatform.localplatform._get_effective_user()
return localplatform.localplatform._get_effective_user() # pyright: ignore [reportPrivateUsage]
# Get the effective user group id of the running process
def get_effective_user_group_id() -> int:
return localplatform.localplatform._get_effective_user_group_id()
return localplatform.localplatform._get_effective_user_group_id() # pyright: ignore [reportPrivateUsage]
# Get the effective user group of the running process
def get_effective_user_group() -> str:
return localplatform.localplatform._get_effective_user_group()
return localplatform.localplatform._get_effective_user_group() # pyright: ignore [reportPrivateUsage]
# Get the user owner of the given file path.
def get_user_owner(file_path) -> str:
return localplatform.localplatform._get_user_owner(file_path)
def get_user_owner(file_path: str) -> str:
return localplatform.localplatform._get_user_owner(file_path) # pyright: ignore [reportPrivateUsage]
# Get the user group of the given file path.
def get_user_group(file_path) -> str:
return localplatform.localplatform._get_user_group(file_path)
# Get the user group of the given file path, or the user group hosting the plugin loader
def get_user_group(file_path: str | None = None) -> str:
return localplatform.localplatform._get_user_group(file_path) # pyright: ignore [reportPrivateUsage]
# Get the group id of the user hosting the plugin loader
def get_user_group_id() -> int:
return localplatform.localplatform._get_user_group_id()
# Get the group of the user hosting the plugin loader
def get_user_group() -> str:
return localplatform.localplatform._get_user_group()
return localplatform.localplatform._get_user_group_id() # pyright: ignore [reportPrivateUsage]
# Get the default home path unless a user is specified
def get_home_path(username = None) -> str:
def get_home_path(username: str | None = None) -> str:
return localplatform.get_home_path(UserType.ROOT if username == "root" else UserType.HOST_USER)
async def is_systemd_unit_active(unit_name: str) -> bool:
+50 -34
View File
@@ -2,10 +2,9 @@
from asyncio import sleep
from logging import getLogger
from traceback import format_exc
from typing import List
from typing import Any, Callable, List, TypedDict, Dict
from aiohttp import ClientSession, WSMsgType
from aiohttp import ClientSession
from aiohttp.client_exceptions import ClientConnectorError, ClientOSError
from asyncio.exceptions import TimeoutError
import uuid
@@ -14,35 +13,43 @@ BASE_ADDRESS = "http://localhost:8080"
logger = getLogger("Injector")
class _TabResponse(TypedDict):
title: str
id: str
url: str
webSocketDebuggerUrl: str
class Tab:
cmd_id = 0
def __init__(self, res) -> None:
self.title = res["title"]
self.id = res["id"]
self.url = res["url"]
self.ws_url = res["webSocketDebuggerUrl"]
def __init__(self, res: _TabResponse) -> None:
self.title: str = res["title"]
self.id: str = res["id"]
self.url: str = res["url"]
self.ws_url: str = res["webSocketDebuggerUrl"]
self.websocket = None
self.client = None
async def open_websocket(self):
self.client = ClientSession()
self.websocket = await self.client.ws_connect(self.ws_url)
self.websocket = await self.client.ws_connect(self.ws_url) # type: ignore
async def close_websocket(self):
await self.websocket.close()
await self.client.close()
if self.websocket:
await self.websocket.close()
if self.client:
await self.client.close()
async def listen_for_message(self):
async for message in self.websocket:
data = message.json()
yield data
logger.warn(f"The Tab {self.title} socket has been disconnected while listening for messages.")
await self.close_websocket()
if self.websocket:
async for message in self.websocket:
data = message.json()
yield data
logger.warn(f"The Tab {self.title} socket has been disconnected while listening for messages.")
await self.close_websocket()
async def _send_devtools_cmd(self, dc, receive=True):
async def _send_devtools_cmd(self, dc: Dict[str, Any], receive: bool = True):
if self.websocket:
self.cmd_id += 1
dc["id"] = self.cmd_id
@@ -54,7 +61,7 @@ class Tab:
return None
raise RuntimeError("Websocket not opened")
async def evaluate_js(self, js, run_async=False, manage_socket=True, get_result=True):
async def evaluate_js(self, js: str, run_async: bool | None = False, manage_socket: bool | None = True, get_result: bool = True):
try:
if manage_socket:
await self.open_websocket()
@@ -73,15 +80,16 @@ class Tab:
await self.close_websocket()
return res
async def has_global_var(self, var_name, manage_socket=True):
async def has_global_var(self, var_name: str, manage_socket: bool = True):
res = await self.evaluate_js(f"window['{var_name}'] !== null && window['{var_name}'] !== undefined", False, manage_socket)
assert res is not None
if not "result" in res or not "result" in res["result"] or not "value" in res["result"]["result"]:
return False
return res["result"]["result"]["value"]
async def close(self, manage_socket=True):
async def close(self, manage_socket: bool = True):
try:
if manage_socket:
await self.open_websocket()
@@ -111,7 +119,7 @@ class Tab:
"method": "Page.disable",
}, False)
async def refresh(self, manage_socket=True):
async def refresh(self, manage_socket: bool = True):
try:
if manage_socket:
await self.open_websocket()
@@ -125,7 +133,7 @@ class Tab:
await self.close_websocket()
return
async def reload_and_evaluate(self, js, manage_socket=True):
async def reload_and_evaluate(self, js: str, manage_socket: bool = True):
"""
Reloads the current tab, with JS to run on load via debugger
"""
@@ -153,11 +161,13 @@ class Tab:
}
}, True)
assert breakpoint_res is not None
logger.info(breakpoint_res)
# Page finishes loading when breakpoint hits
for x in range(20):
for _ in range(20):
# this works around 1/5 of the time, so just send it 8 times.
# the js accounts for being injected multiple times allowing only one instance to run at a time anyway
await self._send_devtools_cmd({
@@ -176,7 +186,7 @@ class Tab:
}
}, False)
for x in range(4):
for _ in range(4):
await self._send_devtools_cmd({
"method": "Debugger.resume"
}, False)
@@ -190,7 +200,7 @@ class Tab:
await self.close_websocket()
return
async def add_script_to_evaluate_on_new_document(self, js, add_dom_wrapper=True, manage_socket=True, get_result=True):
async def add_script_to_evaluate_on_new_document(self, js: str, add_dom_wrapper: bool = True, manage_socket: bool = True, get_result: bool = True):
"""
How the underlying call functions is not particularly clear from the devtools docs, so stealing puppeteer's description:
@@ -253,7 +263,7 @@ class Tab:
await self.close_websocket()
return res
async def remove_script_to_evaluate_on_new_document(self, script_id, manage_socket=True):
async def remove_script_to_evaluate_on_new_document(self, script_id: str, manage_socket: bool = True):
"""
Removes a script from a page that was added with `add_script_to_evaluate_on_new_document`
@@ -267,7 +277,7 @@ class Tab:
if manage_socket:
await self.open_websocket()
res = await self._send_devtools_cmd({
await self._send_devtools_cmd({
"method": "Page.removeScriptToEvaluateOnNewDocument",
"params": {
"identifier": script_id
@@ -278,15 +288,16 @@ class Tab:
if manage_socket:
await self.close_websocket()
async def has_element(self, element_name, manage_socket=True):
async def has_element(self, element_name: str, manage_socket: bool = True):
res = await self.evaluate_js(f"document.getElementById('{element_name}') != null", False, manage_socket)
assert res is not None
if not "result" in res or not "result" in res["result"] or not "value" in res["result"]["result"]:
return False
return res["result"]["result"]["value"]
async def inject_css(self, style, manage_socket=True):
async def inject_css(self, style: str, manage_socket: bool = True):
try:
css_id = str(uuid.uuid4())
@@ -300,6 +311,8 @@ class Tab:
}})()
""", False, manage_socket)
assert result is not None
if "exceptionDetails" in result["result"]:
return {
"success": False,
@@ -316,7 +329,7 @@ class Tab:
"result": e
}
async def remove_css(self, css_id, manage_socket=True):
async def remove_css(self, css_id: str, manage_socket: bool = True):
try:
result = await self.evaluate_js(
f"""
@@ -328,6 +341,8 @@ class Tab:
}})()
""", False, manage_socket)
assert result is not None
if "exceptionDetails" in result["result"]:
return {
"success": False,
@@ -343,8 +358,9 @@ class Tab:
"result": e
}
async def get_steam_resource(self, url):
async def get_steam_resource(self, url: str):
res = await self.evaluate_js(f'(async function test() {{ return await (await fetch("{url}")).text() }})()', True)
assert res is not None
return res["result"]["result"]["value"]
def __repr__(self):
@@ -380,14 +396,14 @@ async def get_tabs() -> List[Tab]:
raise Exception(f"/json did not return 200. {await res.text()}")
async def get_tab(tab_name) -> Tab:
async def get_tab(tab_name: str) -> Tab:
tabs = await get_tabs()
tab = next((i for i in tabs if i.title == tab_name), None)
if not tab:
raise ValueError(f"Tab {tab_name} not found")
return tab
async def get_tab_lambda(test) -> Tab:
async def get_tab_lambda(test: Callable[[Tab], bool]) -> Tab:
tabs = await get_tabs()
tab = next((i for i in tabs if test(i)), None)
if not tab:
@@ -408,7 +424,7 @@ async def get_gamepadui_tab() -> Tab:
raise ValueError(f"GamepadUI Tab not found")
return tab
async def inject_to_tab(tab_name, js, run_async=False):
async def inject_to_tab(tab_name: str, js: str, run_async: bool = False):
tab = await get_tab(tab_name)
return await tab.evaluate_js(js, run_async)
+43 -33
View File
@@ -1,34 +1,43 @@
from asyncio import Queue, sleep
from __future__ import annotations
from asyncio import AbstractEventLoop, Queue, sleep
from json.decoder import JSONDecodeError
from logging import getLogger
from os import listdir, path
from pathlib import Path
from traceback import print_exc
from typing import Any, Tuple
from aiohttp import web
from os.path import exists
from watchdog.events import RegexMatchingEventHandler
from watchdog.observers import Observer
from watchdog.events import RegexMatchingEventHandler, DirCreatedEvent, DirModifiedEvent, FileCreatedEvent, FileModifiedEvent # type: ignore
from watchdog.observers import Observer # type: ignore
from injector import get_tab, get_gamepadui_tab
from plugin import PluginWrapper
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .main import PluginManager
from .injector import get_tab, get_gamepadui_tab
from .plugin import PluginWrapper
Plugins = dict[str, PluginWrapper]
ReloadQueue = Queue[Tuple[str, str, bool | None] | Tuple[str, str]]
class FileChangeHandler(RegexMatchingEventHandler):
def __init__(self, queue, plugin_path) -> None:
super().__init__(regexes=[r'^.*?dist\/index\.js$', r'^.*?main\.py$'])
def __init__(self, queue: ReloadQueue, plugin_path: str) -> None:
super().__init__(regexes=[r'^.*?dist\/index\.js$', r'^.*?main\.py$']) # type: ignore
self.logger = getLogger("file-watcher")
self.plugin_path = plugin_path
self.queue = queue
self.disabled = True
def maybe_reload(self, src_path):
def maybe_reload(self, src_path: str):
if self.disabled:
return
plugin_dir = Path(path.relpath(src_path, self.plugin_path)).parts[0]
if exists(path.join(self.plugin_path, plugin_dir, "plugin.json")):
self.queue.put_nowait((path.join(self.plugin_path, plugin_dir, "main.py"), plugin_dir, True))
def on_created(self, event):
def on_created(self, event: DirCreatedEvent | FileCreatedEvent):
src_path = event.src_path
if "__pycache__" in src_path:
return
@@ -42,7 +51,7 @@ class FileChangeHandler(RegexMatchingEventHandler):
self.logger.debug(f"file created: {src_path}")
self.maybe_reload(src_path)
def on_modified(self, event):
def on_modified(self, event: DirModifiedEvent | FileModifiedEvent):
src_path = event.src_path
if "__pycache__" in src_path:
return
@@ -57,25 +66,25 @@ class FileChangeHandler(RegexMatchingEventHandler):
self.maybe_reload(src_path)
class Loader:
def __init__(self, server_instance, plugin_path, loop, live_reload=False) -> None:
def __init__(self, server_instance: PluginManager, plugin_path: str, loop: AbstractEventLoop, live_reload: bool = False) -> None:
self.loop = loop
self.logger = getLogger("Loader")
self.plugin_path = plugin_path
self.logger.info(f"plugin_path: {self.plugin_path}")
self.plugins : dict[str, PluginWrapper] = {}
self.plugins: Plugins = {}
self.watcher = None
self.live_reload = live_reload
self.reload_queue = Queue()
self.reload_queue: ReloadQueue = Queue()
self.loop.create_task(self.handle_reloads())
if live_reload:
self.observer = Observer()
self.watcher = FileChangeHandler(self.reload_queue, plugin_path)
self.observer.schedule(self.watcher, self.plugin_path, recursive=True)
self.observer.schedule(self.watcher, self.plugin_path, recursive=True) # type: ignore
self.observer.start()
self.loop.create_task(self.enable_reload_wait())
server_instance.add_routes([
server_instance.web_app.add_routes([
web.get("/frontend/{path:.*}", self.handle_frontend_assets),
web.get("/locales/{path:.*}", self.handle_frontend_locales),
web.get("/plugins", self.get_plugins),
@@ -93,40 +102,41 @@ class Loader:
async def enable_reload_wait(self):
if self.live_reload:
await sleep(10)
self.logger.info("Hot reload enabled")
self.watcher.disabled = False
if self.watcher:
self.logger.info("Hot reload enabled")
self.watcher.disabled = False
async def handle_frontend_assets(self, request):
file = path.join(path.dirname(__file__), "static", request.match_info["path"])
async def handle_frontend_assets(self, request: web.Request):
file = path.join(path.dirname(__file__), "..", "static", request.match_info["path"])
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
async def handle_frontend_locales(self, request):
async def handle_frontend_locales(self, request: web.Request):
req_lang = request.match_info["path"]
file = path.join(path.dirname(__file__), "locales", req_lang)
file = path.join(path.dirname(__file__), "..", "locales", req_lang)
if exists(file):
return web.FileResponse(file, headers={"Cache-Control": "no-cache", "Content-Type": "application/json"})
else:
self.logger.info(f"Language {req_lang} not available, returning an empty dictionary")
return web.json_response(data={}, headers={"Cache-Control": "no-cache"})
async def get_plugins(self, request):
async def get_plugins(self, request: web.Request):
plugins = list(self.plugins.values())
return web.json_response([{"name": str(i) if not i.legacy else "$LEGACY_"+str(i), "version": i.version} for i in plugins])
def handle_plugin_frontend_assets(self, request):
async def handle_plugin_frontend_assets(self, request: web.Request):
plugin = self.plugins[request.match_info["plugin_name"]]
file = path.join(self.plugin_path, plugin.plugin_directory, "dist/assets", request.match_info["path"])
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
def handle_frontend_bundle(self, request):
async def handle_frontend_bundle(self, request: web.Request):
plugin = self.plugins[request.match_info["plugin_name"]]
with open(path.join(self.plugin_path, plugin.plugin_directory, "dist/index.js"), "r", encoding="utf-8") as bundle:
return web.Response(text=bundle.read(), content_type="application/javascript")
def import_plugin(self, file, plugin_directory, refresh=False, batch=False):
def import_plugin(self, file: str, plugin_directory: str, refresh: bool | None = False, batch: bool | None = False):
try:
plugin = PluginWrapper(file, plugin_directory, self.plugin_path)
if plugin.name in self.plugins:
@@ -146,7 +156,7 @@ class Loader:
self.logger.error(f"Could not load {file}. {e}")
print_exc()
async def dispatch_plugin(self, name, version):
async def dispatch_plugin(self, name: str, version: str | None):
gpui_tab = await get_gamepadui_tab()
await gpui_tab.evaluate_js(f"window.importDeckyPlugin('{name}', '{version}')")
@@ -161,15 +171,15 @@ class Loader:
async def handle_reloads(self):
while True:
args = await self.reload_queue.get()
self.import_plugin(*args)
self.import_plugin(*args) # type: ignore
async def handle_plugin_method_call(self, request):
async def handle_plugin_method_call(self, request: web.Request):
res = {}
plugin = self.plugins[request.match_info["plugin_name"]]
method_name = request.match_info["method_name"]
try:
method_info = await request.json()
args = method_info["args"]
args: Any = method_info["args"]
except JSONDecodeError:
args = {}
try:
@@ -189,7 +199,7 @@ class Loader:
can introduce it more smoothly and give people the chance to sample the new features even
without plugin support. They will be removed once legacy plugins are no longer relevant.
"""
async def load_plugin_main_view(self, request):
async def load_plugin_main_view(self, request: web.Request):
plugin = self.plugins[request.match_info["name"]]
with open(path.join(self.plugin_path, plugin.plugin_directory, plugin.main_view_html), "r", encoding="utf-8") as template:
template_data = template.read()
@@ -201,7 +211,7 @@ class Loader:
"""
return web.Response(text=ret, content_type="text/html")
async def handle_sub_route(self, request):
async def handle_sub_route(self, request: web.Request):
plugin = self.plugins[request.match_info["name"]]
route_path = request.match_info["path"]
self.logger.info(path)
@@ -212,14 +222,14 @@ class Loader:
return web.Response(text=ret)
async def get_steam_resource(self, request):
async def get_steam_resource(self, request: web.Request):
tab = await get_tab("SP")
try:
return web.Response(text=await tab.get_steam_resource(f"https://steamloopback.host/{request.match_info['path']}"), content_type="text/html")
except Exception as e:
return web.Response(text=str(e), status=400)
async def handle_backend_reload_request(self, request):
async def handle_backend_reload_request(self, request: web.Request):
plugin_name : str = request.match_info["plugin_name"]
plugin = self.plugins[plugin_name]
@@ -4,11 +4,11 @@ ON_WINDOWS = platform.system() == "Windows"
ON_LINUX = not ON_WINDOWS
if ON_WINDOWS:
from localplatformwin import *
import localplatformwin as localplatform
from .localplatformwin import *
from . import localplatformwin as localplatform
else:
from localplatformlinux import *
import localplatformlinux as localplatform
from .localplatformlinux import *
from . import localplatformlinux as localplatform
def get_privileged_path() -> str:
'''Get path accessible by elevated user. Holds plugins, decky loader and decky loader configs'''
@@ -40,4 +40,13 @@ def get_keep_systemd_service() -> bool:
def get_log_level() -> int:
return {"CRITICAL": 50, "ERROR": 40, "WARNING": 30, "INFO": 20, "DEBUG": 10}[
os.getenv("LOG_LEVEL", "INFO")
]
]
def get_selinux() -> bool:
if ON_LINUX:
from subprocess import check_output
try:
if (check_output("getenforce").decode("ascii").strip("\n") == "Enforcing"): return True
except FileNotFoundError:
pass
return False
@@ -1,6 +1,6 @@
import os, pwd, grp, sys, logging
from subprocess import call, run, DEVNULL, PIPE, STDOUT
from customtypes import UserType
from .customtypes import UserType
logger = logging.getLogger("localplatform")
@@ -29,21 +29,17 @@ def _get_effective_user_group() -> str:
return grp.getgrgid(_get_effective_user_group_id()).gr_name
# Get the user owner of the given file path.
def _get_user_owner(file_path) -> str:
def _get_user_owner(file_path: str) -> str:
return pwd.getpwuid(os.stat(file_path).st_uid).pw_name
# Get the user group of the given file path.
def _get_user_group(file_path) -> str:
return grp.getgrgid(os.stat(file_path).st_gid).gr_name
# Get the user group of the given file path, or the user group hosting the plugin loader
def _get_user_group(file_path: str | None = None) -> str:
return grp.getgrgid(os.stat(file_path).st_gid if file_path is not None else _get_user_group_id()).gr_name
# Get the group id of the user hosting the plugin loader
def _get_user_group_id() -> int:
return pwd.getpwuid(_get_user_id()).pw_gid
# Get the group of the user hosting the plugin loader
def _get_user_group() -> str:
return grp.getgrgid(_get_user_group_id()).gr_name
def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool = True) -> bool:
user_str = ""
@@ -60,6 +56,8 @@ def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool =
return result == 0
def chmod(path : str, permissions : int, recursive : bool = True) -> bool:
if _get_effective_user_id() != 0:
return True
result = call(["chmod", "-R", str(permissions), path] if recursive else ["chmod", str(permissions), path])
return result == 0
@@ -144,7 +142,7 @@ def get_privileged_path() -> str:
return path
def _parent_dir(path : str) -> str:
def _parent_dir(path : str | None) -> str | None:
if path == None:
return None
@@ -164,7 +162,7 @@ def get_unprivileged_path() -> str:
# Expected path of loader binary is /home/deck/homebrew/service/PluginLoader
path = _parent_dir(_parent_dir(os.path.realpath(sys.argv[0])))
if not os.path.exists(path):
if path != None and not os.path.exists(path):
path = None
if path == None:
@@ -191,4 +189,4 @@ def get_unprivileged_user() -> str:
logger.warn("Unprivileged user is not properly configured. Defaulting to 'deck'")
user = 'deck'
return user
return user
@@ -1,4 +1,4 @@
from customtypes import UserType
from .customtypes import UserType
import os, sys
def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool = True) -> bool:
@@ -1,10 +1,13 @@
import asyncio, time, random
from localplatform import ON_WINDOWS
import asyncio, time
from typing import Awaitable, Callable
import random
from .localplatform import ON_WINDOWS
BUFFER_LIMIT = 2 ** 20 # 1 MiB
class UnixSocket:
def __init__(self, on_new_message):
def __init__(self, on_new_message: Callable[[str], Awaitable[str|None]]):
'''
on_new_message takes 1 string argument.
It's return value gets used, if not None, to write data to the socket.
@@ -46,28 +49,32 @@ class UnixSocket:
self.reader = None
async def read_single_line(self) -> str|None:
reader, writer = await self.get_socket_connection()
reader, _ = await self.get_socket_connection()
if self.reader == None:
return None
try:
assert reader
except AssertionError:
return
return await self._read_single_line(reader)
async def write_single_line(self, message : str):
reader, writer = await self.get_socket_connection()
_, writer = await self.get_socket_connection()
if self.writer == None:
return;
try:
assert writer
except AssertionError:
return
await self._write_single_line(writer, message)
async def _read_single_line(self, reader) -> str:
async def _read_single_line(self, reader: asyncio.StreamReader) -> str:
line = bytearray()
while True:
try:
line.extend(await reader.readuntil())
except asyncio.LimitOverrunError:
line.extend(await reader.read(reader._limit))
line.extend(await reader.read(reader._limit)) # type: ignore
continue
except asyncio.IncompleteReadError as err:
line.extend(err.partial)
@@ -77,27 +84,27 @@ class UnixSocket:
return line.decode("utf-8")
async def _write_single_line(self, writer, message : str):
async def _write_single_line(self, writer: asyncio.StreamWriter, message : str):
if not message.endswith("\n"):
message += "\n"
writer.write(message.encode("utf-8"))
await writer.drain()
async def _listen_for_method_call(self, reader, writer):
async def _listen_for_method_call(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
while True:
line = await self._read_single_line(reader)
try:
res = await self.on_new_message(line)
except Exception as e:
except Exception:
return
if res != None:
await self._write_single_line(writer, res)
class PortSocket (UnixSocket):
def __init__(self, on_new_message):
def __init__(self, on_new_message: Callable[[str], Awaitable[str|None]]):
'''
on_new_message takes 1 string argument.
It's return value gets used, if not None, to write data to the socket.
@@ -125,7 +132,7 @@ class PortSocket (UnixSocket):
return True
if ON_WINDOWS:
class LocalSocket (PortSocket):
class LocalSocket (PortSocket): # type: ignore
pass
else:
class LocalSocket (UnixSocket):
+192
View File
@@ -0,0 +1,192 @@
# Change PyInstaller files permissions
import sys
from typing import Dict
from .localplatform import (chmod, chown, service_stop, service_start,
ON_WINDOWS, get_log_level, get_live_reload,
get_server_port, get_server_host, get_chown_plugin_path,
get_privileged_path)
if hasattr(sys, '_MEIPASS'):
chmod(sys._MEIPASS, 755) # type: ignore
# Full imports
from asyncio import AbstractEventLoop, new_event_loop, set_event_loop, sleep
from logging import basicConfig, getLogger
from os import path
from traceback import format_exc
import multiprocessing
import aiohttp_cors # type: ignore
# Partial imports
from aiohttp import client_exceptions
from aiohttp.web import Application, Response, Request, get, run_app, static # type: ignore
from aiohttp_jinja2 import setup as jinja_setup
# local modules
from .browser import PluginBrowser
from .helpers import (REMOTE_DEBUGGER_UNIT, csrf_middleware, get_csrf_token,
mkdir_as_user, get_system_pythonpaths, get_effective_user_id)
from .injector import get_gamepadui_tab, Tab, close_old_tabs
from .loader import Loader
from .settings import SettingsManager
from .updater import Updater
from .utilities import Utilities
from .customtypes import UserType
basicConfig(
level=get_log_level(),
format="[%(module)s][%(levelname)s]: %(message)s"
)
logger = getLogger("Main")
plugin_path = path.join(get_privileged_path(), "plugins")
def chown_plugin_dir():
if not path.exists(plugin_path): # For safety, create the folder before attempting to do anything with it
mkdir_as_user(plugin_path)
if not chown(plugin_path, UserType.HOST_USER) or not chmod(plugin_path, 555):
logger.error(f"chown/chmod exited with a non-zero exit code")
if get_chown_plugin_path() == True:
chown_plugin_dir()
class PluginManager:
def __init__(self, loop: AbstractEventLoop) -> None:
self.loop = loop
self.web_app = Application()
self.web_app.middlewares.append(csrf_middleware)
self.cors = aiohttp_cors.setup(self.web_app, defaults={
"https://steamloopback.host": aiohttp_cors.ResourceOptions(
expose_headers="*",
allow_headers="*",
allow_credentials=True
)
})
self.plugin_loader = Loader(self, plugin_path, self.loop, get_live_reload())
self.settings = SettingsManager("loader", path.join(get_privileged_path(), "settings"))
self.plugin_browser = PluginBrowser(plugin_path, self.plugin_loader.plugins, self.plugin_loader, self.settings)
self.utilities = Utilities(self)
self.updater = Updater(self)
jinja_setup(self.web_app)
async def startup(_: Application):
if self.settings.getSetting("cef_forward", False):
self.loop.create_task(service_start(REMOTE_DEBUGGER_UNIT))
else:
self.loop.create_task(service_stop(REMOTE_DEBUGGER_UNIT))
self.loop.create_task(self.loader_reinjector())
self.loop.create_task(self.load_plugins())
self.web_app.on_startup.append(startup)
self.loop.set_exception_handler(self.exception_handler)
self.web_app.add_routes([get("/auth/token", self.get_auth_token)])
for route in list(self.web_app.router.routes()):
self.cors.add(route) # type: ignore
self.web_app.add_routes([static("/static", path.join(path.dirname(__file__), '..', 'static'))])
self.web_app.add_routes([static("/legacy", path.join(path.dirname(__file__), 'legacy'))])
def exception_handler(self, loop: AbstractEventLoop, context: Dict[str, str]):
if context["message"] == "Unclosed connection":
return
loop.default_exception_handler(context)
async def get_auth_token(self, request: Request):
return Response(text=get_csrf_token())
async def load_plugins(self):
# await self.wait_for_server()
logger.debug("Loading plugins")
self.plugin_loader.import_plugins()
# await inject_to_tab("SP", "window.syncDeckyPlugins();")
if self.settings.getSetting("pluginOrder", None) == None:
self.settings.setSetting("pluginOrder", list(self.plugin_loader.plugins.keys()))
logger.debug("Did not find pluginOrder setting, set it to default")
async def loader_reinjector(self):
while True:
tab = None
nf = False
dc = False
while not tab:
try:
tab = await get_gamepadui_tab()
except (client_exceptions.ClientConnectorError, client_exceptions.ServerDisconnectedError):
if not dc:
logger.debug("Couldn't connect to debugger, waiting...")
dc = True
pass
except ValueError:
if not nf:
logger.debug("Couldn't find GamepadUI tab, waiting...")
nf = True
pass
if not tab:
await sleep(5)
await tab.open_websocket()
await tab.enable()
await self.inject_javascript(tab, True)
try:
async for msg in tab.listen_for_message():
# this gets spammed a lot
if msg.get("method", None) != "Page.navigatedWithinDocument":
logger.debug("Page event: " + str(msg.get("method", None)))
if msg.get("method", None) == "Page.domContentEventFired":
if not await tab.has_global_var("deckyHasLoaded", False):
await self.inject_javascript(tab)
if msg.get("method", None) == "Inspector.detached":
logger.info("CEF has requested that we detach.")
await tab.close_websocket()
break
# If this is a forceful disconnect the loop will just stop without any failure message. In this case, injector.py will handle this for us so we don't need to close the socket.
# This is because of https://github.com/aio-libs/aiohttp/blob/3ee7091b40a1bc58a8d7846e7878a77640e96996/aiohttp/client_ws.py#L321
logger.info("CEF has disconnected...")
# At this point the loop starts again and we connect to the freshly started Steam client once it is ready.
except Exception:
logger.error("Exception while reading page events " + format_exc())
await tab.close_websocket()
pass
# while True:
# await sleep(5)
# if not await tab.has_global_var("deckyHasLoaded", False):
# logger.info("Plugin loader isn't present in Steam anymore, reinjecting...")
# await self.inject_javascript(tab)
async def inject_javascript(self, tab: Tab, first: bool=False, request: Request|None=None):
logger.info("Loading Decky frontend!")
try:
if first:
if await tab.has_global_var("deckyHasLoaded", False):
await close_old_tabs()
await tab.evaluate_js("try{if (window.deckyHasLoaded){setTimeout(() => location.reload(), 100)}else{window.deckyHasLoaded = true;(async()=>{try{while(!window.SP_REACT){await new Promise(r => setTimeout(r, 10))};await import('http://localhost:1337/frontend/index.js')}catch(e){console.error(e)};})();}}catch(e){console.error(e)}", False, False, False)
except:
logger.info("Failed to inject JavaScript into tab\n" + format_exc())
pass
def run(self):
return run_app(self.web_app, host=get_server_host(), port=get_server_port(), loop=self.loop, access_log=None)
def main():
if ON_WINDOWS:
# Fix windows/flask not recognising that .js means 'application/javascript'
import mimetypes
mimetypes.add_type('application/javascript', '.js')
# Required for multiprocessing support in frozen files
multiprocessing.freeze_support()
else:
if get_effective_user_id() != 0:
logger.warning(f"decky is running as an unprivileged user, this is not officially supported and may cause issues")
# Append the loader's plugin path to the recognized python paths
sys.path.append(path.join(path.dirname(__file__), "plugin"))
# Append the system and user python paths
sys.path.extend(get_system_pythonpaths())
loop = new_event_loop()
set_event_loop(loop)
PluginManager(loop).run()
+20 -13
View File
@@ -1,7 +1,6 @@
import multiprocessing
from asyncio import (Lock, get_event_loop, new_event_loop,
set_event_loop, sleep)
from concurrent.futures import ProcessPoolExecutor
from importlib.util import module_from_spec, spec_from_file_location
from json import dumps, load, loads
from logging import getLogger
@@ -9,19 +8,19 @@ from traceback import format_exc
from os import path, environ
from signal import SIGINT, signal
from sys import exit, path as syspath
from time import time
from localsocket import LocalSocket
from localplatform import setgid, setuid, get_username, get_home_path
from customtypes import UserType
import helpers
from typing import Any, Dict
from .localsocket import LocalSocket
from .localplatform import setgid, setuid, get_username, get_home_path
from .customtypes import UserType
from . import helpers
class PluginWrapper:
def __init__(self, file, plugin_directory, plugin_path) -> None:
def __init__(self, file: str, plugin_directory: str, plugin_path: str) -> None:
self.file = file
self.plugin_path = plugin_path
self.plugin_directory = plugin_directory
self.method_call_lock = Lock()
self.socket = LocalSocket(self._on_new_message)
self.socket: LocalSocket = LocalSocket(self._on_new_message)
self.version = None
@@ -63,21 +62,27 @@ class PluginWrapper:
environ["DECKY_USER_HOME"] = helpers.get_home_path()
environ["DECKY_HOME"] = helpers.get_homebrew_path()
environ["DECKY_PLUGIN_SETTINGS_DIR"] = path.join(environ["DECKY_HOME"], "settings", self.plugin_directory)
helpers.mkdir_as_user(path.join(environ["DECKY_HOME"], "settings"))
helpers.mkdir_as_user(environ["DECKY_PLUGIN_SETTINGS_DIR"])
environ["DECKY_PLUGIN_RUNTIME_DIR"] = path.join(environ["DECKY_HOME"], "data", self.plugin_directory)
helpers.mkdir_as_user(path.join(environ["DECKY_HOME"], "data"))
helpers.mkdir_as_user(environ["DECKY_PLUGIN_RUNTIME_DIR"])
environ["DECKY_PLUGIN_LOG_DIR"] = path.join(environ["DECKY_HOME"], "logs", self.plugin_directory)
helpers.mkdir_as_user(path.join(environ["DECKY_HOME"], "logs"))
helpers.mkdir_as_user(environ["DECKY_PLUGIN_LOG_DIR"])
environ["DECKY_PLUGIN_DIR"] = path.join(self.plugin_path, self.plugin_directory)
environ["DECKY_PLUGIN_NAME"] = self.name
environ["DECKY_PLUGIN_VERSION"] = self.version
if self.version:
environ["DECKY_PLUGIN_VERSION"] = self.version
environ["DECKY_PLUGIN_AUTHOR"] = self.author
# append the plugin's `py_modules` to the recognized python paths
syspath.append(path.join(environ["DECKY_PLUGIN_DIR"], "py_modules"))
spec = spec_from_file_location("_", self.file)
assert spec is not None
module = module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
self.Plugin = module.Plugin
@@ -115,7 +120,8 @@ class PluginWrapper:
get_event_loop().close()
raise Exception("Closing message listener")
d = {"res": None, "success": True}
# TODO there is definitely a better way to type this
d: Dict[str, Any] = {"res": None, "success": True}
try:
d["res"] = await getattr(self.Plugin, data["method"])(self.Plugin, **data["args"])
except Exception as e:
@@ -134,17 +140,18 @@ class PluginWrapper:
if self.passive:
return
async def _(self):
async def _(self: PluginWrapper):
await self.socket.write_single_line(dumps({ "stop": True }, ensure_ascii=False))
await self.socket.close_socket_connection()
get_event_loop().create_task(_(self))
async def execute_method(self, method_name, kwargs):
async def execute_method(self, method_name: str, kwargs: Dict[Any, Any]):
if self.passive:
raise RuntimeError("This plugin is passive (aka does not implement main.py)")
async with self.method_call_lock:
reader, writer = await self.socket.get_socket_connection()
# reader, writer =
await self.socket.get_socket_connection()
await self.socket.write_single_line(dumps({ "method": method_name, "args": kwargs }, ensure_ascii=False))
@@ -1,13 +1,14 @@
from json import dump, load
from os import mkdir, path, listdir, rename
from localplatform import chown, folder_owner, get_chown_plugin_path
from customtypes import UserType
from typing import Any, Dict
from .localplatform import chown, folder_owner, get_chown_plugin_path
from .customtypes import UserType
from helpers import get_homebrew_path
from .helpers import get_homebrew_path
class SettingsManager:
def __init__(self, name, settings_directory = None) -> None:
def __init__(self, name: str, settings_directory: str | None = None) -> None:
wrong_dir = get_homebrew_path()
if settings_directory == None:
settings_directory = path.join(wrong_dir, "settings")
@@ -31,11 +32,11 @@ class SettingsManager:
if folder_owner(settings_directory) != expected_user:
chown(settings_directory, expected_user, False)
self.settings = {}
self.settings: Dict[str, Any] = {}
try:
open(self.path, "x", encoding="utf-8")
except FileExistsError as e:
except FileExistsError as _:
self.read()
pass
@@ -51,9 +52,9 @@ class SettingsManager:
with open(self.path, "w+", encoding="utf-8") as file:
dump(self.settings, file, indent=4, ensure_ascii=False)
def getSetting(self, key, default=None):
def getSetting(self, key: str, default: Any = None) -> Any:
return self.settings.get(key, default)
def setSetting(self, key, value):
def setSetting(self, key: str, value: Any) -> Any:
self.settings[key] = value
self.commit()
+32 -12
View File
@@ -1,23 +1,33 @@
from __future__ import annotations
import os
import shutil
import uuid
from asyncio import sleep
from ensurepip import version
from json.decoder import JSONDecodeError
from logging import getLogger
from os import getcwd, path, remove
from localplatform import chmod, service_restart, ON_LINUX, get_keep_systemd_service
from typing import TYPE_CHECKING, List, TypedDict
if TYPE_CHECKING:
from .main import PluginManager
from .localplatform import chmod, service_restart, ON_LINUX, get_keep_systemd_service, get_selinux
from aiohttp import ClientSession, web
import helpers
from injector import get_gamepadui_tab, inject_to_tab
from settings import SettingsManager
from . import helpers
from .injector import get_gamepadui_tab
from .settings import SettingsManager
logger = getLogger("Updater")
class RemoteVerAsset(TypedDict):
name: str
browser_download_url: str
class RemoteVer(TypedDict):
tag_name: str
prerelease: bool
assets: List[RemoteVerAsset]
class Updater:
def __init__(self, context) -> None:
def __init__(self, context: PluginManager) -> None:
self.context = context
self.settings = self.context.settings
# Exposes updater methods to frontend
@@ -28,8 +38,8 @@ class Updater:
"do_restart": self.do_restart,
"check_for_updates": self.check_for_updates
}
self.remoteVer = None
self.allRemoteVers = None
self.remoteVer: RemoteVer | None = None
self.allRemoteVers: List[RemoteVer] = []
self.localVer = helpers.get_loader_version()
try:
@@ -44,7 +54,7 @@ class Updater:
])
context.loop.create_task(self.version_reloader())
async def _handle_server_method_call(self, request):
async def _handle_server_method_call(self, request: web.Request):
method_name = request.match_info["method_name"]
try:
args = await request.json()
@@ -52,7 +62,7 @@ class Updater:
args = {}
res = {}
try:
r = await self.updater_methods[method_name](**args)
r = await self.updater_methods[method_name](**args) # type: ignore
res["result"] = r
res["success"] = True
except Exception as e:
@@ -105,7 +115,7 @@ class Updater:
selectedBranch = self.get_branch(self.context.settings)
async with ClientSession() as web:
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases", ssl=helpers.get_ssl_context()) as res:
remoteVersions = await res.json()
remoteVersions: List[RemoteVer] = await res.json()
if selectedBranch == 0:
logger.debug("release type: release")
remoteVersions = list(filter(lambda ver: ver["tag_name"].startswith("v") and not ver["prerelease"] and not ver["tag_name"].find("-pre") > 0 and ver["tag_name"], remoteVersions))
@@ -142,6 +152,12 @@ class Updater:
async def do_update(self):
logger.debug("Starting update.")
try:
assert self.remoteVer
except AssertionError:
logger.error("Unable to update as remoteVer is missing")
return
version = self.remoteVer["tag_name"]
download_url = None
download_filename = "PluginLoader" if ON_LINUX else "PluginLoader.exe"
@@ -208,6 +224,10 @@ class Updater:
remove(path.join(getcwd(), download_filename))
shutil.move(path.join(getcwd(), download_temp_filename), path.join(getcwd(), download_filename))
chmod(path.join(getcwd(), download_filename), 777, False)
if get_selinux():
from asyncio.subprocess import create_subprocess_exec
process = await create_subprocess_exec("chcon", "-t", "bin_t", path.join(getcwd(), download_filename))
logger.info(f"Setting the executable flag with chcon returned {await process.wait()}")
logger.info("Updated loader installation.")
await tab.evaluate_js("window.DeckyUpdater.finish()", False, False)
@@ -1,26 +1,36 @@
from __future__ import annotations
from os import stat_result
import uuid
import os
from json.decoder import JSONDecodeError
from os.path import splitext
import re
from traceback import format_exc
from stat import FILE_ATTRIBUTE_HIDDEN
from stat import FILE_ATTRIBUTE_HIDDEN # type: ignore
from asyncio import sleep, start_server, gather, open_connection
from asyncio import StreamReader, StreamWriter, start_server, gather, open_connection
from aiohttp import ClientSession, web
from typing import TYPE_CHECKING, Callable, Coroutine, Dict, Any, List, TypedDict
from logging import getLogger
from injector import inject_to_tab, get_gamepadui_tab, close_old_tabs, get_tab
from pathlib import Path
from localplatform import ON_WINDOWS
import helpers
import subprocess
from localplatform import service_stop, service_start, get_home_path, get_username
from .browser import PluginInstallRequest, PluginInstallType
if TYPE_CHECKING:
from .main import PluginManager
from .injector import inject_to_tab, get_gamepadui_tab, close_old_tabs, get_tab
from .localplatform import ON_WINDOWS
from . import helpers
from .localplatform import service_stop, service_start, get_home_path, get_username
class FilePickerObj(TypedDict):
file: Path
filest: stat_result
is_dir: bool
class Utilities:
def __init__(self, context) -> None:
def __init__(self, context: PluginManager) -> None:
self.context = context
self.util_methods = {
self.util_methods: Dict[str, Callable[..., Coroutine[Any, Any, Any]]] = {
"ping": self.ping,
"http_request": self.http_request,
"install_plugin": self.install_plugin,
@@ -53,7 +63,7 @@ class Utilities:
web.post("/methods/{method_name}", self._handle_server_method_call)
])
async def _handle_server_method_call(self, request):
async def _handle_server_method_call(self, request: web.Request):
method_name = request.match_info["method_name"]
try:
args = await request.json()
@@ -69,7 +79,7 @@ class Utilities:
res["success"] = False
return web.json_response(res)
async def install_plugin(self, artifact="", name="No name", version="dev", hash=False, install_type=0):
async def install_plugin(self, artifact: str="", name: str="No name", version: str="dev", hash: str="", install_type: PluginInstallType=PluginInstallType.INSTALL):
return await self.context.plugin_browser.request_plugin_install(
artifact=artifact,
name=name,
@@ -78,21 +88,21 @@ class Utilities:
install_type=install_type
)
async def install_plugins(self, requests):
async def install_plugins(self, requests: List[PluginInstallRequest]):
return await self.context.plugin_browser.request_multiple_plugin_installs(
requests=requests
)
async def confirm_plugin_install(self, request_id):
async def confirm_plugin_install(self, request_id: str):
return await self.context.plugin_browser.confirm_plugin_install(request_id)
def cancel_plugin_install(self, request_id):
async def cancel_plugin_install(self, request_id: str):
return self.context.plugin_browser.cancel_plugin_install(request_id)
async def uninstall_plugin(self, name):
async def uninstall_plugin(self, name: str):
return await self.context.plugin_browser.uninstall_plugin(name)
async def http_request(self, method="", url="", **kwargs):
async def http_request(self, method: str="", url: str="", **kwargs: Any):
async with ClientSession() as web:
res = await web.request(method, url, ssl=helpers.get_ssl_context(), **kwargs)
text = await res.text()
@@ -102,12 +112,13 @@ class Utilities:
"body": text
}
async def ping(self, **kwargs):
async def ping(self, **kwargs: Any):
return "pong"
async def execute_in_tab(self, tab, run_async, code):
async def execute_in_tab(self, tab: str, run_async: bool, code: str):
try:
result = await inject_to_tab(tab, code, run_async)
assert result
if "exceptionDetails" in result["result"]:
return {
"success": False,
@@ -124,7 +135,7 @@ class Utilities:
"result": e
}
async def inject_css_into_tab(self, tab, style):
async def inject_css_into_tab(self, tab: str, style: str):
try:
css_id = str(uuid.uuid4())
@@ -138,7 +149,7 @@ class Utilities:
}})()
""", False)
if "exceptionDetails" in result["result"]:
if result and "exceptionDetails" in result["result"]:
return {
"success": False,
"result": result["result"]
@@ -154,7 +165,7 @@ class Utilities:
"result": e
}
async def remove_css_from_tab(self, tab, css_id):
async def remove_css_from_tab(self, tab: str, css_id: str):
try:
result = await inject_to_tab(tab,
f"""
@@ -166,7 +177,7 @@ class Utilities:
}})()
""", False)
if "exceptionDetails" in result["result"]:
if result and "exceptionDetails" in result["result"]:
return {
"success": False,
"result": result
@@ -181,10 +192,10 @@ class Utilities:
"result": e
}
async def get_setting(self, key, default):
async def get_setting(self, key: str, default: Any):
return self.context.settings.getSetting(key, default)
async def set_setting(self, key, value):
async def set_setting(self, key: str, value: Any):
return self.context.settings.setSetting(key, value)
async def allow_remote_debugging(self):
@@ -209,32 +220,34 @@ class Utilities:
if path == None:
path = get_home_path()
path = Path(path).resolve()
path_obj = Path(path).resolve()
files, folders = [], []
files: List[FilePickerObj] = []
folders: List[FilePickerObj] = []
#Resolving all files/folders in the requested directory
for file in path.iterdir():
for file in path_obj.iterdir():
if file.exists():
filest = file.stat()
is_hidden = file.name.startswith('.')
if ON_WINDOWS and not is_hidden:
is_hidden = bool(filest.st_file_attributes & FILE_ATTRIBUTE_HIDDEN)
is_hidden = bool(filest.st_file_attributes & FILE_ATTRIBUTE_HIDDEN) # type: ignore
if include_folders and file.is_dir():
if (is_hidden and include_hidden) or not is_hidden:
folders.append({"file": file, "filest": filest, "is_dir": True})
elif include_files:
# Handle requested extensions if present
if 'all_files' in include_ext or splitext(file.name)[1].lstrip('.') in include_ext:
if len(include_ext) == 0 or 'all_files' in include_ext \
or splitext(file.name)[1].lstrip('.') in include_ext:
if (is_hidden and include_hidden) or not is_hidden:
files.append({"file": file, "filest": filest, "is_dir": False})
# Filter logic
if filter_for is not None:
try:
if re.compile(filter_for):
files = filter(lambda file: re.search(filter_for, file.name) != None, files)
files = list(filter(lambda file: re.search(filter_for, file["file"].name) != None, files))
except re.error:
files = filter(lambda file: file.name.find(filter_for) != -1, files)
files = list(filter(lambda file: file["file"].name.find(filter_for) != -1, files))
# Ordering logic
ord_arg = order_by.split("_")
@@ -254,6 +267,9 @@ class Utilities:
files.sort(key=lambda x: x['filest'].st_size, reverse = not rev)
# Folders has no file size, order by name instead
folders.sort(key=lambda x: x['file'].name.casefold())
case _:
files.sort(key=lambda x: x['file'].name.casefold(), reverse = rev)
folders.sort(key=lambda x: x['file'].name.casefold(), reverse = rev)
#Constructing the final file list, folders first
all = [{
@@ -273,14 +289,14 @@ class Utilities:
# Based on https://stackoverflow.com/a/46422554/13174603
def start_rdt_proxy(self, ip, port):
async def pipe(reader, writer):
def start_rdt_proxy(self, ip: str, port: int):
async def pipe(reader: StreamReader, writer: StreamWriter):
try:
while not reader.at_eof():
writer.write(await reader.read(2048))
finally:
writer.close()
async def handle_client(local_reader, local_writer):
async def handle_client(local_reader: StreamReader, local_writer: StreamWriter):
try:
remote_reader, remote_writer = await open_connection(
ip, port)
@@ -294,9 +310,10 @@ class Utilities:
self.rdt_proxy_task = self.context.loop.create_task(self.rdt_proxy_server)
def stop_rdt_proxy(self):
if self.rdt_proxy_server:
if self.rdt_proxy_server != None:
self.rdt_proxy_server.close()
self.rdt_proxy_task.cancel()
if self.rdt_proxy_task:
self.rdt_proxy_task.cancel()
async def _enable_rdt(self):
# TODO un-hardcode port
@@ -346,11 +363,11 @@ class Utilities:
await tab.evaluate_js("location.reload();", False, True, False)
self.logger.info("React DevTools disabled")
async def get_user_info(self) -> dict:
async def get_user_info(self) -> Dict[str, str]:
return {
"username": get_username(),
"path": get_home_path()
}
async def get_tab_id(self, name):
async def get_tab_id(self, name: str):
return (await get_tab(name)).id
+1 -1
View File
@@ -36,7 +36,7 @@ export default {
lineEnding: 'auto',
// Control the line ending. See options at https://github.com/ryanve/eol
locales: ['en-US', 'it-IT'],
locales: ['en-US'],
// An array of the locales in your applications
namespaceSeparator: false,
+1 -1
View File
@@ -46,7 +46,7 @@
"dependencies": {
"decky-frontend-lib": "3.21.1",
"filesize": "^10.0.7",
"i18next": "^23.1.0",
"i18next": "^23.2.1",
"i18next-http-backend": "^2.2.1",
"react-file-icon": "^1.3.0",
"react-i18next": "^12.3.1",
+105 -35
View File
@@ -1,4 +1,4 @@
lockfileVersion: '6.1'
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
@@ -12,8 +12,8 @@ dependencies:
specifier: ^10.0.7
version: 10.0.7
i18next:
specifier: ^23.1.0
version: 23.1.0
specifier: ^23.2.1
version: 23.2.1
i18next-http-backend:
specifier: ^2.2.1
version: 2.2.1
@@ -22,7 +22,7 @@ dependencies:
version: 1.3.0(react-dom@16.14.0)(react@16.14.0)
react-i18next:
specifier: ^12.3.1
version: 12.3.1(i18next@23.1.0)(react-dom@16.14.0)(react@16.14.0)
version: 12.3.1(i18next@23.2.1)(react-dom@16.14.0)(react@16.14.0)
react-icons:
specifier: ^4.9.0
version: 4.9.0(react@16.14.0)
@@ -120,6 +120,14 @@ packages:
'@jridgewell/trace-mapping': 0.3.18
dev: true
/@babel/code-frame@7.22.13:
resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/highlight': 7.22.20
chalk: 2.4.2
dev: true
/@babel/code-frame@7.22.5:
resolution: {integrity: sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==}
engines: {node: '>=6.9.0'}
@@ -144,7 +152,7 @@ packages:
'@babel/helpers': 7.22.5
'@babel/parser': 7.22.5
'@babel/template': 7.22.5
'@babel/traverse': 7.22.5
'@babel/traverse': 7.23.2
'@babel/types': 7.22.5
convert-source-map: 1.9.0
debug: 4.3.4
@@ -165,6 +173,16 @@ packages:
jsesc: 2.5.2
dev: true
/@babel/generator@7.23.0:
resolution: {integrity: sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.23.0
'@jridgewell/gen-mapping': 0.3.3
'@jridgewell/trace-mapping': 0.3.18
jsesc: 2.5.2
dev: true
/@babel/helper-compilation-targets@7.22.5(@babel/core@7.22.5):
resolution: {integrity: sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==}
engines: {node: '>=6.9.0'}
@@ -179,24 +197,29 @@ packages:
semver: 6.3.0
dev: true
/@babel/helper-environment-visitor@7.22.20:
resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==}
engines: {node: '>=6.9.0'}
dev: true
/@babel/helper-environment-visitor@7.22.5:
resolution: {integrity: sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==}
engines: {node: '>=6.9.0'}
dev: true
/@babel/helper-function-name@7.22.5:
resolution: {integrity: sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==}
/@babel/helper-function-name@7.23.0:
resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/template': 7.22.5
'@babel/types': 7.22.5
'@babel/template': 7.22.15
'@babel/types': 7.23.0
dev: true
/@babel/helper-hoist-variables@7.22.5:
resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.22.5
'@babel/types': 7.23.0
dev: true
/@babel/helper-module-imports@7.22.5:
@@ -216,7 +239,7 @@ packages:
'@babel/helper-split-export-declaration': 7.22.5
'@babel/helper-validator-identifier': 7.22.5
'@babel/template': 7.22.5
'@babel/traverse': 7.22.5
'@babel/traverse': 7.23.2
'@babel/types': 7.22.5
transitivePeerDependencies:
- supports-color
@@ -236,11 +259,23 @@ packages:
'@babel/types': 7.22.5
dev: true
/@babel/helper-split-export-declaration@7.22.6:
resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.23.0
dev: true
/@babel/helper-string-parser@7.22.5:
resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==}
engines: {node: '>=6.9.0'}
dev: true
/@babel/helper-validator-identifier@7.22.20:
resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==}
engines: {node: '>=6.9.0'}
dev: true
/@babel/helper-validator-identifier@7.22.5:
resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==}
engines: {node: '>=6.9.0'}
@@ -256,12 +291,21 @@ packages:
engines: {node: '>=6.9.0'}
dependencies:
'@babel/template': 7.22.5
'@babel/traverse': 7.22.5
'@babel/traverse': 7.23.2
'@babel/types': 7.22.5
transitivePeerDependencies:
- supports-color
dev: true
/@babel/highlight@7.22.20:
resolution: {integrity: sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/helper-validator-identifier': 7.22.20
chalk: 2.4.2
js-tokens: 4.0.0
dev: true
/@babel/highlight@7.22.5:
resolution: {integrity: sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==}
engines: {node: '>=6.9.0'}
@@ -279,12 +323,29 @@ packages:
'@babel/types': 7.22.5
dev: true
/@babel/parser@7.23.0:
resolution: {integrity: sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==}
engines: {node: '>=6.0.0'}
hasBin: true
dependencies:
'@babel/types': 7.23.0
dev: true
/@babel/runtime@7.22.5:
resolution: {integrity: sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==}
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.13.11
/@babel/template@7.22.15:
resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/code-frame': 7.22.13
'@babel/parser': 7.23.0
'@babel/types': 7.23.0
dev: true
/@babel/template@7.22.5:
resolution: {integrity: sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==}
engines: {node: '>=6.9.0'}
@@ -294,18 +355,18 @@ packages:
'@babel/types': 7.22.5
dev: true
/@babel/traverse@7.22.5:
resolution: {integrity: sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==}
/@babel/traverse@7.23.2:
resolution: {integrity: sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/code-frame': 7.22.5
'@babel/generator': 7.22.5
'@babel/helper-environment-visitor': 7.22.5
'@babel/helper-function-name': 7.22.5
'@babel/code-frame': 7.22.13
'@babel/generator': 7.23.0
'@babel/helper-environment-visitor': 7.22.20
'@babel/helper-function-name': 7.23.0
'@babel/helper-hoist-variables': 7.22.5
'@babel/helper-split-export-declaration': 7.22.5
'@babel/parser': 7.22.5
'@babel/types': 7.22.5
'@babel/helper-split-export-declaration': 7.22.6
'@babel/parser': 7.23.0
'@babel/types': 7.23.0
debug: 4.3.4
globals: 11.12.0
transitivePeerDependencies:
@@ -321,6 +382,15 @@ packages:
to-fast-properties: 2.0.0
dev: true
/@babel/types@7.23.0:
resolution: {integrity: sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/helper-string-parser': 7.22.5
'@babel/helper-validator-identifier': 7.22.20
to-fast-properties: 2.0.0
dev: true
/@esbuild/android-arm64@0.17.19:
resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==}
engines: {node: '>=12'}
@@ -1108,8 +1178,8 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
dependencies:
caniuse-lite: 1.0.30001504
electron-to-chromium: 1.4.433
caniuse-lite: 1.0.30001506
electron-to-chromium: 1.4.435
node-releases: 2.0.12
update-browserslist-db: 1.0.11(browserslist@4.21.9)
dev: true
@@ -1161,8 +1231,8 @@ packages:
engines: {node: '>=4'}
dev: true
/caniuse-lite@1.0.30001504:
resolution: {integrity: sha512-5uo7eoOp2mKbWyfMXnGO9rJWOGU8duvzEiYITW+wivukL7yHH4gX9yuRaobu6El4jPxo6jKZfG+N6fB621GD/Q==}
/caniuse-lite@1.0.30001506:
resolution: {integrity: sha512-6XNEcpygZMCKaufIcgpQNZNf00GEqc7VQON+9Rd0K1bMYo8xhMZRAo5zpbnbMNizi4YNgIDAFrdykWsvY3H4Hw==}
dev: true
/ccount@2.0.1:
@@ -1518,8 +1588,8 @@ packages:
stream-shift: 1.0.1
dev: true
/electron-to-chromium@1.4.433:
resolution: {integrity: sha512-MGO1k0w1RgrfdbLVwmXcDhHHuxCn2qRgR7dYsJvWFKDttvYPx6FNzCGG0c/fBBvzK2LDh3UV7Tt9awnHnvAAUQ==}
/electron-to-chromium@1.4.435:
resolution: {integrity: sha512-B0CBWVFhvoQCW/XtjRzgrmqcgVWg6RXOEM/dK59+wFV93BFGR6AeNKc4OyhM+T3IhJaOOG8o/V+33Y2mwJWtzw==}
dev: true
/emoji-regex@8.0.0:
@@ -2008,8 +2078,8 @@ packages:
'@babel/runtime': 7.22.5
dev: true
/i18next@23.1.0:
resolution: {integrity: sha512-CObNPofJpw7zGVGYLd58mtMZUF+NZQl9czYMihbJkStjX+Nlu9kC3PHiC6uE1niP3qxP/3ocLXIBc2zqbAb1dg==}
/i18next@23.2.1:
resolution: {integrity: sha512-BAWWqXYM1OtgA2hkGvnXWTs9wg0g3qx0SoiJoVTiL5Ia2EDZK0Veyuq4m2YtZeSs9D/9cwrNFxCKJi2/gdjZ4A==}
dependencies:
'@babel/runtime': 7.22.5
dev: false
@@ -2052,7 +2122,7 @@ packages:
dependencies:
'@babel/core': 7.22.5
'@babel/parser': 7.22.5
'@babel/traverse': 7.22.5
'@babel/traverse': 7.23.2
'@babel/types': 7.22.5
find-line-column: 0.5.2
transitivePeerDependencies:
@@ -3144,7 +3214,7 @@ packages:
react-dom: 16.14.0(react@16.14.0)
dev: false
/react-i18next@12.3.1(i18next@23.1.0)(react-dom@16.14.0)(react@16.14.0):
/react-i18next@12.3.1(i18next@23.2.1)(react-dom@16.14.0)(react@16.14.0):
resolution: {integrity: sha512-5v8E2XjZDFzK7K87eSwC7AJcAkcLt5xYZ4+yTPDAW1i7C93oOY1dnr4BaQM7un4Hm+GmghuiPvevWwlca5PwDA==}
peerDependencies:
i18next: '>= 19.0.0'
@@ -3159,7 +3229,7 @@ packages:
dependencies:
'@babel/runtime': 7.22.5
html-parse-stringify: 3.0.1
i18next: 23.1.0
i18next: 23.2.1
react: 16.14.0
react-dom: 16.14.0(react@16.14.0)
dev: false
@@ -3637,12 +3707,12 @@ packages:
jest-worker: 27.5.1
schema-utils: 3.3.0
serialize-javascript: 6.0.1
terser: 5.18.0
terser: 5.18.1
webpack: 5.87.0
dev: true
/terser@5.18.0:
resolution: {integrity: sha512-pdL757Ig5a0I+owA42l6tIuEycRuM7FPY4n62h44mRLRfnOxJkkOHd6i89dOpwZlpF6JXBwaAHF6yWzFrt+QyA==}
/terser@5.18.1:
resolution: {integrity: sha512-j1n0Ao919h/Ai5r43VAnfV/7azUYW43GPxK7qSATzrsERfW7+y2QW9Cp9ufnRF5CQUWbnLSo7UJokSWCqg4tsQ==}
engines: {node: '>=10'}
hasBin: true
dependencies:
+9
View File
@@ -1,5 +1,6 @@
import { FC, createContext, useContext, useEffect, useState } from 'react';
import { DEFAULT_NOTIFICATION_SETTINGS, NotificationSettings } from '../notification-service';
import { Plugin } from '../plugin';
import { PluginUpdateMapping } from '../store';
import { VerInfo } from '../updater';
@@ -13,6 +14,7 @@ interface PublicDeckyState {
hasLoaderUpdate?: boolean;
isLoaderUpdating: boolean;
versionInfo: VerInfo | null;
notificationSettings: NotificationSettings;
userInfo: UserInfo | null;
}
@@ -30,6 +32,7 @@ export class DeckyState {
private _hasLoaderUpdate: boolean = false;
private _isLoaderUpdating: boolean = false;
private _versionInfo: VerInfo | null = null;
private _notificationSettings = DEFAULT_NOTIFICATION_SETTINGS;
private _userInfo: UserInfo | null = null;
public eventBus = new EventTarget();
@@ -44,6 +47,7 @@ export class DeckyState {
hasLoaderUpdate: this._hasLoaderUpdate,
isLoaderUpdating: this._isLoaderUpdating,
versionInfo: this._versionInfo,
notificationSettings: this._notificationSettings,
userInfo: this._userInfo,
};
}
@@ -93,6 +97,11 @@ export class DeckyState {
this.notifyUpdate();
}
setNotificationSettings(notificationSettings: NotificationSettings) {
this._notificationSettings = notificationSettings;
this.notifyUpdate();
}
setUserInfo(userInfo: UserInfo) {
this._userInfo = userInfo;
this.notifyUpdate();
+7 -3
View File
@@ -1,5 +1,6 @@
import { DialogButton, Focusable, Router, staticClasses } from 'decky-frontend-lib';
import { CSSProperties, VFC } from 'react';
import { useTranslation } from 'react-i18next';
import { BsGearFill } from 'react-icons/bs';
import { FaArrowLeft, FaStore } from 'react-icons/fa';
@@ -13,6 +14,7 @@ const titleStyles: CSSProperties = {
const TitleView: VFC = () => {
const { activePlugin, closeActivePlugin } = useDeckyState();
const { t } = useTranslation();
const onSettingsClick = () => {
Router.CloseSideMenus();
@@ -31,12 +33,14 @@ const TitleView: VFC = () => {
<DialogButton
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
onClick={onStoreClick}
onOKActionDescription={t('TitleView.decky_store_desc')}
>
<FaStore style={{ marginTop: '-4px', display: 'block' }} />
</DialogButton>
<DialogButton
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
onClick={onSettingsClick}
onOKActionDescription={t('TitleView.settings_desc')}
>
<BsGearFill style={{ marginTop: '-4px', display: 'block' }} />
</DialogButton>
@@ -45,15 +49,15 @@ const TitleView: VFC = () => {
}
return (
<div className={staticClasses.Title} style={titleStyles}>
<Focusable className={staticClasses.Title} style={titleStyles}>
<DialogButton
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
onClick={closeActivePlugin}
>
<FaArrowLeft style={{ marginTop: '-4px', display: 'block' }} />
</DialogButton>
<div style={{ flex: 0.9 }}>{activePlugin.name}</div>
</div>
{activePlugin?.titleView || <div style={{ flex: 0.9 }}>{activePlugin.name}</div>}
</Focusable>
);
};
@@ -1,10 +1,11 @@
import { FC, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { IconContext } from 'react-icons';
import { FaExclamationTriangle, FaQuestionCircle } from 'react-icons/fa';
import { FaExclamationTriangle, FaQuestionCircle, FaUserSlash } from 'react-icons/fa';
export enum FileErrorTypes {
FileNotFound,
PermissionDenied,
Unknown,
None,
}
@@ -25,6 +26,10 @@ const FilePickerError: FC<FilePickerErrorProps> = ({ error, rawError = null }) =
setText(t('FilePickerError.errors.file_not_found'));
setIcon(<FaExclamationTriangle />);
break;
case FileErrorTypes.PermissionDenied:
setText(t('FilePickerError.errors.perm_denied'));
setIcon(<FaUserSlash />);
break;
case FileErrorTypes.Unknown:
setText(t('FilePickerError.errors.unknown', { raw_error: rawError }));
setIcon(<FaQuestionCircle />);
@@ -13,7 +13,7 @@ import {
} from 'decky-frontend-lib';
import { filesize } from 'filesize';
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react';
import { FileIcon, defaultStyles } from 'react-file-icon';
import { DefaultExtensionType, FileIcon, defaultStyles } from 'react-file-icon';
import { useTranslation } from 'react-i18next';
import { FaArrowUp, FaFolder } from 'react-icons/fa';
@@ -34,6 +34,7 @@ export interface FilePickerProps {
allowAllFiles?: boolean;
defaultHidden?: boolean;
max?: number;
fileSelType?: FileSelectionType;
onSubmit: (val: { path: string; realpath: string }) => void;
closeModal?: () => void;
}
@@ -48,6 +49,11 @@ export interface File {
created: number;
}
export enum FileSelectionType {
FILE,
FOLDER,
}
interface FileListing {
realpath: string;
files: File[];
@@ -120,13 +126,21 @@ const iconStyles = {
const FilePicker: FunctionComponent<FilePickerProps> = ({
startPath,
//What are we allowing to show in the file picker
includeFiles = true,
filter = undefined,
includeFolders = true,
//Parameter for specifying a specific filename match
filter = undefined,
//Filter for specific extensions as an array
validFileExtensions = undefined,
//Allow to override the fixed extension above
allowAllFiles = true,
//If we need to show hidden files and folders (both Win and Linux should work)
defaultHidden = false, // false by default makes sense for most users
//How much files per page to show, default 1000
max = 1000,
//Which picking option to select by default
fileSelType = FileSelectionType.FOLDER,
onSubmit,
closeModal,
}) => {
@@ -196,6 +210,9 @@ const FilePicker: FunctionComponent<FilePickerProps> = ({
case theError.match(/\[WinError\s3.*/i)?.input:
setError(FileErrorTypes.FileNotFound);
break;
case theError.match(/\[Errno\s13.*/i)?.input:
setError(FileErrorTypes.PermissionDenied);
break;
default:
setRawError(theError);
setError(FileErrorTypes.Unknown);
@@ -299,7 +316,12 @@ const FilePicker: FunctionComponent<FilePickerProps> = ({
) : (
<div style={iconStyles}>
{file.realpath.includes('.') ? (
<FileIcon {...defaultStyles[extension]} {...styleDefObj[extension]} extension={''} />
<FileIcon
{...defaultStyles[extension as DefaultExtensionType]}
// @ts-expect-error
{...styleDefObj[extension]}
extension={''}
/>
) : (
<FileIcon />
)}
@@ -327,7 +349,7 @@ const FilePicker: FunctionComponent<FilePickerProps> = ({
</Focusable>
</DialogControlsSection>
</DialogBody>
{!loading && !error && !includeFiles && (
{!loading && error === FileErrorTypes.None && (
<DialogFooter>
<DialogButton
className="Primary"
@@ -337,7 +359,9 @@ const FilePicker: FunctionComponent<FilePickerProps> = ({
closeModal?.();
}}
>
{t('FilePickerIndex.folder.select')}
{fileSelType === FileSelectionType.FILE
? t('FilePickerIndex.file.select')
: t('FilePickerIndex.folder.select')}
</DialogButton>
</DialogFooter>
)}
@@ -17,6 +17,7 @@ import Logger from '../../../../logger';
import { installFromURL } from '../../../../store';
import { useSetting } from '../../../../utils/hooks/useSetting';
import { getSetting } from '../../../../utils/settings';
import { FileSelectionType } from '../../../modals/filepicker';
import RemoteDebuggingSettings from '../general/RemoteDebugging';
const logger = new Logger('DeveloperIndex');
@@ -27,7 +28,16 @@ const installFromZip = async () => {
logger.error('The default path has not been found!');
return;
}
window.DeckyPluginLoader.openFilePicker(path, true, undefined, true, ['zip', 'rar'], false, true).then((val) => {
window.DeckyPluginLoader.openFilePickerV2(
FileSelectionType.FILE,
path,
true,
true,
undefined,
['zip'],
false,
false,
).then((val) => {
const url = `file://${val.path}`;
console.log(`Installing plugin locally from ${url}`);
installFromURL(url);
@@ -29,10 +29,10 @@ const BranchSelect: FunctionComponent<{}> = () => {
<Field label={t('BranchSelect.update_channel.label')} childrenContainerWidth={'fixed'}>
<Dropdown
rgOptions={Object.values(UpdateBranch)
.filter((branch) => typeof branch == 'string')
.filter((branch) => typeof branch == 'number')
.map((branch) => ({
label: tBranches[UpdateBranch[branch]],
data: UpdateBranch[branch],
label: tBranches[branch as number],
data: branch,
}))}
selectedOption={selectedBranch}
onChange={async (newVal) => {
@@ -0,0 +1,35 @@
import { Field, Toggle } from 'decky-frontend-lib';
import { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { useDeckyState } from '../../../DeckyState';
const NotificationSettings: FC = () => {
const { notificationSettings } = useDeckyState();
const notificationService = window.DeckyPluginLoader.notificationService;
const { t } = useTranslation();
return (
<>
<Field label={t('SettingsGeneralIndex.notifications.decky_updates_label')}>
<Toggle
value={notificationSettings.deckyUpdates}
onChange={(deckyUpdates) => {
notificationService.update({ ...notificationSettings, deckyUpdates });
}}
/>
</Field>
<Field label={t('SettingsGeneralIndex.notifications.plugin_updates_label')}>
<Toggle
value={notificationSettings.pluginUpdates}
onChange={(pluginUpdates) => {
notificationService.update({ ...notificationSettings, pluginUpdates });
}}
/>
</Field>
</>
);
};
export default NotificationSettings;
@@ -26,10 +26,10 @@ const StoreSelect: FunctionComponent<{}> = () => {
<Field label={t('StoreSelect.store_channel.label')} childrenContainerWidth={'fixed'}>
<Dropdown
rgOptions={Object.values(Store)
.filter((store) => typeof store == 'string')
.filter((store) => typeof store == 'number')
.map((store) => ({
label: tStores[Store[store]],
data: Store[store],
label: tStores[store as number],
data: store,
}))}
selectedOption={selectedStore}
onChange={async (newVal) => {
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
import { useDeckyState } from '../../../DeckyState';
import BranchSelect from './BranchSelect';
import NotificationSettings from './NotificationSettings';
import StoreSelect from './StoreSelect';
import UpdaterSettings from './Updater';
@@ -27,6 +28,10 @@ export default function GeneralSettings({
<BranchSelect />
<StoreSelect />
</DialogControlsSection>
<DialogControlsSection>
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.notifications.header')}</DialogControlsSectionHeader>
<NotificationSettings />
</DialogControlsSection>
<DialogControlsSection>
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.other.header')}</DialogControlsSectionHeader>
<Field label={t('SettingsGeneralIndex.developer_mode.label')}>
+39 -5
View File
@@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next';
import logo from '../../../assets/plugin_store.png';
import Logger from '../../logger';
import { StorePlugin, getPluginList } from '../../store';
import { Store, StorePlugin, getPluginList, getStore } from '../../store';
import PluginCard from './PluginCard';
const logger = new Logger('Store');
@@ -21,6 +21,7 @@ const logger = new Logger('Store');
const StorePage: FC<{}> = () => {
const [currentTabRoute, setCurrentTabRoute] = useState<string>('browse');
const [data, setData] = useState<StorePlugin[] | null>(null);
const [isTesting, setIsTesting] = useState<boolean>(false);
const { TabCount } = findModule((m) => {
if (m?.TabCount && m?.TabTitle) return true;
return false;
@@ -33,6 +34,9 @@ const StorePage: FC<{}> = () => {
const res = await getPluginList();
logger.log('got data!', res);
setData(res);
const storeRes = await getStore();
logger.log(`store is ${storeRes}, isTesting is ${storeRes === Store.Testing}`);
setIsTesting(storeRes === Store.Testing);
})();
}, []);
@@ -58,7 +62,7 @@ const StorePage: FC<{}> = () => {
tabs={[
{
title: t('Store.store_tabs.title'),
content: <BrowseTab children={{ data: data }} />,
content: <BrowseTab children={{ data: data, isTesting: isTesting }} />,
id: 'browse',
renderTabAddon: () => <span className={TabCount}>{data.length}</span>,
},
@@ -75,7 +79,7 @@ const StorePage: FC<{}> = () => {
);
};
const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
const BrowseTab: FC<{ children: { data: StorePlugin[]; isTesting: boolean } }> = (data) => {
const { t } = useTranslation();
const sortOptions = useMemo(
@@ -178,6 +182,36 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
</div>
</Focusable>
</div>
{data.children.isTesting && (
<div
style={{
alignItems: 'center',
display: 'flex',
flexDirection: 'column',
marginLeft: '20px',
marginRight: '20px',
marginBottom: '20px',
padding: '8px 36px',
background: 'rgba(255, 255, 0, 0.067)',
textAlign: 'center',
border: '2px solid rgba(255, 255, 0, 0.467)',
}}
>
<h2 style={{ margin: 0 }}>{t('Store.store_testing_warning.label')}</h2>
<span>
{`${t('Store.store_testing_warning.desc')} `}
<a
href="https://decky.xyz/testing"
target="_blank"
style={{
textDecoration: 'none',
}}
>
decky.xyz/testing
</a>
</span>
</div>
)}
<div>
{data.children.data
.filter((plugin: StorePlugin) => {
@@ -229,13 +263,13 @@ const AboutTab: FC<{}> = () => {
<span>
{t('Store.store_testing_cta')}{' '}
<a
href="https://deckbrew.xyz/testing"
href="https://decky.xyz/testing"
target="_blank"
style={{
textDecoration: 'none',
}}
>
deckbrew.xyz/testing
decky.xyz/testing
</a>
</span>
<span className="deckyStoreAboutHeader">{t('Store.store_contrib.label')}</span>
+51
View File
@@ -0,0 +1,51 @@
import { DeckyState } from './components/DeckyState';
import { getSetting, setSetting } from './utils/settings';
export interface NotificationSettings {
deckyUpdates: boolean;
pluginUpdates: boolean;
}
export const DEFAULT_NOTIFICATION_SETTINGS: NotificationSettings = {
deckyUpdates: true,
pluginUpdates: true,
};
/**
* A Service class for managing the notification settings
*
* It's mostly responsible for sending setting updates to the server and keeping the local state in sync.
*/
export class NotificationService {
constructor(private deckyState: DeckyState) {}
async init() {
const notificationSettings = await getSetting<Partial<NotificationSettings>>('notificationSettings', {});
// Adding a fallback to the default settings to be backwards compatible if we ever add new notification settings
this.deckyState.setNotificationSettings({
...DEFAULT_NOTIFICATION_SETTINGS,
...notificationSettings,
});
}
/**
* Sends the new notification settings to the server and persists it locally in the decky state
*
* @param notificationSettings The new notification settings
*/
async update(notificationSettings: NotificationSettings) {
await setSetting('notificationSettings', notificationSettings);
this.deckyState.setNotificationSettings(notificationSettings);
}
/**
* For a specific event, returns true if a notification should be shown
*
* @param event The notification event that should be checked
* @returns true if the notification should be shown
*/
shouldNotify(event: keyof NotificationSettings) {
return this.deckyState.publicState().notificationSettings[event];
}
}
+35 -14
View File
@@ -14,7 +14,7 @@ import { FaExclamationCircle, FaPlug } from 'react-icons/fa';
import { DeckyState, DeckyStateContextProvider, UserInfo, useDeckyState } from './components/DeckyState';
import LegacyPlugin from './components/LegacyPlugin';
import { File } from './components/modals/filepicker';
import { File, FileSelectionType } from './components/modals/filepicker';
import { deinitFilepickerPatches, initFilepickerPatches } from './components/modals/filepicker/patches';
import MultiplePluginsInstallModal from './components/modals/MultiplePluginsInstallModal';
import PluginInstallModal from './components/modals/PluginInstallModal';
@@ -24,6 +24,7 @@ import PluginView from './components/PluginView';
import WithSuspense from './components/WithSuspense';
import { HiddenPluginsService } from './hidden-plugins-service';
import Logger from './logger';
import { NotificationService } from './notification-service';
import { InstallType, Plugin } from './plugin';
import RouterHook from './router-hook';
import { deinitSteamFixes, initSteamFixes } from './steamfixes';
@@ -47,7 +48,9 @@ class PluginLoader extends Logger {
private routerHook: RouterHook = new RouterHook();
public toaster: Toaster = new Toaster();
private deckyState: DeckyState = new DeckyState();
public hiddenPluginsService = new HiddenPluginsService(this.deckyState);
public notificationService = new NotificationService(this.deckyState);
private reloadLock: boolean = false;
// stores a list of plugin names which requested to be reloaded
@@ -121,18 +124,20 @@ class PluginLoader extends Logger {
public async notifyUpdates() {
const versionInfo = await this.updateVersion();
if (versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current) {
this.toaster.toast({
title: <TranslationHelper trans_class={TranslationClass.PLUGIN_LOADER} trans_text="decky_title" />,
body: (
<TranslationHelper
trans_class={TranslationClass.PLUGIN_LOADER}
trans_text="decky_update_available"
i18n_args={{ tag_name: versionInfo?.remote?.tag_name }}
/>
),
onClick: () => Router.Navigate('/decky/settings'),
});
this.deckyState.setHasLoaderUpdate(true);
if (this.notificationService.shouldNotify('deckyUpdates')) {
this.toaster.toast({
title: <TranslationHelper trans_class={TranslationClass.PLUGIN_LOADER} trans_text="decky_title" />,
body: (
<TranslationHelper
trans_class={TranslationClass.PLUGIN_LOADER}
trans_text="decky_update_available"
i18n_args={{ tag_name: versionInfo?.remote?.tag_name }}
/>
),
onClick: () => Router.Navigate('/decky/settings'),
});
}
}
await sleep(7000);
await this.notifyPluginUpdates();
@@ -146,7 +151,7 @@ class PluginLoader extends Logger {
public async notifyPluginUpdates() {
const updates = await this.checkPluginUpdates();
if (updates?.size > 0) {
if (updates?.size > 0 && this.notificationService.shouldNotify('pluginUpdates')) {
this.toaster.toast({
title: <TranslationHelper trans_class={TranslationClass.PLUGIN_LOADER} trans_text="decky_title" />,
body: (
@@ -220,6 +225,7 @@ class PluginLoader extends Logger {
});
this.hiddenPluginsService.init();
this.notificationService.init();
}
public deinit() {
@@ -360,10 +366,23 @@ class PluginLoader extends Logger {
}
openFilePicker(
startPath: string,
selectFiles?: boolean,
regex?: RegExp,
): Promise<{ path: string; realpath: string }> {
if (selectFiles) {
return this.openFilePickerV2(FileSelectionType.FILE, startPath, true, true, regex);
} else {
return this.openFilePickerV2(FileSelectionType.FOLDER, startPath, false, true, regex);
}
}
openFilePickerV2(
select: FileSelectionType,
startPath: string,
includeFiles?: boolean,
filter?: RegExp | ((file: File) => boolean),
includeFolders?: boolean,
filter?: RegExp | ((file: File) => boolean),
extensions?: string[],
showHiddenFiles?: boolean,
allowAllFiles?: boolean,
@@ -389,6 +408,7 @@ class PluginLoader extends Logger {
defaultHidden={showHiddenFiles}
onSubmit={resolve}
closeModal={closeModal}
fileSelType={select}
max={max}
/>
</WithSuspense>
@@ -404,6 +424,7 @@ class PluginLoader extends Logger {
toaster: this.toaster,
callServerMethod: this.callServerMethod,
openFilePicker: this.openFilePicker,
openFilePickerV2: this.openFilePickerV2,
async callPluginMethod(methodName: string, args = {}) {
const response = await fetch(`http://127.0.0.1:1337/plugins/${pluginName}/methods/${methodName}`, {
method: 'POST',
+1
View File
@@ -5,6 +5,7 @@ export interface Plugin {
content?: JSX.Element;
onDismount?(): void;
alwaysRender?: boolean;
titleView?: JSX.Element;
}
export enum InstallType {
+4 -1
View File
@@ -22,6 +22,8 @@ declare global {
}
}
const isPatched = Symbol('is patched');
class RouterHook extends Logger {
private router: any;
private memoizedRouter: any;
@@ -90,9 +92,10 @@ class RouterHook extends Logger {
...routeList[index].props,
children: {
...cloneElement(routeList[index].props.children),
type: (props) => createElement(oType, props),
type: routeList[index].props.children[isPatched] ? oType : (props) => createElement(oType, props),
},
}).children;
routeList[index].props.children[isPatched] = true;
});
}
});
+31 -32
View File
@@ -32,43 +32,42 @@ export interface PluginInstallRequest {
// name: version
export type PluginUpdateMapping = Map<string, StorePluginVersion>;
export async function getStore(): Promise<Store> {
return await getSetting<Store>('store', Store.Default);
}
export async function getPluginList(): Promise<StorePlugin[]> {
let version = await window.DeckyPluginLoader.updateVersion();
let store = await getSetting<Store>('store', Store.Default);
let store = await getSetting<Store | null>('store', null);
let customURL = await getSetting<string>('store-url', 'https://plugins.deckbrew.xyz/plugins');
let storeURL;
if (!store) {
console.log('Could not get a default store, using Default.');
await setSetting('store-url', Store.Default);
return fetch('https://plugins.deckbrew.xyz/plugins', {
method: 'GET',
headers: {
'X-Decky-Version': version.current,
},
}).then((r) => r.json());
} else {
switch (+store) {
case Store.Default:
storeURL = 'https://plugins.deckbrew.xyz/plugins';
break;
case Store.Testing:
storeURL = 'https://testing.deckbrew.xyz/plugins';
break;
case Store.Custom:
storeURL = customURL;
break;
default:
console.error('Somehow you ended up without a standard URL, using the default URL.');
storeURL = 'https://plugins.deckbrew.xyz/plugins';
break;
}
return fetch(storeURL, {
method: 'GET',
headers: {
'X-Decky-Version': version.current,
},
}).then((r) => r.json());
if (store === null) {
console.log('Could not get store, using Default.');
await setSetting('store', Store.Default);
store = Store.Default;
}
switch (+store) {
case Store.Default:
storeURL = 'https://plugins.deckbrew.xyz/plugins';
break;
case Store.Testing:
storeURL = 'https://testing.deckbrew.xyz/plugins';
break;
case Store.Custom:
storeURL = customURL;
break;
default:
console.error('Somehow you ended up without a standard URL, using the default URL.');
storeURL = 'https://plugins.deckbrew.xyz/plugins';
break;
}
return fetch(storeURL, {
method: 'GET',
headers: {
'X-Decky-Version': version.current,
},
}).then((r) => r.json());
}
export async function installFromURL(url: string) {
-1
View File
@@ -14,7 +14,6 @@
"noImplicitThis": true,
"noImplicitAny": true,
"strict": true,
"suppressImplicitAnyIndexErrors": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"resolveJsonModule": true
+9 -1
View File
@@ -17,6 +17,7 @@ __version__ = '0.1.0'
import os
import subprocess
import logging
import time
"""
Constants
@@ -117,7 +118,8 @@ Environment variable: `DECKY_PLUGIN_AUTHOR`.
e.g.: `John Doe`
"""
DECKY_PLUGIN_LOG: str = os.path.join(DECKY_PLUGIN_LOG_DIR, "plugin.log")
__cur_time = time.strftime("%Y-%m-%d %H.%M.%S")
DECKY_PLUGIN_LOG: str = os.path.join(DECKY_PLUGIN_LOG_DIR, f"{__cur_time}.log")
"""
The path to the plugin's main logfile.
Environment variable: `DECKY_PLUGIN_LOG`.
@@ -192,6 +194,12 @@ def migrate_logs(*files_or_directories: str) -> dict[str, str]:
Logging
"""
try:
for x in [entry.name for entry in sorted(os.scandir(DECKY_PLUGIN_LOG_DIR),key=lambda x: x.stat().st_mtime, reverse=True) if entry.name.endswith(".log")][4:]:
os.unlink(os.path.join(DECKY_PLUGIN_LOG_DIR, x))
except Exception as e:
print(f"Failed to delete old logs: {str(e)}")
logging.basicConfig(filename=DECKY_PLUGIN_LOG,
format='[%(asctime)s][%(levelname)s]: %(message)s',
force=True)
-5
View File
@@ -1,5 +0,0 @@
aiohttp==3.8.1
aiohttp-jinja2==1.5.0
aiohttp_cors==0.7.0
watchdog==2.1.7
certifi==2022.12.7