Compare commits

..

301 Commits

Author SHA1 Message Date
AAGaming 785ef02b7c fix testing our own PRs 2024-06-27 00:38:31 -04:00
WerWolvTranslationBot 5d77577ef5 Translations update from Weblate (#606)
* Translated using Weblate (Dutch)

Currently translated at 100.0% (147 of 147 strings)

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

* Added translation using Weblate (Arabic)

* 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 (Arabic)

Currently translated at 35.3% (52 of 147 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (147 of 147 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (147 of 147 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (147 of 147 strings)

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

* Translated using Weblate (Korean)

Currently translated at 100.0% (147 of 147 strings)

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

* Added translation using Weblate (Turkish)

* 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 (Turkish)

Currently translated at 77.5% (114 of 147 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (147 of 147 strings)

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

* Added translation using Weblate (Swedish)

* 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 (Swedish)

Currently translated at 21.7% (32 of 147 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (147 of 147 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (147 of 147 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 95.2% (140 of 147 strings)

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

---------

Co-authored-by: Danae Dekker <genecyll@gmail.com>
Co-authored-by: d7eeem <almutiri21@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: foXaCe <foxace66@gmail.com>
Co-authored-by: Qihan Xu <imhe6@outlook.com>
Co-authored-by: Benedikt Wagener <bwagener@proton.me>
Co-authored-by: Sungjoon Moon <sumoon@seoulsaram.org>
Co-authored-by: Bahasnyldz <bahasnyldz@gmail.com>
Co-authored-by: david082321 <david082321@yahoo.com.tw>
Co-authored-by: mmfa450 <mmou04faa@gmail.com>
Co-authored-by: kotovasia <super.capt2013.ya@gmail.com>
Co-authored-by: Meiton <michal.salati@gmail.com>
Co-authored-by: tobidashite <mpdeandrade3@gmail.com>
2024-06-03 14:42:07 +02:00
Sims 0ab84cacf3 Add new user agent (#610) 2024-06-01 13:49:08 +02:00
Party Wumpus 3a83fa81de Typing fix
linters are the light of my life
2024-05-30 08:18:54 +01:00
AAGaming 5053a52f32 backport webhelper restart logic from websocket 2024-05-29 21:14:22 -04:00
AAGaming 5bfc53231d shut up ts 2024-05-29 21:04:36 -04:00
AAGaming 2b4e3318ca fix latest beta
VALVEEEEE
2024-05-29 21:01:49 -04:00
WerWolvTranslationBot b84dcd99ad Translations update from Weblate (#588)
* Translated using Weblate (Japanese)

Currently translated at 94.5% (139 of 147 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (147 of 147 strings)

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

---------

Co-authored-by: Tak-attack <tak.bts@gmail.com>
Co-authored-by: Eryk Pawlikowski <eryk5188@gmail.com>
2024-03-17 11:31:02 -07:00
Wayne Heaney 34fb7bb538 Add Plugin.uninstall callback support (#555)
* Add Plugin.uninstall callback support

https://github.com/SteamDeckHomebrew/decky-loader/issues/536

* Remove empty deck.sh
2024-03-13 23:59:22 +01:00
AAGaming 4a7e9a5f3d fix: support new minified class names
bumps decky-frontend-lib to 3.25.0
can't stop us THAT easily :P
2024-03-09 17:21:36 -05:00
TrainDoctor 8e8e6a2bd1 Update bug_report.yml 2024-02-23 16:19:20 -08:00
TrainDoctor 55a95e04b5 Update bug_report.yml 2024-02-23 16:18:31 -08:00
WerWolvTranslationBot 49d1e33c14 Translations update from Weblate (#587)
* Added translation using Weblate (Japanese)

* 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 (Japanese)

Currently translated at 82.7% (115 of 139 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 89.2% (124 of 139 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 100.0% (139 of 139 strings)

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

* Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (139 of 139 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (139 of 139 strings)

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

* Translated using Weblate (French)

Currently translated at 91.3% (127 of 139 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (139 of 139 strings)

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

* Translated using Weblate (English)

Currently translated at 100.0% (147 of 147 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (147 of 147 strings)

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

---------

Co-authored-by: Tak-attack <tak.bts@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Fábio Oliveira <fabio.an.oliveira@gmail.com>
Co-authored-by: Andrew Moore <andrewm.finewolf@gmail.com>
Co-authored-by: Danae Dekker <genecyll@gmail.com>
Co-authored-by: Marco Rodolfi <marco.rodolfi@tuta.io>
2024-02-22 20:27:39 +01:00
Party Wumpus 922d0c4153 Appease prettier
i must have done a great deal of harm in a past life to deserve this mistreatment by formatting tools. why do they hate me.
2024-02-15 12:15:05 +00:00
Party Wumpus ecf480059b fix finding qam root node for feb 14th beta 2024-02-15 12:09:21 +00:00
Andrew Moore 7d6b8805df [Feature] Freeze updates for devs (#582) 2024-02-14 20:45:55 -08:00
eXhumer 0dce3a8cbe Get plugin name for development ZIP during installation (#578)
* fix: get plugin name for dev builds from ZIP (SteamDeckHomebrew/decky-loader#527)

Signed-off-by: eXhumer <exhumer1@protonmail.com>
2024-02-14 20:17:26 -08:00
Party Wumpus 9503d5cee0 Testing PRs from within decky (#496)
* git no work so manually uploading files :(

* argh i wish git was working

* ok next time i'll make git work

* Update updater.py

* git please work next time this took ages without you

* fix me locales

* Update updater.py

* Update en-US.json

* Update updater.py

* Update updater.py

* i wish my python LSP stuff was working

* fix it

* Update updater.py

* Update updater.py

* Only show testing branch as an option if it is already selected

* Initial implementation for fetching the open PRs. Still need testing and a token to complete this.

* Wrong filter capitalization

* Fix a couple of typos in the python backend updater.

* Fix typos pt 3

* This should be the last one

* Prepend the PR version number with PR- to make it clearer that's the PR number.

* Update prettier to the latest version otherwise it will never be happy with the formatting.

* fix merge mistake

* fix pyright errors & type hint most new code

* fix strict pyright errors...

* not sure why my local linter didn't catch this

* Reimplement the logic between PR and artifact build to limit API calls

* Fix pyright errors

* use nightly.link for downloads

* remove accidental dollar sign

* fix various logical errors. the code actually works now.

* set branch to testing when user downloads a testing version

---------

Co-authored-by: Marco Rodolfi <marco.rodolfi@tuta.io>
2024-02-14 18:32:58 -08:00
Jozen Blue Martinez 435dfa7884 fix(filepicker_ls): use case insensitive matching for file exts (#585) 2024-02-10 11:34:16 -08:00
Party Wumpus 2500b748ce Revert "Call plugin unload function after stopping event loop (#539)" (#584)
This reverts commit 39f4f2870b , because functions (seemingly) don't run after the event loop closes, so the unload function is never actually run.
2024-02-09 20:33:47 +00:00
Party Wumpus fd4ed811be Refactor plugin store and add sorting by downloads and release date (#547)
* untested first commit

* fix types & names

* comment out built in sorting for now

* rerun search when sort changes

* fix ts complaints

* use prettier

* stop switch-case fall through

* move spinner

* use locale instead of hardcoded string

* fix typo

* add sorting by downloads & try using the data field in the dropdown for data

* fix typing error

* fix asc/desc in dropdown

* fix asc/desc again. asc = smaller one go first aaaaa

* I don't think i know what ascending means maybe

* use props instead of children, like a normal component
2024-02-07 17:38:08 +00:00
Party Wumpus 3e4c255c5b Specify catthehacker/ubuntu:act-22.04 as container for act
Fixes an issue where act wouldn't use the correct container and so couldn't find a compatible python version, so it would fail to build.
2024-02-06 19:49:57 +00:00
AAGaming 62e3128d64 fix: bump dfl to fix error on latest steam beta 2024-02-03 00:33:39 -05:00
AAGaming 7f2caa3ea9 fix: use findInReactTree to find correct errorboundary for toaster
fixes toaster error on latest beta
2024-02-03 00:33:00 -05:00
AAGaming 6b4a56c7dc fix the tasks 2024-02-03 00:32:32 -05:00
WerWolvTranslationBot 647f3fe8ed Translations update from Weblate (#580)
* Added translation using Weblate (Japanese)

* 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 (Japanese)

Currently translated at 82.7% (115 of 139 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 89.2% (124 of 139 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 100.0% (139 of 139 strings)

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

* Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (139 of 139 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (139 of 139 strings)

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

---------

Co-authored-by: Tak-attack <tak.bts@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Fábio Oliveira <fabio.an.oliveira@gmail.com>
2024-01-25 16:03:02 -08:00
Marco Rodolfi 3146ebf85f [Bugfix] Toaster changed name again (#581)
Add another name placeholder for getting the toaster out of the HTML tree. Thanks to @eXhumer for the fix.
2024-01-25 17:21:11 +01:00
dependabot[bot] 9295e4b038 Bump aiohttp from 3.8.5 to 3.9.0 in /backend (#577) 2024-01-23 19:49:13 +00:00
Beebles f9a07da3cc fix: Fix on Chromium 109 beta (#576)
* Add new user agent to do not close tabs list

* fix: bump DFL to fix chromium 109 beta

---------

Co-authored-by: Sims <38142618+suchmememanyskill@users.noreply.github.com>
2024-01-19 18:54:56 -08:00
dependabot[bot] 12a99b8b06 Bump tj-actions/changed-files to 41.0.0 in /.github/workflows (#575)
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 35.6.3 to 41.0.0.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/changed-files/compare/v35.6.3...v41.0.0)

---
updated-dependencies:
- dependency-name: tj-actions/changed-files
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-02 16:42:39 -08:00
WerWolvTranslationBot e3d72b6082 Translations update from Weblate (#553)
* Added translation using Weblate (Japanese)

* 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 (Japanese)

Currently translated at 82.7% (115 of 139 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 89.2% (124 of 139 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 100.0% (139 of 139 strings)

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

* Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (139 of 139 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (139 of 139 strings)

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

---------

Co-authored-by: Tak-attack <tak.bts@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Fábio Oliveira <fabio.an.oliveira@gmail.com>
2023-12-15 18:12:57 -08:00
Jan 39f4f2870b Call plugin unload function after stopping event loop (#539)
This can prevent race conditions where unload is clearing data but main is still working with it
2023-12-15 18:07:54 -08:00
AAGaming 3489fd7d69 fix(developer): add back valve internal on beta
look i was tired when writing yesterday's fix okay
2023-12-13 22:06:22 -05:00
AAGaming e21a5d5890 fix: idiotic formatting error i should have noticed 2023-12-12 22:23:07 -05:00
AAGaming 80a00a0d35 fix: Adjust tabs and toaster hooks to work on react 18, also half-fix Valve Internal 2023-12-12 22:21:25 -05:00
Party Wumpus 91186da979 bump dfl 2023-11-24 15:08:29 -08:00
Jan 7c3ae9b62b replace chmod implementation with os.chmod (#541) 2023-11-11 20:56:32 +00:00
Jan 75ad98a7b2 Check if Linux service is running before trying to start or stop it (#540)
this prevents needless prompts opening up
2023-11-11 20:50:23 +00:00
dependabot[bot] 479a16c655 Bump aiohttp from 3.8.4 to 3.8.5 in /backend (#558) 2023-11-10 21:01:48 +00:00
Party Wumpus 8f26fdec2d Count the number of installs for each plugin (#557) 2023-11-10 17:19:01 +00:00
AAGaming 29d651bed6 fix: get rid of title view jank on latest beta 2023-11-09 15:35:32 -05:00
marios8543 44e6f03b06 Fix logging.handlers import and improve plugin modules 2023-10-31 17:04:48 +02:00
marios8543 d00506d141 fix decky imports from plugins 2023-10-27 00:42:10 +03:00
marios8543 ffe9cd8afe revert decky_plugin pyinstaller to previous version and fix sys path 2023-10-26 23:25:00 +03:00
TrainDoctor a7669799bc Merge aa/type-cleanup-py (work by marios, aa, wolv) 2023-10-25 19:47:33 -07:00
TrainDoctor dacd2c19eb Remove extremely outdated setup scripts. 2023-10-25 19:09:14 -07: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
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
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
WerWolvTranslationBot 66bcdfd84e Translations update from Weblate (#488)
* 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 (Albanian)

Currently translated at 36.5% (49 of 134 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 85.0% (114 of 134 strings)

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

---------

Co-authored-by: Marco Rodolfi <marco.rodolfi@tuta.io>
2023-06-19 17:44:30 +02:00
WerWolvTranslationBot 8494f2ec3e Translations update from Weblate (#487)
* 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 (French)

Currently translated at 71.6% (96 of 134 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 88.0% (118 of 134 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 88.0% (118 of 134 strings)

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

* Translated using Weblate (Albanian)

Currently translated at 35.8% (48 of 134 strings)

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

* Translated using Weblate (Albanian)

Currently translated at 35.8% (48 of 134 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 85.0% (114 of 134 strings)

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

* Translated using Weblate (Greek)

Currently translated at 70.8% (95 of 134 strings)

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

* Translated using Weblate (Greek)

Currently translated at 70.8% (95 of 134 strings)

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

* Translated using Weblate (German)

Currently translated at 73.1% (98 of 134 strings)

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

* Translated using Weblate (Czech)

Currently translated at 88.0% (118 of 134 strings)

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

* Translated using Weblate (Korean)

Currently translated at 88.0% (118 of 134 strings)

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

* Translated using Weblate (Portuguese (Portugal))

Currently translated at 88.0% (118 of 134 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 88.0% (118 of 134 strings)

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

* Translated using Weblate (Russian)

Currently translated at 0.0% (0 of 134 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 85.8% (115 of 134 strings)

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

* 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/

---------

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Marco Rodolfi <marco.rodolfi@tuta.io>
2023-06-19 17:34:07 +02:00
WerWolvTranslationBot 52f25708ad Translations update from Weblate (#486)
* Added translation using Weblate (Czech)

* 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% (118 of 118 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (118 of 118 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (118 of 118 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% (118 of 118 strings)

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

* Added translation using Weblate (Korean)

* 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/

* Added translation using Weblate (Portuguese)

* 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/

* Added translation using Weblate (Portuguese (Brazil))

* 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 (French)

Currently translated at 81.3% (96 of 118 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (118 of 118 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (118 of 118 strings)

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

* Translated using Weblate (Korean)

Currently translated at 61.0% (72 of 118 strings)

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

* Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (118 of 118 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (118 of 118 strings)

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

* Added translation using Weblate (Russian)

* 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 (Korean)

Currently translated at 100.0% (118 of 118 strings)

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

* Added translation using Weblate (Ukrainian)

* 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/

---------

Co-authored-by: Meiton <michal.salati@gmail.com>
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: david082321 <david082321@yahoo.com.tw>
Co-authored-by: Sungjoon Moon <sumoon@seoulsaram.org>
Co-authored-by: Samuel Medalha <smedalha@protonmail.com>
Co-authored-by: Paulo Victor de Lima Sfair Alvares <pvsfair@gmail.com>
Co-authored-by: Libo Chen <clb729@gmail.com>
Co-authored-by: Ivan Poletsky <wiperus55@gmail.com>
Co-authored-by: Denys Dovhan <denysdovhan@gmail.com>
2023-06-19 17:25:04 +02:00
Marco Rodolfi 24b02114cb Fix weblate crap 2023-06-19 17:22:13 +02:00
Denys Dovhan 651215723d Translated using Weblate (Ukrainian)
Currently translated at 97.4% (115 of 118 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/uk/
2023-06-19 17:21:17 +02:00
Weblate ca9831be05 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-06-19 17:21:17 +02:00
Weblate f7fd7b712f Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-06-19 17:21:17 +02:00
Weblate 3859c0e483 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-06-19 17:21:17 +02:00
Weblate 2db92536fd Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-06-19 17:21:17 +02:00
Weblate a6ffc8f04d Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-06-19 17:21:17 +02:00
Weblate 4df8cb5026 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-06-19 17:21:17 +02:00
Weblate a86d473280 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-06-19 17:21:17 +02:00
Denys Dovhan 4d226b00fc Added translation using Weblate (Ukrainian) 2023-06-19 17:21:17 +02:00
Weblate 8b16b156ed Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-06-19 17:21:17 +02:00
suchmememanyskill 4029a43637 Fix uninstalling plugins (#485)
* Fix bad debug print statement in uninstall plugin

* Safely remove element from hidden plugin/plugin order list
2023-06-19 06:24:24 -07:00
Marco Rodolfi 57f4555350 [Feature] File picker improvements (#454)
* First iteration for internationalization of the loader

* First iteration for internationalization of the loader

* Cleanup node mess

* Cleanup node mess pt2

* Additional touches

* Latest decky changed merged into i18n and updated translation.

* Styling fixes

* Initial backend hosting implementation

* Added correct url path of the loopback server.

* Added correct url path of the loopback server.

* Some better namespaced text.

* Added whitelist for locales path.

* Refactor languages and fix hooks logic bugs.

* Small typo in language translation structure.

* Working backend, automatically swtich languages with steam and language fixes.

* Fix to languages

* Key fixes

* Additional language fixes.

* Additional json changes

* Final text revision and added a vscode tasks to automatically extract text from code.

* Typo in the middleware

* Remove unused imports

* Cleanup whitespaces.

* Import changes

* Revert "Import changes"

This reverts commit 8e8231950f.

* Update index.d.ts

* Clean up unused imports

* Delete pnpm-lock.yaml

* Update rollup.config.js

* Update PluginInstallModal.tsx

* Update index.tsx

* Update plugin-loader.tsx

* Update plugin-loader.tsx

* Revert "Delete pnpm-lock.yaml"

This reverts commit 3a39f36f21.

* Additional strings reworks.

* Fixes for issues coming from github merge.

* Fixes for master

* Styling fixes

* Styling pt2

* Missed a few strings in master,

* Styling fixes

* Additional master merge fixes.

* Final cleanup and adaptation to master.

* Final empty language cleanup and few string added

* Small changes to italian translation

* Disabled translation on a few components inside plugin-loader for missing react hooks.

* Fixed passing tag to translation.

* Disable debug output for reducing console spam.

* Return correct content type

* Small italian language change

* Added support for country code

* Fixed missing translation for uninstall popup.

* Fix class name shenanigans for  toast notification

* Update dependencies

* Fixed github workflow to include the new locales folder

* Update dependencies to latest version (unless it's React) and fixed the new small errors that cropped up

* Missed a file name change

* Updated dev dependencies to latest version

* Missed a few dev dependencies

* Revert "Update dependencies to latest version (unless it's React) and fixed the new small errors that cropped up"

Messed up merge with a different main branch

* Messed up deletion of rollup config.

* Fix broken pnpm lock file

* Missed a localized string during the merge

* Fixed a parameter mistake in the uninstall text parameter

* Fix pnpm random issues

* Small italian language tweaks

* Fix wrong parameter passed to the uninstall function call

* Another fix on a wrong function parameter

* Additional translation text on the store and branch selection channels

* Changed the default type passed to map to being able to index the two arrays.

* Reverted and reworked the last changes

* Distinguish events in UI for installing vs reinstalling plugins

* Additional fixes for reinstall prompt

* Revert the use of intevalPlural since the parser doesn't seem to support that.

* Missed a routing path in the backend

* Small bugfixes

* Small fixes

* Correctly adding the parameter to the request headers.

* Refactoring of the UI popup modal

* Fix pnpm shenanigans

* Final fixes for the install UI localization

* Clean up unnedeed backend code

* Small rework on text selection.

* Cleaned up parser configuration

* Removed extracttext dependency to pnpmsetup

* Merged translation and cleaned up parser

* Fixed JSON structure after manual merge.

* Added translation to the file picker

* First iteration for merging the new filepicker.

* Revert changes to PluginInstallModal

* Reworked the text modal for the final time

* Missed the proper linted text

* Missed the backend change

* Final branch cleanup

* First iteration for porting the new file picker

* Hotfix for i18n where the detector was overriding localStorage

* Please, pnpm, cooperate

* Small fix regarding the backend getting hammered when switching to not supported languages plus a small english typo

* Initial working upstream iteration for file picker

* Typo on translation variable

* File picker final improvements

* Stylistic fixes and fix on wrong bool passed to fp

* Fixup merge from main

* Other merge errors fixed

* Minor cleanups

* Fixed missing padding under text label extension

* Implement pagination backend side

* First draft for filtering backend side

* Implemented matching on file names.

* Fix for unable to order per size on folders.

* Hard checking a return value

* Added a missing import.

* Implemented show more as a frontend button

* Whoops, python typo

* Fixed python backend

* Rendering bug fix and small qol improvement

* Added missing parameter to openFilePicker call

* Fixed path on windows and unknown error on wrong path

* Small backend fixes

* Extension fix

* Simplified extension logic

* Less string conversions.

* Optimize backend code and removed additional components.

* Take correctly into account the max value

The button will now respect the actual maximum desired number of entries.

* Bugfix for ordering logic and ignore cases during sorting

* Regex call was missing an argument

* Fixed issues with filtering extensions

* Rollback testing changes

* Minor cleanup and attempt at fixing the not updating multimodal.

* Cleanup variable types.

* Mantains the same api format from the original source code.

* Removing hardcoded paths in the code

* Additional fixes for resolving the user path

* Cleanup useless modifications

* Final fixes for avoid path hardcoding

* Update lockfile and i18next version
2023-06-19 06:23:27 -07:00
WerWolvTranslationBot bd87cc852b Translations update from Weblate (#481)
* Added translation using Weblate (Czech)

* 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% (118 of 118 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (118 of 118 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (118 of 118 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% (118 of 118 strings)

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

* Added translation using Weblate (Korean)

* 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/

* Added translation using Weblate (Portuguese)

* 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/

* Added translation using Weblate (Portuguese (Brazil))

* 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 (French)

Currently translated at 81.3% (96 of 118 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (118 of 118 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (118 of 118 strings)

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

* Translated using Weblate (Korean)

Currently translated at 61.0% (72 of 118 strings)

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

* Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (118 of 118 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (118 of 118 strings)

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

* Added translation using Weblate (Russian)

* 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 (Korean)

Currently translated at 100.0% (118 of 118 strings)

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

---------

Co-authored-by: Meiton <michal.salati@gmail.com>
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: david082321 <david082321@yahoo.com.tw>
Co-authored-by: Sungjoon Moon <sumoon@seoulsaram.org>
Co-authored-by: Samuel Medalha <smedalha@protonmail.com>
Co-authored-by: Paulo Victor de Lima Sfair Alvares <pvsfair@gmail.com>
Co-authored-by: Libo Chen <clb729@gmail.com>
Co-authored-by: Ivan Poletsky <wiperus55@gmail.com>
2023-06-19 09:55:02 +02:00
AAGaming 30d7c9bb81 fix: fix blank plugins
this is why i shouldn't program at night
2023-06-18 17:27:23 -04:00
AAGaming 890599c7bb chore: remove useless import 2023-06-18 17:26:42 -04:00
Marco Rodolfi b8e48d2146 WIP on main: 60353df Merge remote-tracking branch 'weblate/main' 2023-06-16 14:46:25 +02:00
Marco Rodolfi da1db5a053 index on main: 60353df Merge remote-tracking branch 'weblate/main' 2023-06-16 14:46:25 +02:00
Marco Rodolfi 60353df7ed Merge remote-tracking branch 'weblate/main' 2023-06-16 14:46:09 +02:00
Party Wumpus e8dfe5a87d When decky is uncertain of branch, set the setting to match the guess (#480)
* If branch setting is missing, set it using the 'guess' from backend

* Make the frontend default to stable branch like the backend
2023-06-15 05:53:02 -07:00
WerWolvTranslationBot d0b7d1a4a6 Translations update from Weblate (#477)
* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (100 of 100 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (100 of 100 strings)

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

* Translated using Weblate (Italian)

Currently translated at 91.8% (102 of 111 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (111 of 111 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (114 of 114 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (114 of 114 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (114 of 114 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (118 of 118 strings)

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

---------

Co-authored-by: david082321 <david082321@yahoo.com.tw>
Co-authored-by: Jonas Dellinger <jonas@dellinger.dev>
Co-authored-by: Marco Rodolfi <marco.rodolfi@tuta.io>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Sean <zhangshuyan@fuji.waseda.jp>
Co-authored-by: Avery <aveeryy@protonmail.com>
2023-06-08 16:49:07 +02:00
Marco Rodolfi 13b6ed5ad9 Translated using Weblate (Italian)
Currently translated at 100.0% (118 of 118 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/it/
2023-06-08 14:46:26 +00:00
Weblate 89bbbf6fe4 Merge remote-tracking branch 'origin/main' 2023-06-08 14:40:08 +00:00
AAGaming 9a05c228a0 fix dumb crash when updating decky while a plugin that uses BrowserView is running 2023-06-07 18:03:41 -04:00
Party Wumpus 00d9b03322 add firefox .download clarification in readme 2023-06-07 16:56:47 +01:00
Jonas Dellinger 47bc910a84 Add functionality to hide plugins from quick access menu (#468) 2023-06-06 22:35:05 -07:00
Weblate fdc556edee Merge remote-tracking branch 'origin/main' 2023-06-04 15:09:46 +00:00
WerWolvTranslationBot 1c6270ccd6 Translations update from Weblate (#476)
* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (100 of 100 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (100 of 100 strings)

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

* Translated using Weblate (Italian)

Currently translated at 91.8% (102 of 111 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (111 of 111 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (114 of 114 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (114 of 114 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (114 of 114 strings)

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

---------

Co-authored-by: david082321 <david082321@yahoo.com.tw>
Co-authored-by: Jonas Dellinger <jonas@dellinger.dev>
Co-authored-by: Marco Rodolfi <marco.rodolfi@tuta.io>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Sean <zhangshuyan@fuji.waseda.jp>
Co-authored-by: Avery <aveeryy@protonmail.com>
2023-06-04 17:09:14 +02:00
Weblate f87e794c3f Merge remote-tracking branch 'origin/main' 2023-06-04 15:08:29 +00:00
Avery 8f17f2b0fe Translated using Weblate (Spanish)
Currently translated at 100.0% (114 of 114 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/es/
2023-06-04 17:08:01 +02:00
Sean 81b601c0e7 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (114 of 114 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/zh_Hans/
2023-06-04 17:08:01 +02:00
Marco Rodolfi 83e8c89c97 Translated using Weblate (Italian)
Currently translated at 100.0% (114 of 114 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/it/
2023-06-04 17:08:01 +02:00
WerWolvTranslationBot ca107feb25 Translations update from Weblate (#475)
* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (100 of 100 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (100 of 100 strings)

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

* Translated using Weblate (Italian)

Currently translated at 91.8% (102 of 111 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (111 of 111 strings)

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

---------

Co-authored-by: david082321 <david082321@yahoo.com.tw>
Co-authored-by: Jonas Dellinger <jonas@dellinger.dev>
Co-authored-by: Marco Rodolfi <marco.rodolfi@tuta.io>
Co-authored-by: Weblate <noreply@weblate.org>
2023-06-04 11:06:53 +02:00
Avery 56719df827 Translated using Weblate (Spanish)
Currently translated at 100.0% (114 of 114 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/es/
2023-06-03 16:59:04 +00:00
Sean 3c11bb71f6 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (114 of 114 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/zh_Hans/
2023-06-03 16:59:04 +00:00
Marco Rodolfi 413cec9244 Translated using Weblate (Italian)
Currently translated at 100.0% (114 of 114 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/it/
2023-06-03 16:59:04 +00:00
AAGaming e5277190ed Revert "Refactor TabsHook (#458)"
This reverts commit b27b625921.

These changes broke Decky's QAM injection when the lock screen is enabled and need to be revised
2023-06-03 12:45:09 -04:00
Weblate 935de1ad0c Merge remote-tracking branch 'origin/main' 2023-06-02 10:07:20 +00:00
suchmememanyskill 2e8e0fc7c1 Plugin backend reload (#463)
Co-authored-by: beebls <102569435+beebls@users.noreply.github.com>
2023-06-01 18:44:55 -07:00
Party Wumpus 8049417e03 Attempt to appease the linter
I think the first navigation being on one line looks nicer, but it's over the 120 character limit :(
2023-06-02 00:26:08 +01:00
Witherking25 f4c0a8b5aa add cef console button to developer settings (#441)
* add cef console button

* Small fix: handle missing localization in backend plus a small typo in the english language (#443)

* Hotfix for i18n where the detector was overriding localStorage

* Please, pnpm, cooperate

* Small fix regarding the backend getting hammered when switching to not supported languages plus a small english typo

* Add a get_tab_id function to utilities

* Go straight to SharedJSContext into console button

* clean up some log statements, and some extra parentheses

---------

Co-authored-by: Marco Rodolfi <marco.rodolfi@tuta.io>
Co-authored-by: Party Wumpus <48649272+PartyWumpus@users.noreply.github.com>
2023-06-02 00:01:21 +01:00
WerWolvTranslationBot d3584a9931 Translations update from Weblate (#469)
* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (100 of 100 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (100 of 100 strings)

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

* Translated using Weblate (Italian)

Currently translated at 91.8% (102 of 111 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (111 of 111 strings)

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

---------

Co-authored-by: david082321 <david082321@yahoo.com.tw>
Co-authored-by: Jonas Dellinger <jonas@dellinger.dev>
Co-authored-by: Marco Rodolfi <marco.rodolfi@tuta.io>
2023-05-31 13:36:02 +02:00
Marco Rodolfi 9542708c92 Translated using Weblate (Italian)
Currently translated at 100.0% (111 of 111 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/it/
2023-05-31 06:53:52 +00:00
Marco Rodolfi b3c363c89f Translated using Weblate (Italian)
Currently translated at 91.8% (102 of 111 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/it/
2023-05-31 06:53:52 +00:00
Jonas Dellinger 5f5e8ad5d7 Translated using Weblate (German)
Currently translated at 100.0% (100 of 100 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/de/
2023-05-31 06:53:52 +00:00
david082321 6b6de2fcd5 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (100 of 100 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/zh_Hant/
2023-05-31 06:53:52 +00:00
MIkhail Kozlov b27b625921 Refactor TabsHook (#458) 2023-05-30 23:53:48 -07:00
Jonas Dellinger c5229c6a62 adjust some small store stylings (#471) 2023-05-30 16:35:42 -07:00
Marco Rodolfi c631d40aa3 Missed a toaster for the react tools 2023-05-30 19:32:41 +02:00
Jonas Dellinger d21b221575 quick fix: overwrite plugin list marginTop to be always 0 2023-05-29 18:40:17 +02:00
Jonas Dellinger 010feddf36 Add update all button to plugin list (#466) 2023-05-29 09:29:36 -07:00
WerWolvTranslationBot 5114bb5711 Translations update from Weblate (#467)
* Translated using Weblate (Italian)

Currently translated at 100.0% (100 of 100 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (100 of 100 strings)

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

---------

Co-authored-by: Marco Rodolfi <marco.rodolfi@tuta.io>
Co-authored-by: Jonas Dellinger <jonas@dellinger.dev>
2023-05-28 14:57:14 +02:00
Marco Rodolfi 4e7001efd6 Merge remote-tracking branch 'weblate/main' 2023-05-28 13:48:22 +02:00
Weblate 0c9d90df10 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-28 13:46:51 +02:00
Weblate 09963ef4bb Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-28 13:46:14 +02:00
Weblate 2f24454b1e Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-28 13:46:14 +02:00
Weblate 177ed35522 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-28 13:46:14 +02:00
Weblate c5aeb018db Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-28 13:46:14 +02:00
Weblate 671120a517 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-28 13:46:14 +02:00
Weblate f306239b5f Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-28 13:46:11 +02:00
WerWolvTranslationBot 44859be657 Translations update from Weblate (#465)
* Update translation files

Updated by "Remove blank strings" hook in Weblate.

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

* Added translation using Weblate (German)

* 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/

---------

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: DarkSide1305 <darkside1305@web.de>
2023-05-28 11:06:03 +02:00
Weblate ee4c706529 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-27 18:03:47 +00:00
Weblate 5ca3015609 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-27 18:03:47 +00:00
Weblate 8b05fb6943 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-27 18:03:47 +00:00
Weblate e4ebbed477 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-27 18:03:47 +00:00
Weblate d13536955e Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-27 18:03:47 +00:00
Weblate 37462548b3 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-27 18:03:47 +00:00
Weblate 74d06aaca6 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-27 18:03:47 +00:00
DarkSide1305 1934d12aac Added translation using Weblate (German) 2023-05-27 18:03:47 +00:00
Weblate 70bd5adad3 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-27 18:03:47 +00:00
Marco Rodolfi f9d5c4ba2a Merge branch 'main' of github.com:SteamDeckHomebrew/decky-loader 2023-05-27 20:03:25 +02:00
Marco Rodolfi cc5e6ac24d Small code cleanup. 2023-05-27 20:02:47 +02:00
Party Wumpus d44ce0f74b Nouns don't have gender in english 2023-05-27 18:44:03 +01:00
Marco Rodolfi 687f7bf5db Simplified the translation error. 2023-05-27 19:23:15 +02:00
Marco Rodolfi 9cd219fab7 Simplified error message. 2023-05-27 14:26:25 +02:00
Marco Rodolfi 6e6f8caca8 Unified translation classes, fixed missing toaster translation and improved the error styling report. 2023-05-27 13:55:26 +02:00
Marco Rodolfi 3a83062438 Simplified inlining and cleaning up unused translations 2023-05-26 14:08:09 +02:00
Marco Rodolfi dfdad14ede Fix: actually force the cog icon to stay inline 2023-05-26 13:29:47 +02:00
Marco Rodolfi 852897c502 Fixed untranslated error message and added french suffix country code. 2023-05-26 12:14:40 +02:00
Marco Rodolfi 9e502f85fa Missed cleaning up new languages. 2023-05-25 19:45:18 +02:00
Marco Rodolfi 368a1044da Fixed file names for language strings. 2023-05-25 17:02:36 +02:00
Marco Rodolfi 578b8b3ee2 Merge remote-tracking branch 'weblate/main' 2023-05-25 16:58:59 +02:00
Marco Rodolfi ede1067bb3 [Hotfix] Wrong key name interpreted as plural (#459) 2023-05-20 15:07:59 -07:00
judgmentkazzy00 bec1c61366 Translated using Weblate (Spanish)
Currently translated at 50.0% (50 of 100 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/es/
2023-05-20 22:07:13 +00:00
david082321 5d2cc1c133 Translated using Weblate (Chinese (Traditional))
Currently translated at 99.0% (99 of 100 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/zh_Hant/
2023-05-20 22:07:13 +00:00
Sean ef97921e30 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (100 of 100 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/zh_Hans/
2023-05-20 22:07:13 +00:00
Marco Rodolfi 0009a53800 Translated using Weblate (Italian)
Currently translated at 100.0% (100 of 100 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/it/
2023-05-20 22:07:13 +00:00
Marco Rodolfi 3eff60e8aa Translated using Weblate (English)
Currently translated at 99.0% (99 of 100 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/en/
2023-05-20 22:07:13 +00:00
marios cbfa548f98 Translated using Weblate (Greek)
Currently translated at 100.0% (100 of 100 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/el/
2023-05-20 22:07:13 +00:00
EMERALD 3f2d54ddbd Translated using Weblate (Spanish)
Currently translated at 7.0% (7 of 100 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/es/
2023-05-20 22:07:13 +00:00
Adem Odza 0ecc9bf579 Translated using Weblate (Albanian)
Currently translated at 51.0% (51 of 100 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/sq/
2023-05-20 22:07:13 +00:00
Weblate 88b9984b0f Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate d6c025da1c Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate 7ba864136f Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate d5dda39add Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate 320f392ad3 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate 47388b1083 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
marios c1edb9f2e9 Added translation using Weblate (Greek) 2023-05-20 22:07:13 +00:00
Weblate d184e1c4af Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate 88a4c0c361 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate ff1f902c91 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate c1215072a9 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate ad3fc990f5 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate d3038efd45 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
EMERALD 92b953a22d Added translation using Weblate (Spanish) 2023-05-20 22:07:13 +00:00
Weblate 9accb676e6 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate ba9f12ffe7 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate a4f8f5fcf5 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate 4400226c2d Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate 6e6ef81e66 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate c4a4249440 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Adem Odza a4e02b8201 Added translation using Weblate (Albanian) 2023-05-20 22:07:13 +00:00
Weblate ea56b03b38 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Anonymous 3fe1d44515 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (100 of 100 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/zh_Hant/
2023-05-20 22:07:13 +00:00
Anonymous 3932c69ad6 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (100 of 100 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/zh_Hans/
2023-05-20 22:07:13 +00:00
Weblate 1b8fe4f82f Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Anonymous 67dc7e7893 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (100 of 100 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/zh_Hant/
2023-05-20 22:07:13 +00:00
Anonymous c14f7043bc Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (100 of 100 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/zh_Hans/
2023-05-20 22:07:13 +00:00
Anonymous 6d47a2111b Translated using Weblate (French)
Currently translated at 100.0% (100 of 100 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/fr/
2023-05-20 22:07:13 +00:00
Anonymous aceeaeee07 Translated using Weblate (Italian)
Currently translated at 100.0% (100 of 100 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/it/
2023-05-20 22:07:13 +00:00
AAGaming a77ad33ea0 Deleted translation using Weblate (Greek) 2023-05-20 22:07:13 +00:00
Marco Rodolfi e2691592ba Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (100 of 100 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/zh_Hant/
2023-05-20 22:07:13 +00:00
Marco Rodolfi 3d8629b803 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (100 of 100 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/zh_Hans/
2023-05-20 22:07:13 +00:00
elliotfontaine 4c5468ae97 Translated using Weblate (French)
Currently translated at 100.0% (100 of 100 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/fr/
2023-05-20 22:07:13 +00:00
Weblate 81cb3dd0ec Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate f94866f473 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate d4a4d2287a Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate 4cfeb8ef3e Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate 826e014456 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate 9fb211316d Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate ba01ad6e13 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Marco Rodolfi d0b897ff7f Added translation using Weblate (Chinese (Traditional)) 2023-05-20 22:07:13 +00:00
Weblate dd3d313517 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate 83faf6697b Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate 7bc2187c8c Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate 8a61ecc71a Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate 579d52982a Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate d895b7c0ef Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate a3f6004fd9 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Marco Rodolfi 73c5a890ce Added translation using Weblate (Chinese (Simplified)) 2023-05-20 22:07:13 +00:00
elliotfontaine 0cb0fb7165 Translated using Weblate (French)
Currently translated at 4.0% (4 of 100 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/fr/
2023-05-20 22:07:13 +00:00
Weblate 5376478b2d Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate c74cfc51e7 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate 6b10c87648 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate 8ab0b34a2e Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate b5aeee505a Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate d7f343aac4 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
elliotfontaine 1eacdc4bce Added translation using Weblate (French) 2023-05-20 22:07:13 +00:00
Weblate a2e2335dd9 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate 6882de6027 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate fccadefe47 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate 70a4b26984 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate 39206b782e Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
Weblate 3ca625d838 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/
2023-05-20 22:07:13 +00:00
marios 1042655eb9 Added translation using Weblate (Greek) 2023-05-20 22:07:13 +00:00
suchmememanyskill cad2babbca Add env var to not replace systemd service file (#462) 2023-05-20 15:07:09 -07:00
Party Wumpus dbd1ea9543 move pull request template 2023-05-18 18:45:15 +01:00
Party Wumpus 313f6db5fa Update and rename pull_request.md to pull_request_template.md 2023-05-18 18:43:59 +01:00
Party Wumpus 3b58001abe github pull requests don't do yml :( 2023-05-18 18:36:33 +01:00
Party Wumpus bf99bce579 Create new_feature.yml 2023-05-18 18:29:52 +01:00
Party Wumpus 9c02ccc537 Fix formatting of translation badge 2023-05-18 11:23:47 +01:00
AAGaming fedbfcb041 add Weblate badge
yeah the formatting is different I'll fix it later because GitHub mobile doesn't let me
2023-05-18 06:18:06 -04:00
Marco Rodolfi 3c52b33e18 [Hotfix] Windows lang fix (#457)
* Hotfix for i18n where the detector was overriding localStorage

* Please, pnpm, cooperate

* Small fix regarding the backend getting hammered when switching to not supported languages plus a small english typo

* Typo on translation variable

* Hotfix: Fix for missing locale data on windows
2023-05-18 03:03:19 -07:00
AAGaming d99f332523 Initial implementation of global DFL instance (#451) 2023-05-11 22:02:04 -04:00
Party Wumpus 0c83c9a2b5 Bump DFL to 3.20.7 (#449) 2023-05-10 22:12:27 +01:00
Marco Rodolfi 6b14f08d59 Hotfix: wrong variable name for English translation (#444) 2023-05-09 15:40:57 -07:00
Marco Rodolfi 089e6b086c Small fix: handle missing localization in backend plus a small typo in the english language (#443)
* Hotfix for i18n where the detector was overriding localStorage

* Please, pnpm, cooperate

* Small fix regarding the backend getting hammered when switching to not supported languages plus a small english typo
2023-05-04 16:46:00 +01:00
Marco Rodolfi 08d5c942a4 Hotfix for i18n where the detector was overriding localStorage (#439)
* Hotfix for i18n where the detector was overriding localStorage

* Please, pnpm, cooperate
2023-05-03 21:51:18 +01:00
Marco Rodolfi 35e7c80835 [Feature] Implement internazionalization for Decky Loader (#361)
* First iteration for internationalization of the loader

* First iteration for internationalization of the loader

* Cleanup node mess

* Cleanup node mess pt2

* Additional touches

* Latest decky changed merged into i18n and updated translation.

* Styling fixes

* Initial backend hosting implementation

* Added correct url path of the loopback server.

* Added correct url path of the loopback server.

* Some better namespaced text.

* Added whitelist for locales path.

* Refactor languages and fix hooks logic bugs.

* Small typo in language translation structure.

* Working backend, automatically swtich languages with steam and language fixes.

* Fix to languages

* Key fixes

* Additional language fixes.

* Additional json changes

* Final text revision and added a vscode tasks to automatically extract text from code.

* Typo in the middleware

* Remove unused imports

* Cleanup whitespaces.

* Import changes

* Revert "Import changes"

This reverts commit 8e8231950f.

* Update index.d.ts

* Clean up unused imports

* Delete pnpm-lock.yaml

* Update rollup.config.js

* Update PluginInstallModal.tsx

* Update index.tsx

* Update plugin-loader.tsx

* Update plugin-loader.tsx

* Revert "Delete pnpm-lock.yaml"

This reverts commit 3a39f36f21.

* Additional strings reworks.

* Fixes for issues coming from github merge.

* Fixes for master

* Styling fixes

* Styling pt2

* Missed a few strings in master,

* Styling fixes

* Additional master merge fixes.

* Final cleanup and adaptation to master.

* Final empty language cleanup and few string added

* Small changes to italian translation

* Disabled translation on a few components inside plugin-loader for missing react hooks.

* Fixed passing tag to translation.

* Disable debug output for reducing console spam.

* Return correct content type

* Small italian language change

* Added support for country code

* Fixed missing translation for uninstall popup.

* Fix class name shenanigans for  toast notification

* Update dependencies

* Fixed github workflow to include the new locales folder

* Update dependencies to latest version (unless it's React) and fixed the new small errors that cropped up

* Missed a file name change

* Updated dev dependencies to latest version

* Missed a few dev dependencies

* Revert "Update dependencies to latest version (unless it's React) and fixed the new small errors that cropped up"

Messed up merge with a different main branch

* Messed up deletion of rollup config.

* Fix broken pnpm lock file

* Missed a localized string during the merge

* Fixed a parameter mistake in the uninstall text parameter

* Fix pnpm random issues

* Small italian language tweaks

* Fix wrong parameter passed to the uninstall function call

* Another fix on a wrong function parameter

* Additional translation text on the store and branch selection channels

* Changed the default type passed to map to being able to index the two arrays.

* Reverted and reworked the last changes

* Distinguish events in UI for installing vs reinstalling plugins

* Additional fixes for reinstall prompt

* Revert the use of intevalPlural since the parser doesn't seem to support that.

* Missed a routing path in the backend

* Small bugfixes

* Small fixes

* Correctly adding the parameter to the request headers.

* Refactoring of the UI popup modal

* Fix pnpm shenanigans

* Final fixes for the install UI localization

* Clean up unnedeed backend code

* Small rework on text selection.

* Cleaned up parser configuration

* Removed extracttext dependency to pnpmsetup

* Merged translation and cleaned up parser

* Fixed JSON structure after manual merge.

* Added translation to the file picker

* Revert changes to PluginInstallModal

* Reworked the text modal for the final time

* Missed the proper linted text

* Missed the backend change

* Final branch cleanup

* Fixed small translation bleeding

Caused from the manual merge of _old.json files.

* fix extra space in browser.py

* fix extra newline in plugin-loader.tsx

* Cleanup i18next-parser.config.mjs

* Update plugin-loader.tsx

* Cleanup language files

* Better labeling of text

* Fixed language typos in BranchSelect

* Fixed language typos in StoreSelect

* Cleanup plugin-loader.tsx from unused imports

* Removed the path bypass since I'm using authentication from the frontend.

* Reimplemented this component as a functional component.

* Updated dependencies and lockfile

* Removed static route from main.py

Already handled in loader.py

* Small italian coherency fixes

* Fix small typography fixes on plugin name uninstall

* Fixed italian typo on removal popup

* Reenabled manual escaping value in i18next

* Set to fallback to the default language if the string in the JSON file is empty.

* Fixed pnpm wankery

* Added a missed italian text translation string

---------

Co-authored-by: AAGaming <aa@mail.catvibers.me>
2023-05-02 16:42:39 +01:00
AAGaming caf37d681f Fix tab jank on latest steam beta 2023-04-28 22:29:46 -04:00
EMERALD 93151e4e5e Add file picker plugin install, plugin installs to developer page (#405) 2023-04-25 19:20:39 -07:00
suchmememanyskill d6f336d84b Feat/configurable paths (#404) 2023-04-24 20:12:42 -07:00
Beebles 4777963b65 Make patch notes modal only show current branch (#429) 2023-04-23 17:18:54 -07:00
Party Wumpus fc193f98db Fix browser.py (#431) 2023-04-21 20:14:34 -07:00
Travis Lane a07e4d6fe6 fix: version is no longer missing from plugin list (#417) 2023-04-10 16:47:59 -07:00
Party Wumpus 4ab7d97ab2 Various readme changes (#422)
* various installation clarifications

* Update README.md

* Update README.md

* Update README.md

* rip crankshaft, hope you come back one day

* Update README.md

* oops i linked to PartyWumpus/decky-loader by accident

i fix
2023-04-07 11:04:08 -07:00
AAGaming 15a6f7fdb8 fix UI reloading on latest beta to prevent freeze when updating Decky 2023-04-06 13:00:54 -04:00
AAGaming 7d2cff8745 fix two missing arguments, fixing reordering of newly installed plugins (#412) 2023-04-04 14:52:32 -04:00
Party Wumpus ee5ed3faf0 Make readme's download button into an svg (#413) 2023-04-04 12:36:30 -04:00
Travis Lane 0f36e87cce Add plugin reordering (#378)
* feat: started work on saving plugin order

* feat: implemented local ReorderableList

* feat: reoder complete except for usage of DFL

* switched to using dfl reorderableList

* fix: added missing file and removed frag

* updated to newest dfl

* Update defsettings.json

* fix: plugin order was missing on init

* fix: now await pluginOrder

* fix: moved the plugin-order load to plugin-loader

* chore: v6 and dfl bump
2023-04-03 14:21:31 -07:00
suchmememanyskill fd325ef1cc Add cross-platform support to decky (#387)
* Import generic watchdog observer over platform specific import

* Use os.path rather than genericpath

* Split off socket management in plugin.py

* Don't specify multiprocessing start type

Default on linux is already fork

* Move all platform-specific functions to seperate files

TODO: make plugin.py platform agnostic

* fix import

* add backwards compat to helpers.py

* add backwards compatibility to helpers.py harder

* Testing autobuild for win

* Testing autobuild for win, try 2

* Testing autobuild for win, try 3

* Testing autobuild for win, try 4

* Create the plugins folder before attempting to use it

* Implement win get_username()

* Create win install script

* Fix branch guess from version

* Create .loader.version in install script

* Add .cmd shim to facilitate auto-restarts

* Properly fix branch guess from version

* Fix updater on windows

* Try 2 of fixing updates for windows

* Test

* pain

* Update install script

* Powershell doesn't believe in utf8

* Powershell good

* add ON_LINUX variable to localplatform

* Fix more merge issues

* test

* Move custom imports to main.py

* Move custom imports to after __main__ check 

Due to windows' default behaviour being spawn, it will spawn a new process and thus import into sys.path multiple times

* Log errors in get_system_pythonpaths() and get_loader_version() + 

split get_system_pythonpaths() on newline

* Remove whitespace in result of get_system_pythonpaths()

* use python3 on linux and python on windows in get_system_pythonpaths()

* Remove fork-specific urls

* Fix MIME types not working on Windows
2023-03-21 17:37:23 -07:00
TrainDoctor faf46ba533 Update edit-check.yml 2023-03-09 16:32:54 -08:00
TrainDoctor 94ec434eae Update edit-check.yml 2023-03-09 16:31:43 -08:00
TrainDoctor a223efd6f5 Update edit-check.yml 2023-03-09 10:24:01 -08:00
suchmememanyskill 395e45167d Shared Ctx tab rename to SharedJSContext (#395) 2023-03-09 18:58:19 +01:00
TrainDoctor 0dd0d9f4bd Add CI to automatically update plugin stub in template 2023-03-05 16:23:17 -08:00
geeksville 3e5404abdd fix #390 the plugin_directory argument to import_plugin was incorrect (#391) 2023-03-05 14:30:26 -08:00
Beebles 46abc5a266 Fix QAM And Toaster Injection for Mar 02 Beta (#388) 2023-03-01 20:20:31 -08:00
TrainDoctor 88e1e9b869 Update README.md 2023-02-25 10:30:20 -08:00
TrainDoctor fc0089f7a5 Update bug_report.yml 2023-02-25 07:26:58 -08:00
AAGaming d335562328 update NavigateToExternalWeb in Markdown to use Navigation 2023-02-22 22:17:28 -05:00
AAGaming f9624a0859 how did this ever happen 2023-02-22 22:03:19 -05:00
AAGaming 97bb3fa4c8 Fix loader on feb 22 2023 beta 2023-02-22 22:00:30 -05:00
TrainDoctor 611245aec9 Update bug_report.yml 2023-02-22 17:38:46 -08:00
suchmememanyskill e1807e8c75 General Backend Fixes (#373)
* General Backend Fixes

* Ajust helpers.get_loader_version() to never throw an exception
2023-02-19 16:37:26 -08:00
TrainDoctor b94cfe32d9 Update README.md 2023-02-19 16:22:26 -08:00
Philipp Richter f1e679c3fb Expose a 'decky_plugin' module to decky plugins (#353)
* Expose a 'decky_plugin' module to decky plugins

* expose decky user home path
* support 'py_modules' python modules in plugins
* allow for a '_migration' method in plugins to have an explicit file
  moving step

* Expose the plugin python module as .pyi stub interface

* Expose system and user python paths to plugins
2023-02-19 14:42:55 -08:00
Beebles e1b138bcbd Fix fullscreen route inject issues caused by Feb. 17th beta. (#372)
* remove gamepad ui

* Refactor
2023-02-17 17:27:20 -08:00
Kevin Hester c6be8f6c14 Minor README fix for build instructions. (#370) 2023-02-17 14:10:03 -08:00
TrainDoctor ac086cf59e Update README.md 2023-02-08 18:43:33 -08:00
Marco Rodolfi 3e120ea312 Fix class name shenanigans for toast notification (#366)
* Fix class name shenanigans for  toast notification

* Corrected number of iterations
2023-02-06 17:30:44 -08:00
121 changed files with 13500 additions and 4187 deletions
+14 -3
View File
@@ -12,6 +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 may not be responded to).
- type: textarea
attributes:
@@ -41,7 +42,7 @@ body:
label: SteamOS version
# description: Can be found with `uname -a`
# placeholder: "Linux steamdeck 5.13.0-valve36-1-neptune #1 SMP PREEMPT Mon, 19 Dec 2022 23:39:41 +0000 x86_64 GNU/Linux"
placeholder: "SteamOS 3.4.3 Stable"
placeholder: "SteamOS 3.5.7 Stable"
validations:
required: true
@@ -66,8 +67,18 @@ body:
- type: textarea
attributes:
label: Logs
label: Backend Logs
description: Please reboot your deck (if possible) when attempting to recreate the issue, then run ``cd ~ && journalctl -b0 -u plugin_loader.service > deckylog.txt``. This will save the log file to ``~`` aka ``/home/deck``. Please upload the file here
placeholder: deckylog.txt
validations:
required: false
required: true
- type: textarea
attributes:
label: Frontend Logs
description: Please copy from your deck ~/.steam/steam/logs/cef_log.txt and ~/.steam/steam/logs/cef_log.previous.txt. Make sure to scrub your Steam username as it may appear in these logs.
placeholder: cef_log.txt
validations:
required: true
+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.
+13
View File
@@ -0,0 +1,13 @@
Please tick as appropriate:
- [ ] I have tested this code on a steam deck or on a PC
- [ ] My changes generate no new errors/warnings
- [ ] This is a bugfix/hotfix
- [ ] This is a new feature
If you're wanting to update a translation or add a new one, please use the weblate page: https://weblate.werwolv.net/projects/decky/
# Description
This fixes issue: #
Please provide a clear and concise description of what the new feature is. If appropriate, include screenshots or videos.
+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;/plugin" --hidden-import=logging.handlers --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;/plugin" --hidden-import=logging.handlers --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
+27 -9
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,16 +47,34 @@ 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
pip install pyinstaller==5.13.0
pip install -r requirements.txt
- name: Install JS dependencies ⬇️
working-directory: ./frontend
@@ -69,7 +87,7 @@ jobs:
run: pnpm run build
- name: Build Python Backend 🛠️
run: pyinstaller --noconfirm --onefile --name "PluginLoader" --add-data ./backend/static:/static --add-data ./backend/legacy:/legacy ./backend/*.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:/plugin --hidden-import=logging.handlers --hidden-import=sqlite3 ./backend/main.py
- name: Upload package artifact ⬆️
if: ${{ !env.ACT }}
@@ -110,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"
@@ -189,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"
+55
View File
@@ -0,0 +1,55 @@
name: Push Updated Plugin Stub to Template
on: push
jobs:
copy-stub:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@8230315d06ad95c617244d2f265d237a1682d445
with:
ref: ${{ github.sha }}
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v3
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v41.0.0
with:
separator: ","
files: |
plugin/*
- name: Is stub changed
id: changed-stub
run: |
STUB_CHANGED="false"
PATHS=(plugin plugin/decky_plugin.pyi)
SHA=${{ github.sha }}
SHA_PREV=HEAD^
FILES=$(git diff $SHA_PREV..$SHA --name-only -- ${PATHS[@]} | jq -Rsc 'split("\n")[:-1] | join (",")')
if [[ "$FILES" == *"plugin/decky_plugin.pyi"* ]]; then
$STUB_CHANGED="true"
echo "Stub has changed, pushing updated stub"
else
echo "Stub has not changed, exiting."
echo "has_changed=$STUB_CHANGED" >> $GITHUB_OUTPUT
exit 0
fi
echo "has_changed=$STUB_CHANGED" >> $GITHUB_OUTPUT
- name: Push updated stub
if: steps.changed-stub.outputs.has_changed == true
uses: dmnemec/copy_file_to_another_repo_action@bbebd3da22e4a37d04dca5f782edd5201cb97083
env:
API_TOKEN_GITHUB: ${{ secrets.GITHUB_TOKEN }}
with:
source_file: 'plugin/decky_plugin.pyi'
destination_repo: 'SteamDeckHomebrew/decky-plugin-template'
user_email: '11465594+TrainDoctor@users.noreply.github.com'
user_name: 'TrainDoctor'
commit_message: 'Updated template with latest plugin stub changes'
+10 -14
View File
@@ -2,6 +2,7 @@ name: Lint
on:
push:
pull_request:
jobs:
lint:
@@ -9,19 +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 black (Python formatting)
uses: lgeiger/black-action@v1.0.1
with:
args: "./backend --experimental-string-processing --config ./backend/pyproject.toml"
- name: Run ruff (Python linting)
uses: jpetrucciani/ruff-check@main
with:
path: "./backend"
- 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 -1
View File
@@ -4,4 +4,4 @@
"deckpass" : "ssap",
"deckkey" : "-i ${env:HOME}/.ssh/id_rsa",
"deckdir" : "/home/deck"
}
}
+10 -2
View File
@@ -38,7 +38,15 @@
"type": "shell",
"group": "none",
"detail": "Check for local runs, create a plugins folder",
"command": "rsync -azp --rsh='ssh -p ${config:deckport} ${config:deckkey}' requirements.txt deck@${config:deckip}:${config:deckdir}/homebrew/dev/pluginloader/requirements.txt && ssh deck@${config:deckip} -p ${config:deckport} ${config:deckkey} 'python -m ensurepip && python -m pip install --upgrade pip && python -m pip install --upgrade setuptools && python -m pip install -r ${config:deckdir}/homebrew/dev/pluginloader/requirements.txt'",
"command": "rsync -azp --rsh='ssh -p ${config:deckport} ${config:deckkey}' backend/requirements.txt deck@${config:deckip}:${config:deckdir}/homebrew/dev/pluginloader/backend/requirements.txt && ssh deck@${config:deckip} -p ${config:deckport} ${config:deckkey} 'python -m ensurepip && python -m pip install --upgrade --break-system-packages pip && python -m pip install --break-system-packages --upgrade setuptools && python -m pip install --break-system-packages -r ${config:deckdir}/homebrew/dev/pluginloader/backend/requirements.txt'",
"problemMatcher": []
},
{
"label": "extracttext",
"type": "shell",
"group": "none",
"detail": "Check for new strings in the frontend source code and extract it into the corresponding json language files",
"command": "cd frontend && ./node_modules/.bin/i18next --config ./i18next-parser.config.mjs",
"problemMatcher": []
},
// BUILD
@@ -97,7 +105,7 @@
"detail": "Deploy dev PluginLoader to deck",
"type": "shell",
"group": "none",
"command": "rsync -azp --delete --rsh='ssh -p ${config:deckport} ${config:deckkey}' --exclude='.git/' --exclude='.github/' --exclude='.vscode/' --exclude='frontend/' --exclude='dist/' --exclude='contrib/' --exclude='*.log' --exclude='requirements.txt' --exclude='backend/__pycache__/' --exclude='.gitignore' . deck@${config:deckip}:${config:deckdir}/homebrew/dev/pluginloader",
"command": "rsync -azp --delete --rsh='ssh -p ${config:deckport} ${config:deckkey}' --exclude='.git/' --exclude='.github/' --exclude='.vscode/' --exclude='frontend/' --exclude='dist/' --exclude='contrib/' --exclude='*.log' --exclude='requirements.txt' --exclude='**/__pycache__/' --exclude='.gitignore' . deck@${config:deckip}:${config:deckdir}/homebrew/dev/pluginloader",
"problemMatcher": []
},
// RUN
+15 -13
View File
@@ -3,15 +3,16 @@
<br>
Decky Loader
<br>
<a name="logo" href="https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/decky_installer.desktop"><img src="./docs/images/download_button.png" alt="Download decky" width="350"></a>
<a name="download button" href="https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/decky_installer.desktop"><img src="./docs/images/download_button.svg" alt="Download decky" width="350px" style="padding-top: 15px;"></a>
</h1>
<p align="center">
<a href="https://github.com/SteamDeckHomebrew/decky-loader/releases"><img src="https://img.shields.io/github/downloads/SteamDeckHomebrew/decky-loader/total" /></a>
<a href="https://github.com/SteamDeckHomebrew/decky-loader/stargazers"><img src="https://img.shields.io/github/stars/SteamDeckHomebrew/decky-loader" /></a>
<a href="https://github.com/SteamDeckHomebrew/decky-loader/commits/main"><img src="https://img.shields.io/github/last-commit/SteamDeckHomebrew/decky-loader.svg" /></a>
<a href="https://weblate.werwolv.net/engage/decky/"><img src="https://weblate.werwolv.net/widgets/decky/-/decky/svg-badge.svg" alt="Translation status" /></a>
<a href="https://github.com/SteamDeckHomebrew/decky-loader/blob/main/LICENSE"><img src="https://img.shields.io/github/license/SteamDeckHomebrew/decky-loader" /></a>
<a href="https://discord.gg/ZU74G2NJzk"><img src="https://img.shields.io/discord/960281551428522045?color=%235865F2&label=discord" /></a>
<a href="https://deckbrew.xyz/discord"><img src="https://img.shields.io/discord/960281551428522045?color=%235865F2&label=discord" /></a>
<br>
<br>
<img src="https://media.discordapp.net/attachments/966017112244125756/1012466063893610506/main.jpg" alt="Decky screenshot" width="80%">
@@ -33,40 +34,40 @@ For more information about Decky Loader as well as documentation and development
### 🤔 Common Issues
- Crankshaft is incompatible with Decky Loader. If you are using Crankshaft, please uninstall it before installing Decky Loader.
- 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.
- If you run the installer and it just opens a file in a text editor: click the (...) button in the top right of dolphin (the file manager) then 'configure' and 'configure dolphin'. Click on the 'confirmations' tab and set 'when opening an executable file' to 'run script'.
- 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).
## 💾 Installation
- This installation can be done without an admin/sudo password set.
1. Prepare a mouse and keyboard if possible.
- Keyboards and mice can be connected to the Steam Deck via USB-C or Bluetooth.
- Many Bluetooth keyboard and mouse apps are available for iOS and Android.
- Many Bluetooth keyboard and mouse apps are available for iOS and Android. (KDE connect is preinstalled on the steam deck)
- The Steam Link app is available on [Windows](https://media.steampowered.com/steamlink/windows/latest/SteamLink.zip), [macOS](https://apps.apple.com/us/app/steam-link/id1246969117), and [Linux](https://flathub.org/apps/details/com.valvesoftware.SteamLink). It works well as a remote desktop substitute.
- If you have no other options, use the right trackpad as a mouse and press <img src="./docs/images/light/steam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/steam.svg#gh-light-mode-only" height=16>+<img src="./docs/images/light/x.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/x.svg#gh-light-mode-only" height=16> to open the on-screen keyboard as needed.
1. Press the <img src="./docs/images/light/steam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/steam.svg#gh-light-mode-only" height=16> button and open the Power menu.
1. Select "Switch to Desktop".
1. Navigate to this Github page on a browser of your choice.
1. Press the 'Download' button at the top of the page.
1. Run the downloaded file by clicking on it in Dolphin (the file manager).
1. Either type your admin password or allow Decky to temporarily set your password to `Decky!`
1. Download the [installer file](https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/decky_installer.desktop). (If using firefox, it will be named `decky_installer.desktop.download`. Rename it to `decky_installer.desktop` before running it)
1. Drag the file onto your desktop and double click it to run it.
1. Either type your admin password or allow Decky to temporarily set your admin password to `Decky!` (this password will be removed after the installer finishes)
1. Choose the version of Decky Loader you want to install.
- **Latest Release**
Intended for most users. This is the latest stable version of Decky Loader.
- **Latest Pre-Release**
Intended for plugin developers. Pre-releases are unlikely to be fully stable but contain the latest changes. For more information on plugin development, please consult [the wiki page](https://deckbrew.xyz/en/loader-dev/development).
Intended for plugin developers. Pre-releases are unlikely to be fully stable but contain the latest changes. For more information on plugin development, please consult [the wiki page](https://wiki.deckbrew.xyz/en/loader-dev/development).
1. Open the Return to Gaming Mode shortcut on your desktop.
- There is also a fast install for those who can use Konsole. Run `curl -L https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/install_release.sh | sh` and type your password when prompted.
### 👋 Uninstallation
We are sorry to see you go! If you are considering uninstalling because you are having issues, please consider [opening an issue](https://github.com/SteamDeckHomebrew/decky-loader/issues) or [joining our Discord](https://discord.gg/ZU74G2NJzk) so we can help you and other users.
We are sorry to see you go! If you are considering uninstalling because you are having issues, please consider [opening an issue](https://github.com/SteamDeckHomebrew/decky-loader/issues) or [joining our Discord](https://deckbrew.xyz/discord) so we can help you and other users.
1. Press the <img src="./docs/images/light/steam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/steam.svg#gh-light-mode-only" height=16> button and open the Power menu.
1. Select "Switch to Desktop".
1. Run the installer file again, and select `uninstall decky loader`
1. Run the installer file again, and select `uninstall decky loader`.
- There is also a fast uninstall for those who can use Konsole. Run `curl -L https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/uninstall.sh | sh` and type your password when prompted.
## 🚀 Getting Started
@@ -84,15 +85,16 @@ Now that you have Decky Loader installed, you can start using plugins. Each plug
### 🛠️ Plugin Development
There is no complete plugin development documentation yet. However a good starting point is the [plugin template repository](https://github.com/SteamDeckHomebrew/decky-plugin-template). Consider [joining our Discord](https://discord.gg/ZU74G2NJzk) if you have any questions.
There is no complete plugin development documentation yet. However a good starting point is the [plugin template repository](https://github.com/SteamDeckHomebrew/decky-plugin-template). Consider [joining our Discord](https://deckbrew.xyz/discord) if you have any questions.
### 🤝 Contributing
Please consult [the wiki page regarding development](https://deckbrew.xyz/en/loader-dev/development) for more information on installing development versions of Decky Loader. You can also install the Steam Deck UI on a Windows or Linux computer for testing by following [this YouTube guide](https://youtu.be/1IAbZte8e7E?t=112).
Please consult [the wiki page regarding development](https://wiki.deckbrew.xyz/en/loader-dev/development) for more information on installing development versions of Decky Loader. You can also install the Steam Deck UI on a Windows or Linux computer for testing by following [this YouTube guide](https://youtu.be/1IAbZte8e7E?t=112).
1. Clone the repository using the latest commit to main before starting your PR.
1. In your clone of the repository, run these commands.
```bash
cd frontend
pnpm i
pnpm run build
```
+2 -2
View File
@@ -26,10 +26,10 @@ cd ..
if [[ "$type" == "release" ]]; then
printf "release!\n"
act workflow_dispatch -e act/release.json --artifact-server-path act/artifacts --container-architecture linux/amd64
act workflow_dispatch -e act/release.json --artifact-server-path act/artifacts --container-architecture linux/amd64 --platform ubuntu-22.04=catthehacker/ubuntu:act-22.04
elif [[ "$type" == "prerelease" ]]; then
printf "prerelease!\n"
act workflow_dispatch -e act/prerelease.json --artifact-server-path act/artifacts --container-architecture linux/amd64
act workflow_dispatch -e act/prerelease.json --artifact-server-path act/artifacts --container-architecture linux/amd64 --platform ubuntu-22.04=catthehacker/ubuntu:act-22.04
else
printf "Release type unspecified/badly specified.\n"
printf "Options: 'release' or 'prerelease'\n"
-241
View File
@@ -1,241 +0,0 @@
# Full imports
import json
# import pprint
# from pprint import pformat
# Partial imports
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, listdir, access, mkdir
from shutil import rmtree
from subprocess import call
from time import time
from zipfile import ZipFile
# Local modules
from helpers import (
get_ssl_context,
get_user,
get_user_group,
download_remote_binary_to_path,
)
from injector import get_gamepadui_tab
logger = getLogger("Browser")
class PluginInstallContext:
def __init__(self, artifact, name, version, hash) -> None:
self.artifact = artifact
self.name = name
self.version = version
self.hash = hash
class PluginBrowser:
def __init__(self, plugin_path, plugins, loader) -> None:
self.plugin_path = plugin_path
self.plugins = plugins
self.loader = loader
self.install_requests = {}
def _unzip_to_plugin_dir(self, zip, name, hash):
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 = self.find_plugin_folder(name)
code_chown = call(
["chown", "-R", get_user() + ":" + get_user_group(), plugin_dir]
)
code_chmod = call(["chmod", "-R", "555", plugin_dir])
if code_chown != 0 or code_chmod != 0:
logger.error(
f"chown/chmod exited with a non-zero exit code (chown: {code_chown},"
f" chmod: {code_chmod})"
)
return False
return True
async def _download_remote_binaries_for_plugin_with_name(self, pluginBasePath):
rv = False
try:
packageJsonPath = path.join(pluginBasePath, "package.json")
pluginBinPath = path.join(pluginBasePath, "bin")
if access(packageJsonPath, R_OK):
with open(packageJsonPath, "r", encoding="utf-8") as f:
packageJson = json.load(f)
if (
"remote_binary" in packageJson
and len(packageJson["remote_binary"]) > 0
):
# create bin directory if needed.
call(["chmod", "-R", "777", pluginBasePath])
if access(pluginBasePath, W_OK):
if not path.exists(pluginBinPath):
mkdir(pluginBinPath)
if not access(pluginBinPath, W_OK):
call(["chmod", "-R", "777", pluginBinPath])
rv = True
for remoteBinary in packageJson["remote_binary"]:
# Required Fields. If any Remote Binary is missing these fail the install.
binName = remoteBinary["name"]
binURL = remoteBinary["url"]
binHash = remoteBinary["sha256hash"]
if not await download_remote_binary_to_path(
binURL, binHash, path.join(pluginBinPath, binName)
):
rv = False
raise Exception(
"Error Downloading Remote Binary"
f" {binName}@{binURL} with hash {binHash} to"
f" {path.join(pluginBinPath, binName)}"
)
call(
[
"chown",
"-R",
get_user() + ":" + get_user_group(),
self.plugin_path,
]
)
call(["chmod", "-R", "555", pluginBasePath])
else:
rv = True
logger.debug("No Remote Binaries to Download")
except Exception as e:
rv = False
logger.debug(str(e))
return rv
def find_plugin_folder(self, name):
for folder in listdir(self.plugin_path):
try:
with open(
path.join(self.plugin_path, folder, "plugin.json"),
"r",
encoding="utf-8",
) as f:
plugin = json.load(f)
if plugin["name"] == name:
return str(path.join(self.plugin_path, folder))
except Exception:
logger.debug(f"skipping {folder}")
async def uninstall_plugin(self, name):
if self.loader.watcher:
self.loader.watcher.disabled = True
tab = await get_gamepadui_tab()
try:
logger.info("uninstalling " + name)
logger.info(" at dir " + self.find_plugin_folder(name))
logger.debug("calling frontend unload for %s" % str(name))
res = await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')")
logger.debug("result of unload from UI: %s", res)
# plugins_snapshot = self.plugins.copy()
# snapshot_string = pformat(plugins_snapshot)
# logger.debug("current plugins: %s", snapshot_string)
if self.plugins[name]:
logger.debug("Plugin %s was found", name)
self.plugins[name].stop()
logger.debug("Plugin %s was stopped", name)
del self.plugins[name]
logger.debug("Plugin %s was removed from the dictionary", name)
logger.debug("removing files %s" % str(name))
rmtree(self.find_plugin_folder(name))
except FileNotFoundError:
logger.warning(f"Plugin {name} not installed, skipping uninstallation")
except Exception as e:
logger.error(
f"Plugin {name} in {self.find_plugin_folder(name)} was not uninstalled"
)
logger.error("Error at %s", exc_info=e)
if self.loader.watcher:
self.loader.watcher.disabled = False
async def _install(self, artifact, name, version, hash):
isInstalled = False
if self.loader.watcher:
self.loader.watcher.disabled = True
try:
pluginFolderPath = self.find_plugin_folder(name)
if pluginFolderPath:
isInstalled = True
except Exception:
logger.error(
f"Failed to determine if {name} is already installed, continuing"
" anyway."
)
logger.info(f"Installing {name} (Version: {version})")
async with ClientSession() as client:
logger.debug(f"Fetching {artifact}")
res = await client.get(artifact, ssl=get_ssl_context())
if res.status == 200:
logger.debug("Got 200. Reading...")
data = await res.read()
logger.debug(f"Read {len(data)} bytes")
res_zip = BytesIO(data)
if isInstalled:
try:
logger.debug("Uninstalling existing plugin...")
await self.uninstall_plugin(name)
except Exception:
logger.error(f"Plugin {name} could not be uninstalled.")
logger.debug("Unzipping...")
ret = self._unzip_to_plugin_dir(res_zip, name, hash)
if ret:
plugin_dir = self.find_plugin_folder(name)
ret = await self._download_remote_binaries_for_plugin_with_name(
plugin_dir
)
if ret:
logger.info(f"Installed {name} (Version: {version})")
if name in self.loader.plugins:
self.loader.plugins[name].stop()
self.loader.plugins.pop(name, None)
await sleep(1)
self.loader.import_plugin(
path.join(plugin_dir, "main.py"), plugin_dir
)
else:
logger.fatal("Failed Downloading Remote Binaries")
else:
self.log.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
if self.loader.watcher:
self.loader.watcher.disabled = False
else:
logger.fatal(f"Could not fetch from URL. {await res.text()}")
async def request_plugin_install(self, artifact, name, version, hash):
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}',"
f" '{request_id}', '{hash}')"
)
async def confirm_plugin_install(self, request_id):
request = self.install_requests.pop(request_id)
await self._install(
request.artifact, request.name, request.version, request.hash
)
def cancel_plugin_install(self, request_id):
self.install_requests.pop(request_id)
-199
View File
@@ -1,199 +0,0 @@
import grp
import pwd
import re
import ssl
import subprocess
import uuid
import os
import sys
from hashlib import sha256
from io import BytesIO
import certifi
from aiohttp.web import Response, middleware
from aiohttp import ClientSession
REMOTE_DEBUGGER_UNIT = "steam-web-debug-portforward.service"
# global vars
csrf_token = str(uuid.uuid4())
ssl_ctx = ssl.create_default_context(cafile=certifi.where())
assets_regex = re.compile("^/plugins/.*/assets/.*")
frontend_regex = re.compile("^/frontend/.*")
def get_ssl_context():
return ssl_ctx
def get_csrf_token():
return csrf_token
@middleware
async def csrf_middleware(request, 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")
# Deprecated
def set_user():
pass
# Get the user id hosting the plugin loader
def get_user_id() -> int:
proc_path = os.path.realpath(sys.argv[0])
pws = sorted(pwd.getpwall(), reverse=True, key=lambda pw: len(pw.pw_dir))
for pw in pws:
if proc_path.startswith(os.path.realpath(pw.pw_dir)):
return pw.pw_uid
raise PermissionError(
"The plugin loader does not seem to be hosted by any known user."
)
# Get the user hosting the plugin loader
def get_user() -> str:
return pwd.getpwuid(get_user_id()).pw_name
# Get the effective user id of the running process
def get_effective_user_id() -> int:
return os.geteuid()
# Get the effective user of the running process
def get_effective_user() -> str:
return pwd.getpwuid(get_effective_user_id()).pw_name
# Get the effective user group id of the running process
def get_effective_user_group_id() -> int:
return os.getegid()
# Get the effective user group of the running process
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:
return pwd.getpwuid(os.stat(file_path).st_uid).pw_name
# Deprecated
def set_user_group() -> str:
return get_user_group()
# 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(file_path) -> str:
if file_path:
return grp.getgrgid(os.stat(file_path).st_gid).gr_name
else:
return grp.getgrgid(get_user_group_id()).gr_name
# Get the default home path unless a user is specified
def get_home_path(username=None) -> str:
if username is None:
username = get_user()
return pwd.getpwnam(username).pw_dir
# Get the default homebrew path unless a home_path is specified
def get_homebrew_path(home_path=None) -> str:
if home_path is None:
home_path = get_home_path()
return os.path.join(home_path, "homebrew")
# Recursively create path and chown as user
def mkdir_as_user(path):
path = os.path.realpath(path)
os.makedirs(path, exist_ok=True)
chown_path = get_home_path()
parts = os.path.relpath(path, chown_path).split(os.sep)
uid = get_user_id()
gid = get_user_group_id()
for p in parts:
chown_path = os.path.join(chown_path, p)
os.chown(chown_path, uid, gid)
# Fetches the version of loader
def get_loader_version() -> str:
with open(
os.path.join(os.path.dirname(sys.argv[0]), ".loader.version"),
"r",
encoding="utf-8",
) as version_file:
return version_file.readline().replace("\n", "")
# Download Remote Binaries to local Plugin
async def download_remote_binary_to_path(url, binHash, path) -> bool:
rv = False
try:
if os.access(os.path.dirname(path), os.W_OK):
async with ClientSession() as client:
res = await client.get(url, ssl=get_ssl_context())
if res.status == 200:
data = BytesIO(await res.read())
remoteHash = sha256(data.getbuffer()).hexdigest()
if binHash == remoteHash:
data.seek(0)
with open(path, "wb") as f:
f.write(data.getbuffer())
rv = True
else:
raise Exception(
f"Fatal Error: Hash Mismatch for remote binary {path}@{url}"
)
else:
rv = False
except Exception:
rv = False
return rv
async def is_systemd_unit_active(unit_name: str) -> bool:
res = subprocess.run(
["systemctl", "is-active", unit_name],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
return res.returncode == 0
async def stop_systemd_unit(unit_name: str) -> subprocess.CompletedProcess:
cmd = ["systemctl", "stop", unit_name]
return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
async def start_systemd_unit(unit_name: str) -> subprocess.CompletedProcess:
cmd = ["systemctl", "start", unit_name]
return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
-277
View File
@@ -1,277 +0,0 @@
from asyncio import 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 aiohttp import web
from genericpath import exists
from watchdog.events import RegexMatchingEventHandler
from watchdog.utils import UnsupportedLibc
try:
from watchdog.observers.inotify import InotifyObserver as Observer
except UnsupportedLibc:
from watchdog.observers.fsevents import FSEventsObserver as Observer
from injector import get_tab, get_gamepadui_tab
from plugin import PluginWrapper
class FileChangeHandler(RegexMatchingEventHandler):
def __init__(self, queue, plugin_path) -> None:
super().__init__(regexes=[r"^.*?dist\/index\.js$", r"^.*?main\.py$"])
self.logger = getLogger("file-watcher")
self.plugin_path = plugin_path
self.queue = queue
self.disabled = True
def maybe_reload(self, src_path):
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):
src_path = event.src_path
if "__pycache__" in src_path:
return
# check to make sure this isn't a directory
if path.isdir(src_path):
return
# get the directory name of the plugin so that we can find its "main.py" and reload it; the
# file that changed is not necessarily the one that needs to be reloaded
self.logger.debug(f"file created: {src_path}")
self.maybe_reload(src_path)
def on_modified(self, event):
src_path = event.src_path
if "__pycache__" in src_path:
return
# check to make sure this isn't a directory
if path.isdir(src_path):
return
# get the directory name of the plugin so that we can find its "main.py" and reload it; the
# file that changed is not necessarily the one that needs to be reloaded
self.logger.debug(f"file modified: {src_path}")
self.maybe_reload(src_path)
class Loader:
def __init__(self, server_instance, plugin_path, loop, live_reload=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 = {}
self.watcher = None
self.live_reload = live_reload
if live_reload:
self.reload_queue = Queue()
self.observer = Observer()
self.watcher = FileChangeHandler(self.reload_queue, plugin_path)
self.observer.schedule(self.watcher, self.plugin_path, recursive=True)
self.observer.start()
self.loop.create_task(self.handle_reloads())
self.loop.create_task(self.enable_reload_wait())
server_instance.add_routes(
[
web.get("/frontend/{path:.*}", self.handle_frontend_assets),
web.get("/plugins", self.get_plugins),
web.get(
"/plugins/{plugin_name}/frontend_bundle",
self.handle_frontend_bundle,
),
web.post(
"/plugins/{plugin_name}/methods/{method_name}",
self.handle_plugin_method_call,
),
web.get(
"/plugins/{plugin_name}/assets/{path:.*}",
self.handle_plugin_frontend_assets,
),
# The following is legacy plugin code.
web.get("/plugins/load_main/{name}", self.load_plugin_main_view),
web.get(
"/plugins/plugin_resource/{name}/{path:.+}", self.handle_sub_route
),
web.get("/steam_resource/{path:.+}", self.get_steam_resource),
]
)
async def enable_reload_wait(self):
if self.live_reload:
await sleep(10)
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"])
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
async def get_plugins(self, 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):
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):
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):
try:
plugin = PluginWrapper(file, plugin_directory, self.plugin_path)
if plugin.name in self.plugins:
if "debug" not in plugin.flags and refresh:
self.logger.info(
f"Plugin {plugin.name} is already loaded and has requested to"
" not be re-loaded"
)
return
else:
self.plugins[plugin.name].stop()
self.plugins.pop(plugin.name, None)
if plugin.passive:
self.logger.info(f"Plugin {plugin.name} is passive")
self.plugins[plugin.name] = plugin.start()
self.logger.info(f"Loaded {plugin.name}")
if not batch:
self.loop.create_task(
self.dispatch_plugin(
plugin.name if not plugin.legacy else "$LEGACY_" + plugin.name,
plugin.version,
)
)
except Exception as e:
self.logger.error(f"Could not load {file}. {e}")
print_exc()
async def dispatch_plugin(self, name, version):
gpui_tab = await get_gamepadui_tab()
await gpui_tab.evaluate_js(f"window.importDeckyPlugin('{name}', '{version}')")
def import_plugins(self):
self.logger.info(f"import plugins from {self.plugin_path}")
directories = [
i
for i in listdir(self.plugin_path)
if path.isdir(path.join(self.plugin_path, i))
and path.isfile(path.join(self.plugin_path, i, "plugin.json"))
]
for directory in directories:
self.logger.info(f"found plugin: {directory}")
self.import_plugin(
path.join(self.plugin_path, directory, "main.py"),
directory,
False,
True,
)
async def handle_reloads(self):
while True:
args = await self.reload_queue.get()
self.import_plugin(*args)
async def handle_plugin_method_call(self, 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"]
except JSONDecodeError:
args = {}
try:
if method_name.startswith("_"):
raise RuntimeError("Tried to call private method")
res["result"] = await plugin.execute_method(method_name, args)
res["success"] = True
except Exception as e:
res["result"] = str(e)
res["success"] = False
return web.json_response(res)
"""
The following methods are used to load legacy plugins, which are considered deprecated.
I made the choice to re-add them so that the first iteration/version of the react loader
can work as a drop-in replacement for the stable branch of the PluginLoader, so that we
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):
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()
ret = f"""
<script src="/legacy/library.js"></script>
<script>window.plugin_name = '{plugin.name}' </script>
<base href="http://127.0.0.1:1337/plugins/plugin_resource/{plugin.name}/">
{template_data}
"""
return web.Response(text=ret, content_type="text/html")
async def handle_sub_route(self, request):
plugin = self.plugins[request.match_info["name"]]
route_path = request.match_info["path"]
self.logger.info(path)
ret = ""
file_path = path.join(self.plugin_path, plugin.plugin_directory, route_path)
with open(file_path, "r", encoding="utf-8") as resource_data:
ret = resource_data.read()
return web.Response(text=ret)
async def get_steam_resource(self, 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)
+110
View File
@@ -0,0 +1,110 @@
{
"FilePickerIndex": {
"files": {
"file_type": "نوع الملف",
"show_hidden": "أظهر الملفات المخفية",
"all_files": "جميع الملفات"
},
"filter": {
"created_asce": "المنشئة (الأقدم)",
"modified_asce": "المعدلة (الأقدم)",
"modified_desc": "المعدلة (الأحدث)",
"name_asce": "أ-ي",
"name_desc": "أ-ي",
"size_asce": "الحجم ( الأصغر)",
"size_desc": "الحجم ( الأكبر)",
"created_desc": "المنشئة (الأحدث)"
},
"folder": {
"label": "المجلد",
"show_more": "أظهر المزيد من الملفات",
"select": "إستخدم هذا المجلد"
},
"file": {
"select": "إختر هذا الملف"
}
},
"MultiplePluginsInstallModal": {
"confirm": "هل أنت متأكد من التعديلات التالية؟",
"description": {
"reinstall": "إعادة تنصيب {{name}} {{version}}",
"update": "تحديث {{name}} إلى {{version}}",
"install": "تنصيب {{name}} {{version}}"
},
"ok_button": {
"idle": "تأكيد"
}
},
"DropdownMultiselect": {
"button": {
"back": "الخلف"
}
},
"BranchSelect": {
"update_channel": {
"label": "قناة التحديثات",
"prerelease": "الإصدار التجريبي",
"stable": "إصدار مستقر",
"testing": "إصدار تحت الإختبار"
}
},
"PluginCard": {
"plugin_full_access": "هذه الإضافة لديها الصلاحية للوصول لمحتويات Steam Deck.",
"plugin_install": "تنصيب",
"plugin_version_label": "رقم إصدار الإضافة",
"plugin_no_desc": "لا يوجد وصف متاح."
},
"PluginInstallModal": {
"install": {
"button_idle": "تنصيب",
"button_processing": "يتم التنصيب",
"title": "تنصيب {{artifact}}",
"desc": "هل أنت متأكد من رغبتك في تنصيب {{artifact}} {{version}}؟"
},
"reinstall": {
"button_idle": "إعادة تنصيب",
"button_processing": "تتم إعادة التنصيب",
"desc": "هل أنت متأكد من رغبتك في إعادة تنصيب {{artifact}} {{version}}؟",
"title": "إعادة تنصيب {{artifact}}"
},
"update": {
"button_idle": "تحديث",
"button_processing": "يتم التحديث",
"title": "تحديث {{artifact}}",
"desc": "هل أنت متأكد من رغبتك في تحديث {{artifact}} {{version}}؟"
}
},
"PluginListIndex": {
"hide": "إخفاء من قائمة الوصول السريع",
"reinstall": "إعادة التنصيب",
"reload": "إعادة التحميل",
"show": "إظهار في قائمة الوصول السريع",
"unfreeze": "السماح بالتحديثات",
"uninstall": "إزالة التنصيب",
"update_to": "التحديث إلى {{name}}"
},
"PluginListLabel": {
"hidden": "مخفي من قائمة الوصول السريع"
},
"PluginLoader": {
"decky_title": "Decky",
"error": "خطا",
"plugin_uninstall": {
"button": "إزالة التنصيب",
"title": "إزالة التنصيب {{name}}"
}
},
"SettingsDeveloperIndex": {
"react_devtools": {
"ip_label": "عنوان الشبكة"
},
"third_party_plugins": {
"button_zip": "تصفح",
"button_install": "تنصيب"
},
"header": "أخرى"
},
"Developer": {
"5secreload": "إعادة التحميل في 5 ثواني"
}
}
+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}} добавки са скрити от този списък"
}
}
+277
View File
@@ -0,0 +1,277 @@
{
"BranchSelect": {
"update_channel": {
"label": "Aktualizační kanál",
"prerelease": "Předběžná vydání",
"stable": "Stabilní",
"testing": "Testování"
}
},
"Developer": {
"disabling": "Vypínám React DevTools",
"enabling": "Zapínám React DevTools",
"5secreload": "Znovu načtení za 5 vteřin"
},
"FilePickerIndex": {
"folder": {
"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": {
"hidden_one": "1 plugin je v tomto seznamu skrytý",
"hidden_few": "{{count}} pluginů je v tomto seznamu skryto",
"hidden_other": "{{count}} pluginů je v tomto seznamu skryto"
},
"PluginListLabel": {
"hidden": "Skryto z nabídky rychlého přístupu"
},
"PluginCard": {
"plugin_full_access": "Tento plugin má plný přístup k vašemu Steam Decku.",
"plugin_install": "Instalovat",
"plugin_no_desc": "Nebyl uveden žádný popis.",
"plugin_version_label": "Verze pluginu"
},
"PluginInstallModal": {
"install": {
"button_idle": "Instalovat",
"button_processing": "Instalování",
"title": "Instalovat {{artifact}}",
"desc": "Jste si jisti, že chcete nainstalovat {{artifact}} {{version}}?"
},
"no_hash": "Tento plugin nemá hash, instalujete jej na vlastní nebezpečí.",
"reinstall": {
"button_idle": "Přeinstalovat",
"button_processing": "Přeinstalování",
"title": "Přeinstalovat {{artifact}}",
"desc": "Jste si jisti, že chcete přeinstalovat {{artifact}} {{version}}?"
},
"update": {
"button_idle": "Aktualizovat",
"button_processing": "Aktualizování",
"desc": "Jste si jisti, že chcete aktualizovat {{artifact}} {{version}}?",
"title": "Aktualizovat {{artifact}}"
}
},
"MultiplePluginsInstallModal": {
"title": {
"mixed_one": "Upravit {{count}} plugin",
"mixed_few": "Upravit {{count}} pluginů",
"mixed_other": "Upravit {{count}} pluginů",
"reinstall_one": "Přeinstalovat 1 plugin",
"reinstall_few": "Přeinstalovat {{count}} pluginů",
"reinstall_other": "Přeinstalovat {{count}} pluginů",
"install_one": "Instalovat 1 plugin",
"install_few": "Instalovat {{count}} pluginů",
"install_other": "Instalovat {{count}} pluginů",
"update_one": "Aktualizovat 1 plugin",
"update_few": "Aktualizovat {{count}} pluginů",
"update_other": "Aktualizovat {{count}} pluginů"
},
"ok_button": {
"idle": "Potvrdit",
"loading": "Probíhá"
},
"description": {
"install": "Instalovat {{name}} {{version}}",
"update": "Aktualizovat {{name}} na {{version}}",
"reinstall": "Přeinstalovat {{name}} {{version}}"
},
"confirm": "Jste si jisti, že chcete udělat následující úpravy?"
},
"PluginListIndex": {
"no_plugin": "Nejsou nainstalovány žádné pluginy!",
"plugin_actions": "Akce pluginu",
"reinstall": "Přeinstalovat",
"reload": "Znovu načíst",
"uninstall": "Odinstalovat",
"update_to": "Aktualizovat na {{name}}",
"show": "Rychlý přístup: Zobrazit",
"hide": "Rychlý přístup: Skrýt",
"update_all_one": "Aktualizovat 1 plugin",
"update_all_few": "Aktualizovat {{count}} pluginů",
"update_all_other": "Aktualizovat {{count}} pluginů",
"freeze": "Pozastavit aktualizace",
"unfreeze": "Povolit aktualizace"
},
"PluginLoader": {
"decky_title": "Decky",
"decky_update_available": "Aktualizace na {{tag_name}} dostupná!",
"error": "Chyba",
"plugin_load_error": {
"message": "Chyba při načítání pluginu {{name}}",
"toast": "Chyba při načítání {{name}}"
},
"plugin_uninstall": {
"button": "Odinstalovat",
"desc": "Opravdu chcete odinstalovat {{name}}?",
"title": "Odinstalovat {{name}}"
},
"plugin_update_one": "Je dostupná aktualizace pro 1 plugin!",
"plugin_update_few": "Jsou dostupné aktualizace pro {{count}} pluginů!",
"plugin_update_other": "Jsou dostupné aktualizace pro {{count}} pluginů!",
"plugin_error_uninstall": "Načítání {{name}} způsobilo chybu uvedenou výše. To obvykle znamená, že plugin vyžaduje aktualizaci SteamUI. Zkontrolujte, zda je aktualizace k dispozici, nebo zvažte odstranění pluginu v nastavení Decky v sekci Pluginy."
},
"SettingsDeveloperIndex": {
"cef_console": {
"button": "Otevřít konzoli",
"label": "CEF konzole",
"desc": "Otevře CEF konzoli. Užitečné pouze pro účely ladění. Věci zde jsou potenciálně nebezpečné a měly by být používány pouze v případě, že jste vývojář pluginů, nebo vás sem nějaký nasměroval."
},
"header": "Ostatní",
"react_devtools": {
"desc": "Umožňuje připojení k počítači, na kterém běží React DevTools. Změnou tohoto nastavení se znovu načte Steam. Před povolením nastavte IP adresu.",
"ip_label": "IP adresa",
"label": "Zapnout React DevTools"
},
"third_party_plugins": {
"button_install": "Instalovat",
"button_zip": "Procházet",
"header": "Pluginy třetí strany",
"label_desc": "URL",
"label_url": "Instalovat plugin z URL",
"label_zip": "Instalovat plugin ze ZIP souboru"
},
"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.",
"label": "Zapnout Valve Internal"
}
},
"RemoteDebugging": {
"remote_cef": {
"label": "Povolit vzdálené CEF ladění",
"desc": "Umožní neověřený přístup k CEF ladění komukoli ve vaší síti"
}
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Decky verze",
"header": "O Decky"
},
"beta": {
"header": "Účast v betě"
},
"developer_mode": {
"label": "Vývojářský režim"
},
"other": {
"header": "Ostatní"
},
"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é",
"plugins_title": "Pluginy",
"testing_title": "Testování"
},
"Store": {
"store_contrib": {
"label": "Přispívání",
"desc": "Pokud byste chtěli přispět do obchodu Decky Plugin Store, podívejte se na repozitář SteamDeckHomebrew/decky-plugin-template na GitHubu. Informace o vývoji a distribuci jsou k dispozici v README."
},
"store_filter": {
"label": "Filtr",
"label_def": "Vše"
},
"store_search": {
"label": "Hledat"
},
"store_sort": {
"label": "Seřadit",
"label_def": "Naposledy aktualizováno (Nejnovější)"
},
"store_source": {
"desc": "Veškerý zdrojový kód pluginu je dostupný v repozitáři SteamDeckHomebrew/decky-plugin-database na GitHubu.",
"label": "Zdrojový kód"
},
"store_tabs": {
"about": "O Decky Plugin Store",
"alph_asce": "Abecedně (Z do A)",
"alph_desc": "Abecedně (A do Z)",
"title": "Procházet",
"date_asce": "Nejstarší",
"downloads_desc": "Nejvíce stažené",
"date_desc": "Nejnovější",
"downloads_asce": "Nejméně stažené"
},
"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": {
"label": "Vlastní obchod",
"url_label": "URL"
},
"store_channel": {
"custom": "Vlastní",
"default": "Výchozí",
"label": "Kanál obchodu",
"testing": "Testování"
}
},
"Updater": {
"updates": {
"lat_version": "Aktuální: běží na verzi {{ver}}",
"reloading": "Znovu načítání",
"updating": "Aktualizování",
"check_button": "Zkontrolovat aktualizace",
"checking": "Kontrolování",
"cur_version": "Aktuální verze: {{ver}}",
"install_button": "Instalovat aktualizaci",
"label": "Aktualizace"
},
"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"
},
"Testing": {
"download": "Stáhnout"
}
}
+270
View File
@@ -0,0 +1,270 @@
{
"BranchSelect": {
"update_channel": {
"label": "Updatekanal",
"prerelease": "Vorabveröffentlichung",
"stable": "Standard",
"testing": "Test"
}
},
"Developer": {
"disabling": "Deaktiviere React DevTools",
"enabling": "Aktiviere React DevTools",
"5secreload": "Neu laden in 5 Sekunden"
},
"FilePickerIndex": {
"folder": {
"select": "Diesen Ordner verwenden",
"label": "Ordner",
"show_more": "Mehr Dateien anzeigen"
},
"filter": {
"name_desc": "A-Z",
"size_asce": "Größe (Kleinste)",
"size_desc": "Größe (Größte)",
"created_asce": "Erstellt (Älteste)",
"created_desc": "Erstellt (Neuste)",
"modified_asce": "Geändert (Älteste)",
"modified_desc": "Geändert (Neuste)",
"name_asce": "Z-A"
},
"file": {
"select": "Diese Datei auswählen"
},
"files": {
"all_files": "Alle Dateien",
"file_type": "Dateityp",
"show_hidden": "Versteckte Dateien anzeigen"
}
},
"PluginCard": {
"plugin_install": "Installieren",
"plugin_no_desc": "Keine Beschreibung angegeben.",
"plugin_version_label": "Erweiterungs Version",
"plugin_full_access": "Diese Erweiterung hat uneingeschränkten Zugriff auf dein Steam Deck."
},
"PluginInstallModal": {
"install": {
"button_idle": "Installieren",
"button_processing": "Wird installiert",
"desc": "Bist du dir sicher, dass du {{artifact}} {{version}} installieren willst?",
"title": "Installiere {{artifact}}"
},
"reinstall": {
"button_idle": "Neu installieren",
"button_processing": "Wird neu installiert",
"desc": "Bist du dir sicher, dass du {{artifact}} {{version}} neu installieren willst?",
"title": "Neu installation {{artifact}}"
},
"update": {
"button_idle": "Aktualisieren",
"button_processing": "Wird aktualisiert",
"title": "Aktualisiere {{artifact}}",
"desc": "Bist du dir sicher, dass du {{artifact}} {{version}} aktualisieren willst?"
},
"no_hash": "Diese Erweiterung besitzt keine Prüfsumme, Installation auf eigene Gefahr."
},
"PluginListIndex": {
"no_plugin": "Keine Erweiterungen installiert!",
"plugin_actions": "Erweiterungs Aktionen",
"reinstall": "Neu installieren",
"reload": "Neu laden",
"uninstall": "Deinstallieren",
"update_to": "Aktualisieren zu {{name}}",
"update_all_one": "{{count}} Plugin aktualisieren",
"update_all_other": "{{count}} Plugins aktualisieren",
"show": "Schnellzugriff: Anzeigen",
"hide": "Schnellzugriff: Ausblenden",
"freeze": "Updates einfrieren",
"unfreeze": "Updates erlauben"
},
"PluginLoader": {
"decky_title": "Decky",
"decky_update_available": "Eine neue Version ({{tag_name}}) ist verfügbar!",
"error": "Fehler",
"plugin_load_error": {
"toast": "Fehler beim Laden von {{name}}",
"message": "Fehler beim Laden von {{name}}"
},
"plugin_uninstall": {
"button": "Deinstallieren",
"desc": "Bist du dir sicher, dass du {{name}} deinstallieren willst?",
"title": "Deinstalliere {{name}}"
},
"plugin_error_uninstall": "Das Laden von {{name}} hat einen Fehler verursacht. Dies bedeutet normalerweise, dass die Erweiterung ein Update für die neue Version von SteamUI benötigt. Prüfe in den Decky-Einstellungen im Bereich Erweiterungen, ob ein Update vorhanden ist.",
"plugin_update_one": "1 Erweiterung kann aktualisiert werden!",
"plugin_update_other": "{{count}} Erweiterungen können aktualisiert werden!"
},
"RemoteDebugging": {
"remote_cef": {
"label": "Remote CEF Debugging Zugriff",
"desc": "Erlaubt jedem aus dem Neztwerk unautorisierten Zugriff auf den CEF Debugger"
}
},
"SettingsDeveloperIndex": {
"header": "Sonstiges",
"react_devtools": {
"ip_label": "IP",
"label": "Aktiviere React DevTools",
"desc": "Erlaubt die Verbindung mit einem anderen Rechner, auf welchem React DevTools läuft. Eine Änderung startet Steam neu. Die IP Adresse muss vor Aktivierung ausgefüllt sein."
},
"third_party_plugins": {
"button_zip": "Durchsuchen",
"header": "Erweiterungen von Drittanbietern",
"label_desc": "URL",
"label_zip": "Installiere Erweiterung via ZIP Datei",
"button_install": "Installieren",
"label_url": "Installiere Erweiterung via URL"
},
"valve_internal": {
"desc2": "Fasse in diesem Menü nichts an, es sei denn, du weißt was du tust.",
"label": "Aktiviere Valve-internes Menü",
"desc1": "Aktiviert das Valve-interne Entwickler Menü."
},
"cef_console": {
"button": "Konsole öffnen",
"label": "CEF Konsole",
"desc": "Öffnet die CEF Konsole. Nur für Debugzwecke. Dinge hier sind potentiell gefährlich und sollten nur durch oder unter Anleitung von Pluginentwickler/innen geschehen."
}
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Decky Version",
"header": "Über"
},
"beta": {
"header": "Beta Teilnahme"
},
"developer_mode": {
"label": "Entwickleroptionen"
},
"other": {
"header": "Sonstiges"
},
"updates": {
"header": "Aktualisierungen"
},
"notifications": {
"decky_updates_label": "Decky Update verfügbar",
"header": "Benachrichtigungen",
"plugin_updates_label": "Plugin Updates verfügbar"
}
},
"SettingsIndex": {
"developer_title": "Entwickler",
"general_title": "Allgemein",
"plugins_title": "Erweiterungen",
"testing_title": "Testen"
},
"Store": {
"store_contrib": {
"label": "Mitwirken",
"desc": "Wenn du Erweiterungen im Decky Store veröffentlichen willst, besuche die SteamDeckHomebrew/decky-plugin-template Repository auf GitHub. Informationen rund um Entwicklung und Veröffentlichung findest du in der README."
},
"store_filter": {
"label": "Filter",
"label_def": "Alle"
},
"store_search": {
"label": "Suche"
},
"store_sort": {
"label": "Sortierung",
"label_def": "Zuletzt aktualisiert"
},
"store_source": {
"desc": "Jeder Erweiterungs Quellcode ist in der SteamDeckHomebrew/decky-plugin-database Repository auf GitHub verfügbar.",
"label": "Quellcode"
},
"store_tabs": {
"about": "Über",
"alph_asce": "Alphabetisch (Z zu A)",
"alph_desc": "Alphabetisch (A zu Z)",
"title": "Durchstöbern",
"date_desc": "Neuste Zuerst",
"downloads_asce": "Wenigste Downloads Zuerst",
"downloads_desc": "Meiste Downloads Zuerst",
"date_asce": "Älteste Zuerst"
},
"store_testing_cta": "Unterstütze das Decky Loader Team mit dem Testen von neuen Erweiterungen!",
"store_testing_warning": {
"label": "Willkommen zum Test Store Kanal",
"desc": "Du kannst diesen Store Kanal nutzen, um brandneue Testversionen von Plugins auszuprobieren. Denk daran Feedback auf GitHub zu hinterlassen, sodass das Plugin für alle Nutzer verbessert werden kann."
}
},
"StoreSelect": {
"custom_store": {
"label": "Benutzerdefiniertes Store",
"url_label": "URL"
},
"store_channel": {
"custom": "Benutzerdefiniert",
"default": "Standard",
"label": "Store Kanal",
"testing": "Test"
}
},
"Updater": {
"decky_updates": "Decky Aktualisierungen",
"patch_notes_desc": "Patchnotizen",
"updates": {
"check_button": "Auf Aktualisierungen prüfen",
"checking": "Wird überprüft",
"cur_version": "Aktualle Version: {{ver}}",
"install_button": "Aktualisierung installieren",
"label": "Aktualisierungen",
"lat_version": "{{ver}} ist die aktuellste",
"reloading": "Lade neu",
"updating": "Aktualisiere"
},
"no_patch_notes_desc": "Für diese Version gibt es keine Patchnotizen"
},
"PluginView": {
"hidden_one": "{{count}} Plugin ist in dieser Liste ausgeblendet",
"hidden_other": "{{count}} Plugins sind in dieser Liste ausgeblendet"
},
"MultiplePluginsInstallModal": {
"title": {
"install_one": "{{count}} Plugin installieren",
"install_other": "{{count}} Plugins installieren",
"mixed_one": "{{count}} Plugin bearbeiten",
"mixed_other": "{{count}} Plugins bearbeiten",
"update_one": "{{count}} Plugin aktualisieren",
"update_other": "{{count}} Plugins aktualisieren",
"reinstall_one": "{{count}} Plugin neu installieren",
"reinstall_other": "{{count}} Plugins neu installieren"
},
"description": {
"install": "{{name}} {{version}} installieren",
"update": "{{name}} auf {{version}} aktualisieren",
"reinstall": "{{name}} {{version}} neu installieren"
},
"confirm": "Bist du sicher, dass du die folgenden Änderungen vornehmen möchtest?",
"ok_button": {
"loading": "An der Arbeit",
"idle": "Bestätigen"
}
},
"PluginListLabel": {
"hidden": "Im Schnellzugriff-Menu ausgeblendet"
},
"TitleView": {
"decky_store_desc": "Decky Store Öffnen",
"settings_desc": "Decky Einstellungen Öffnen"
},
"DropdownMultiselect": {
"button": {
"back": "Zurück"
}
},
"FilePickerError": {
"errors": {
"unknown": "Ein unbekannter Fehler ist aufgetreten. Die ursprüngliche Fehlermeldung ist: {{raw_error}}",
"file_not_found": "Der Pfad ist ungültig. Bitte prüfen und erneut eingeben.",
"perm_denied": "Kein Zugriff auf den angegebenen Dateipfad. Bitte prüfen, ob der Nutzer (deck auf dem Steam Deck) die entsprechenden Zugriffsrechte auf den angegebenen Ordner/die angegebene Datei hat."
}
},
"Testing": {
"download": "Download"
}
}
+260
View File
@@ -0,0 +1,260 @@
{
"SettingsDeveloperIndex": {
"react_devtools": {
"desc": "Επιτρέπει την σύνδεση με υπολογιστή που τρέχει React DevTools. Η αλλαγή αυτής της ρύθμισης θα προκαλέσει επαναφόρτωση του Steam. Ωρίστε την διεύθυνση IP πριν την ενεργοποιήσετε.",
"ip_label": "IP",
"label": "Ενεργοποίηση React DevTools"
},
"third_party_plugins": {
"button_install": "Εγκατάσταση",
"button_zip": "Περιήγηση",
"header": "Επεκτάσεις τρίτων",
"label_desc": "URL",
"label_url": "Εγκατάσταση επέκτασης απο URL",
"label_zip": "Εγκατάσταση επέκτασης από αρχείο ZIP"
},
"valve_internal": {
"desc1": "Ενεργοποιεί το μενού προγραμματιστή της Valve.",
"desc2": "Μην αγγίξετε τίποτα σε αυτό το μενού εκτός και αν ξέρετε τι κάνει.",
"label": "Ενεργοποιήση εσωτερικού μενού Valve"
},
"cef_console": {
"button": "Άνοιγμα Κονσόλας",
"desc": "Ανοίγει την Κονσόλα CEF. Χρήσιμο μόνο για εντοπισμό σφαλμάτων. Τα πράγματα εδώ είναι δυνητικά επικίνδυνα και θα πρέπει να χρησιμοποιηθεί μόνο εάν είστε προγραμματιστής επεκτάσεων, ή κατευθυνθήκατε εδώ από έναν προγραμματιστή.",
"label": "Κονσόλα CEF"
},
"header": "Άλλα"
},
"BranchSelect": {
"update_channel": {
"prerelease": "Προ-κυκλοφορία",
"stable": "Σταθερό",
"label": "Κανάλι ενημερώσεων",
"testing": "Δοκιμαστικό"
}
},
"Developer": {
"5secreload": "Γίνεται επαναφόρτωση σε 5 δευτερόλεπτα",
"disabling": "Γίνεται απενεργοποίηση των React DevTools",
"enabling": "Γίνεται ενεργοποίηση των React DevTools"
},
"PluginCard": {
"plugin_no_desc": "Δεν υπάρχει περιγραφή.",
"plugin_full_access": "Αυτή η επέκταση έχει πλήρη πρόσβαση στο Steam Deck σας.",
"plugin_install": "Εγκατάσταση",
"plugin_version_label": "Έκδοση επέκτασης"
},
"PluginInstallModal": {
"install": {
"desc": "Σίγουρα θέλετε να εγκαταστήσετε το {{artifact}}{{version}};",
"button_idle": "Εγκατάσταση",
"button_processing": "Γίνεται εγκατάσταση",
"title": "Εγκατάσταση {{artifact}}"
},
"no_hash": "Αυτή η επέκταση δεν έχει υπογραφή, την εγκαθηστάτε με δικό σας ρίσκο.",
"reinstall": {
"button_idle": "Επανεγκατάσταση",
"button_processing": "Γίνεται επανεγκατάσταση",
"desc": "Σίγουρα θέλετε να επανεγκαταστήσετε το {{artifact}}{{version}};",
"title": "Επανεγκατάσταση {{artifact}}"
},
"update": {
"button_idle": "Ενημέρωση",
"desc": "Σίγουρα θέλετε να ενημερώσετε το {{artifact}} {{version}};",
"title": "Ενημέρωση {{artifact}}",
"button_processing": "Γίνεται ενημέρωση"
}
},
"PluginListIndex": {
"no_plugin": "Δεν υπάρχουν εγκατεστημένες επεκτάσεις!",
"plugin_actions": "Ενέργειες επεκτάσεων",
"reinstall": "Επανεγκατάσταση",
"reload": "Επαναφόρτωση",
"uninstall": "Απεγκατάσταση",
"update_to": "Ενημέρωση σε {{name}}",
"update_all_one": "Ενημέρωση 1 επέκτασης",
"update_all_other": "Ενημέρωση {{count}} επεκτάσεων",
"show": "Γρήγορη πρόσβαση: Εμφάνιση",
"hide": "Γρήγορη πρόσβαση: Απόκρυψη"
},
"PluginLoader": {
"decky_title": "Decky",
"decky_update_available": "Ενημέρωση σε {{tag_name}} διαθέσιμη!",
"error": "Σφάλμα",
"plugin_error_uninstall": "Η φόρτωση του {{name}} προκάλεσε το παραπάνω σφάλμα. Αυτό συνήθως σημαίνει ότι η επέκταση απαιτεί ενημέρωση για τη νέα έκδοση του SteamUI. Ελέγξτε εάν υπάρχει ενημέρωση ή αξιολογήστε την απεγκαταστήσετε της επέκτασης στις ρυθμίσεις του Decky, στην ενότητα Επεκτάσεις.",
"plugin_load_error": {
"message": "Σφάλμα στη φόρτωση της επέκτασης {{name}}",
"toast": "Σφάλμα φόρτωσης {{name}}"
},
"plugin_uninstall": {
"button": "Απεγκατάσταση",
"desc": "Σίγουρα θέλετε να απεγκαταστήσετε το {{name}};",
"title": "Απεγκατάσταση {{name}}"
},
"plugin_update_one": "Διαθέσιμη ενημέρωση για 1 επέκταση!",
"plugin_update_other": "Διαθέσιμες ενημερώσεις για {{count}} επεκτάσεις!"
},
"RemoteDebugging": {
"remote_cef": {
"label": "Να επιτρέπεται η απομακρυσμένη πρόσβαση στον CEF debugger",
"desc": "Να επιτρέπεται η ανεξέλεγκτη πρόσβαση στον CEF debugger σε οποιονδήποτε στο τοπικό δίκτυο"
}
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Έκδοση Decky",
"header": "Σχετικά"
},
"developer_mode": {
"label": "Λειτουργία προγραμματιστή"
},
"other": {
"header": "Άλλα"
},
"updates": {
"header": "Ενημερώσεις"
},
"beta": {
"header": "Συμμετοχή στη Beta"
},
"notifications": {
"decky_updates_label": "Διαθέσιμη ενημέρωση του Decky",
"header": "Ειδοποιήσεις",
"plugin_updates_label": "Διαθέσιμες ενημερώσεις επεκτάσεων"
}
},
"SettingsIndex": {
"plugins_title": "Επεκτάσεις",
"developer_title": "Προγραμματιστής",
"general_title": "Γενικά"
},
"Store": {
"store_contrib": {
"label": "Συνεισφέροντας",
"desc": "Αν θέλετε να συνεισφέρετε στο κατάστημα επεκτάσεων του Decky, τσεκάρετε το SteamDeckHomebrew/decky-plugin-template repository στο GitHub. Πληροφορίες σχετικά με τη δημιουργία και τη διανομή επεκτάσεων είναι διαθέσιμες στο README."
},
"store_filter": {
"label": "Φίλτρο",
"label_def": "Όλα"
},
"store_search": {
"label": "Αναζήτηση"
},
"store_sort": {
"label": "Ταξινόμηση",
"label_def": "Τελευταία ενημέρωση (Νεότερα)"
},
"store_source": {
"desc": "Ο πηγαίος κώδικας όλων των επεκτάσεων είναι διαθέσιμος στο SteamDeckHomebrew/decky-plugin-database repository στο GitHub.",
"label": "Πηγαίος κώδικας"
},
"store_tabs": {
"about": "Σχετικά",
"alph_asce": "Αλφαβητικά (Ζ σε Α)",
"alph_desc": "Αλφαβητικά (Α σε Ζ)",
"title": "Περιήγηση"
},
"store_testing_cta": "Παρακαλώ σκεφτείτε να τεστάρετε νέες επεκτάσεις για να βοηθήσετε την ομάδα του Decky Loader!",
"store_testing_warning": {
"desc": "Μπορείτε να χρησιμοποιήσετε αυτό το κανάλι του καταστήματος για να δοκιμάσετε τις νεότερες εκδόσεις των επεκτάσεων. Φροντίστε να αφήσετε σχόλια στο GitHub, ώστε να βοηθήσετε στην ενημέρωση της εκάστοτε επέκταση για όλους τους χρήστες.",
"label": "Καλώς ήρθατε στο Δοκιμαστικό Κανάλι τους Καταστήματος"
}
},
"StoreSelect": {
"custom_store": {
"label": "Προσαρμοσμένο κατάστημα",
"url_label": "URL"
},
"store_channel": {
"custom": "Προσαρμοσμένο",
"default": "Προεπιλεγμένο",
"label": "Κανάλι καταστήματος",
"testing": "Δοκιμαστικό"
}
},
"Updater": {
"no_patch_notes_desc": "Κανένα ενημερωτικό σημείωμα για αυτή την έκδοση",
"patch_notes_desc": "Σημειώσεις ενημέρωσης",
"updates": {
"check_button": "Έλεγχος για ενημερώσεις",
"checking": "Γίνεται έλεγχος",
"cur_version": "Τρέχουσα έκδοση: {{ver}}",
"install_button": "Εγκατάσταση ενημέρωσης",
"label": "Ενημερώσεις",
"updating": "Γίνεται ενημέρωση",
"lat_version": "Ενημερωμένο: τρέχουσα έκδοση {{ver}}",
"reloading": "Γίνεται επαναφόρτωση"
},
"decky_updates": "Ενημερώσεις Decky"
},
"FilePickerIndex": {
"folder": {
"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": "1 επέκταση είναι κρυμμένη σε αυτήν τη λίστα",
"hidden_other": "{{count}} επεκτάσεις είναι κρυμμένες σε αυτήν τη λίστα"
},
"MultiplePluginsInstallModal": {
"title": {
"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}}"
}
}
}
+270
View File
@@ -0,0 +1,270 @@
{
"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": {
"freeze": "Freeze updates",
"hide": "Quick access: Hide",
"no_plugin": "No plugins installed!",
"plugin_actions": "Plugin Actions",
"reinstall": "Reinstall",
"reload": "Reload",
"show": "Quick access: Show",
"unfreeze": "Allow updates",
"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",
"testing_title": "Testing"
},
"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)",
"date_asce": "Oldest First",
"date_desc": "Newest First",
"downloads_asce": "Least Downloaded First",
"downloads_desc": "Most Downloaded First",
"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"
}
},
"Testing": {
"download": "Download"
}
}
+217
View File
@@ -0,0 +1,217 @@
{
"SettingsDeveloperIndex": {
"third_party_plugins": {
"button_install": "Instalar",
"button_zip": "Navegar",
"label_desc": "URL",
"label_url": "Instalar plugin desde URL",
"label_zip": "Instalar plugin desde archivo ZIP",
"header": "Plugins de terceros"
},
"valve_internal": {
"desc2": "No toques nada en este menú a menos que sepas lo que haces.",
"label": "Activar menú interno de Valve",
"desc1": "Activa el menú interno de desarrollo de Valve."
},
"cef_console": {
"button": "Abrir consola",
"label": "Consola CEF",
"desc": "Abre la consola del CEF. Solamente es útil para propósitos de depuración. Las cosas que hagas aquí pueden ser potencialmente peligrosas y solo se debería usar si eres un desarrollador de plugins, o uno te ha dirigido aquí."
},
"react_devtools": {
"ip_label": "IP",
"label": "Activar DevTools de React",
"desc": "Permite la conexión a un ordenador ejecutando las DevTools de React. Cambiar este ajuste recargará Steam. Configura la dirección IP antes de activarlo."
},
"header": "Otros"
},
"PluginInstallModal": {
"install": {
"button_idle": "Instalar",
"button_processing": "Instalando",
"title": "Instalar {{artifact}}",
"desc": "¿Estás seguro de que quieres instalar {{artifact}} {{version}}?"
},
"reinstall": {
"button_idle": "Reinstalar",
"button_processing": "Reinstalando",
"desc": "¿Estás seguro de que quieres reinstalar {{artifact}} {{version}}?",
"title": "Reinstalar {{artifact}}"
},
"update": {
"button_processing": "Actualizando",
"button_idle": "Actualizar",
"desc": "¿Estás seguro de que quieres actualizar {{artifact}} {{version}}?",
"title": "Actualizar {{artifact}}"
},
"no_hash": "Este plugin no tiene un hash, lo estás instalando bajo tu propia responsabilidad."
},
"Developer": {
"disabling": "Desactivando DevTools de React",
"enabling": "Activando DevTools de React",
"5secreload": "Recargando en 5 segundos"
},
"BranchSelect": {
"update_channel": {
"prerelease": "Prelanzamiento",
"stable": "Estable",
"label": "Canal de actualización",
"testing": "Pruebas"
}
},
"PluginCard": {
"plugin_full_access": "Este plugin tiene acceso completo a su Steam Deck.",
"plugin_install": "Instalar",
"plugin_version_label": "Versión de Plugin",
"plugin_no_desc": "No se proporcionó una descripción."
},
"FilePickerIndex": {
"folder": {
"select": "Usar esta carpeta"
}
},
"PluginListIndex": {
"uninstall": "Desinstalar",
"reinstall": "Reinstalar",
"reload": "Recargar",
"plugin_actions": "Acciones de plugin",
"no_plugin": "¡No hay plugins instalados!",
"update_all_one": "Actualizar 1 plugin",
"update_all_many": "Actualizar {{count}} plugins",
"update_all_other": "Actualizar {{count}} plugins",
"update_to": "Actualizar a {{name}}"
},
"PluginLoader": {
"error": "Error",
"plugin_uninstall": {
"button": "Desinstalar",
"desc": "¿Estás seguro de que quieres desinstalar {{name}}?",
"title": "Desinstalar {{name}}"
},
"decky_title": "Decky",
"plugin_update_one": "¡Actualización disponible para 1 plugin!",
"plugin_update_many": "¡Actualizaciones disponibles para {{count}} plugins!",
"plugin_update_other": "¡Actualizaciones disponibles para {{count}} plugins!",
"decky_update_available": "¡Actualización {{tag_name}} disponible!",
"plugin_load_error": {
"message": "Se ha producido un error al cargar el plugin {{name}}",
"toast": "Se ha producido un error al cargar {{name}}"
},
"plugin_error_uninstall": "Al cargar {{name}} se ha producido una excepción como se muestra arriba. Esto suele significar que el plugin requiere una actualización para la nueva versión de SteamUI. Comprueba si hay una actualización disponible o valora eliminarlo en los ajustes de Decky, en la sección Plugins."
},
"RemoteDebugging": {
"remote_cef": {
"desc": "Permitir acceso no autenticado al CEF debugger a cualquier persona en su red",
"label": "Permitir depuración remota del CEF"
}
},
"SettingsGeneralIndex": {
"updates": {
"header": "Actualizaciones"
},
"about": {
"header": "Acerca de",
"decky_version": "Versión de Decky"
},
"developer_mode": {
"label": "Modo desarrollador"
},
"beta": {
"header": "Participación en la beta"
},
"other": {
"header": "Otros"
}
},
"SettingsIndex": {
"developer_title": "Desarrollador",
"general_title": "General",
"plugins_title": "Plugins"
},
"Store": {
"store_search": {
"label": "Buscar"
},
"store_sort": {
"label": "Ordenar",
"label_def": "Actualizado por última vez (Nuevos)"
},
"store_contrib": {
"desc": "Si desea contribuir a la tienda de plugins de Decky, mira el repositorio SteamDeckHomebrew/decky-plugin-template en GitHub. Hay información acerca del desarrollo y distribución en el archivo README.",
"label": "Contribuyendo"
},
"store_tabs": {
"about": "Información",
"title": "Navegar",
"alph_asce": "Alfabéticamente (Z-A)",
"alph_desc": "Alfabéticamente (A-Z)"
},
"store_testing_cta": "¡Por favor considera probar plugins nuevos para ayudar al equipo de Decky Loader!",
"store_source": {
"desc": "El código fuente de los plugins está disponible en el repositiorio SteamDeckHomebrew/decky-plugin-database en GitHub.",
"label": "Código fuente"
},
"store_filter": {
"label_def": "Todos",
"label": "Filtrar"
}
},
"Updater": {
"updates": {
"reloading": "Recargando",
"updating": "Actualizando",
"checking": "Buscando",
"check_button": "Buscar actualizaciones",
"install_button": "Instalar actualización",
"label": "Actualizaciones",
"lat_version": "Actualizado: ejecutando {{ver}}",
"cur_version": "Versión actual: {{ver}}"
},
"decky_updates": "Actualizaciones de Decky",
"no_patch_notes_desc": "No hay notas de parche para esta versión",
"patch_notes_desc": "Notas de parche"
},
"MultiplePluginsInstallModal": {
"title": {
"reinstall_one": "Reinstalar 1 plugin",
"reinstall_many": "Reinstalar {{count}} plugins",
"reinstall_other": "Reinstalar {{count}} plugins",
"update_one": "Actualizar 1 plugin",
"update_many": "Actualizar {{count}} plugins",
"update_other": "Actualizar {{count}} plugins",
"mixed_one": "Modificar 1 plugin",
"mixed_many": "Modificar {{count}} plugins",
"mixed_other": "Modificar {{count}} plugins",
"install_one": "Instalar 1 plugin",
"install_many": "Instalar {{count}} plugins",
"install_other": "Instalar {{count}} plugins"
},
"ok_button": {
"idle": "Confirmar",
"loading": "Trabajando"
},
"confirm": "¿Estás seguro de que quieres hacer las siguientes modificaciones?",
"description": {
"install": "Instalar {{name}} {{version}}",
"update": "Actualizar {{name}} a {{version}}",
"reinstall": "Reinstalar {{name}} {{version}}"
}
},
"StoreSelect": {
"custom_store": {
"url_label": "URL",
"label": "Tienda personalizada"
},
"store_channel": {
"custom": "Personalizada",
"default": "Por defecto",
"label": "Canál de la tienda",
"testing": "Pruebas"
}
},
"PluginView": {
"hidden_one": "",
"hidden_many": "",
"hidden_other": ""
}
}
+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"
}
}
+277
View File
@@ -0,0 +1,277 @@
{
"SettingsDeveloperIndex": {
"react_devtools": {
"desc": "Permet la connexion à un ordinateur exécutant React DevTools. Changer ce paramètre rechargera Steam. Définissez l'adresse IP avant l'activation.",
"ip_label": "IP",
"label": "Activer React DevTools"
},
"third_party_plugins": {
"button_install": "Installer",
"button_zip": "Parcourir",
"header": "Plugins tiers",
"label_desc": "URL",
"label_url": "Installer le plugin à partir d'un URL",
"label_zip": "Installer le plugin à partir d'un fichier ZIP"
},
"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.",
"label": "Activer Valve Internal"
},
"cef_console": {
"button": "Ouvrir la console",
"label": "CEF Console",
"desc": "Ouvre la console CEF. Utile uniquement à des fins de débogage. Les éléments présentés ici sont potentiellement dangereux et ne doivent être utilisés que si vous êtes un développeur de plugins ou si vous êtes dirigé ici par un de ces développeurs."
},
"header": "Autre"
},
"BranchSelect": {
"update_channel": {
"prerelease": "Avant-première",
"label": "Canal de mise à jour",
"stable": "Stable",
"testing": "Test"
}
},
"StoreSelect": {
"store_channel": {
"label": "Canal du Plugin Store",
"testing": "Test",
"custom": "Personnalisé",
"default": "Par défaut"
},
"custom_store": {
"label": "Plugin Store personnalisé",
"url_label": "URL"
}
},
"Updater": {
"decky_updates": "Mises à jour de Decky",
"no_patch_notes_desc": "pas de notes de mise à jour pour cette version",
"patch_notes_desc": "Notes de mise à jour",
"updates": {
"check_button": "Chercher les mises à jour",
"checking": "Recherche",
"cur_version": "Version actuelle: {{ver}}",
"install_button": "Installer la mise à jour",
"label": "Mises à jour",
"lat_version": "À jour: version {{ver}}",
"reloading": "Rechargement",
"updating": "Mise à jour en cours"
}
},
"Developer": {
"5secreload": "Rechargement dans 5 secondes",
"disabling": "Désactivation des React DevTools",
"enabling": "Activation des React DevTools"
},
"FilePickerIndex": {
"folder": {
"select": "Utiliser ce dossier",
"label": "Dossier",
"show_more": "Afficher plus de fichiers"
},
"files": {
"show_hidden": "Afficher les fichiers cachés",
"all_files": "Tout les fichiers",
"file_type": "Type de fichier"
},
"file": {
"select": "Sélectionner ce fichier"
},
"filter": {
"created_desc": "Création (Plus récent)",
"modified_asce": "Modifié (Plus vieux)",
"modified_desc": "Modifié (Plus récent)",
"created_asce": "Création (Plus vieux)",
"name_asce": "Z-A",
"name_desc": "A-Z",
"size_asce": "Taille (Plus petit)",
"size_desc": "Taille (Plus grand)"
}
},
"PluginCard": {
"plugin_full_access": "Ce plugin a un accès complet à votre Steam Deck.",
"plugin_install": "Installer",
"plugin_no_desc": "Aucune description fournie.",
"plugin_version_label": "Version du plugin"
},
"PluginInstallModal": {
"install": {
"button_idle": "Installer",
"button_processing": "Installation en cours",
"title": "Installer {{artifact}}",
"desc": "Êtes-vous sûr de vouloir installer {{artifact}} {{version}}?"
},
"no_hash": "Ce plugin n'a pas de somme de contrôle, vous l'installez à vos risques et périls.",
"reinstall": {
"button_idle": "Réinstaller",
"button_processing": "Réinstallation en cours",
"desc": "Êtes-vous sûr de vouloir réinstaller {{artifact}} {{version}}?",
"title": "Réinstaller {{artifact}}"
},
"update": {
"button_idle": "Mettre à jour",
"button_processing": "Mise à jour",
"title": "Mettre à jour {{artifact}}",
"desc": "Êtes-vous sûr de vouloir mettre à jour {{artifact}} {{version}}?"
}
},
"PluginListIndex": {
"plugin_actions": "Plugin Actions",
"reinstall": "Réinstaller",
"reload": "Recharger",
"uninstall": "Désinstaller",
"update_to": "Mettre à jour vers {{name}}",
"no_plugin": "Aucun plugin installé !",
"update_all_one": "Mettre à jour 1 plugin",
"update_all_many": "Mettre à jour {{count}} plugins",
"update_all_other": "Mettre à jour {{count}} plugins",
"show": "Accès Rapide : Afficher",
"hide": "Accès rapide : Cacher",
"unfreeze": "Autoriser les mises à jour",
"freeze": "Geler les mises à jour"
},
"PluginLoader": {
"decky_title": "Decky",
"error": "Erreur",
"plugin_error_uninstall": "Allez sur {{name}} dans le menu de Decky si vous voulez désinstaller ce plugin.",
"plugin_load_error": {
"message": "Erreur lors du chargement du plugin {{name}}",
"toast": "Erreur lors du chargement de {{name}}"
},
"decky_update_available": "Mise à jour vers {{tag_name}} disponible !",
"plugin_uninstall": {
"button": "Désinstaller",
"title": "Désinstaller {{name}}",
"desc": "Êtes-vous sûr.e de vouloir désinstaller {{name}} ?"
},
"plugin_update_one": "Mise à jour disponible pour 1 plugin !",
"plugin_update_many": "Mises à jour disponible pour {{count}} plugins !",
"plugin_update_other": "Mises à jour disponible pour {{count}} plugins !"
},
"RemoteDebugging": {
"remote_cef": {
"desc": "Autoriser l'accès non authentifié au débogueur CEF à toute personne de votre réseau",
"label": "Autoriser le débogage CEF à distance"
}
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Version de Decky",
"header": "À propos"
},
"beta": {
"header": "Participation à la Bêta"
},
"developer_mode": {
"label": "Mode développeur"
},
"other": {
"header": "Autre"
},
"updates": {
"header": "Mises à jour"
},
"notifications": {
"decky_updates_label": "Mise à jour Decky disponible",
"header": "Notifications",
"plugin_updates_label": "Mises à jour du plugin disponibles"
}
},
"SettingsIndex": {
"developer_title": "Développeur",
"general_title": "Général",
"plugins_title": "Plugins",
"testing_title": "Essai"
},
"Store": {
"store_contrib": {
"desc": "Si vous souhaitez contribuer au Decky Plugin Store, consultez le dépôt SteamDeckHomebrew/decky-plugin-template sur GitHub. Des informations sur le développement et la distribution sont disponibles dans le fichier README.",
"label": "Contributions"
},
"store_filter": {
"label": "Filtrer",
"label_def": "Tous"
},
"store_search": {
"label": "Rechercher"
},
"store_sort": {
"label": "Trier",
"label_def": "Mises à jour (Plus récentes)"
},
"store_source": {
"desc": "Tout le code source des plugins est disponible sur le dépôt SteamDeckHomebrew/decky-plugin-database sur GitHub.",
"label": "Code Source"
},
"store_tabs": {
"about": "À propos",
"alph_asce": "Alphabétique (Z à A)",
"alph_desc": "Alphabétique (A à Z)",
"title": "Explorer",
"date_asce": "Plus ancien en premier",
"date_desc": "Le plus récent d'abord",
"downloads_asce": "Le moins téléchargé en premier",
"downloads_desc": "Les plus téléchargés en premier"
},
"store_testing_cta": "Pensez à tester de nouveaux plugins pour aider l'équipe Decky Loader !",
"store_testing_warning": {
"label": "Bienvenue sur la chaîne du magasin de tests",
"desc": "Vous pouvez utiliser cette chaîne de magasin pour tester des versions de plugins. Assurez-vous de laisser des commentaires sur GitHub afin que le plugin puisse être mis à jour pour tous les utilisateurs."
}
},
"PluginView": {
"hidden_one": "1 plugin est masqué dans cette liste",
"hidden_many": "{{count}} plugins sont masqués de cette liste",
"hidden_other": "{{count}} plugins sont masqués de cette liste"
},
"MultiplePluginsInstallModal": {
"title": {
"reinstall_one": "Réinstaller 1 plugin",
"reinstall_many": "Réinstaller {{count}} plugins",
"reinstall_other": "Réinstaller {{count}} plugins",
"install_one": "Installer 1 plugin",
"install_many": "Installer {{count}} plugins",
"install_other": "Installer {{count}} plugins",
"mixed_one": "Modifier {{count}} plugin",
"mixed_many": "Modifier {{count}} plugins",
"mixed_other": "Modifier {{count}} plugins",
"update_one": "Mettre à jour 1 plugin",
"update_many": "Mettre à jour {{count}} plugins",
"update_other": "Mettre à jour {{count}} plugins"
},
"confirm": "Êtes-vous sûr de vouloir apporter les modifications suivantes ?",
"description": {
"install": "Installer {{name}} {{version}}",
"update": "Mettre à jour {{name}} à {{version}}",
"reinstall": "Réinstaller {{name}} {{version}}"
},
"ok_button": {
"idle": "Confirmer",
"loading": "En cours"
}
},
"PluginListLabel": {
"hidden": "Caché du menu d'accès rapide"
},
"FilePickerError": {
"errors": {
"perm_denied": "Vous n'avez pas accès au dossier spécifié. Veuillez vérifier que votre utilisateur (deck sur un Steam Deck) possède les permissions requises pour accéder au dossier/fichier spécifié.",
"file_not_found": "Le chemin spécifié n'est pas valide. Veuillez vérifier et essayer à nouveau.",
"unknown": "Une erreur inconnue est survenue. L'erreur est : {{raw_error}}"
}
},
"DropdownMultiselect": {
"button": {
"back": "Retour"
}
},
"TitleView": {
"decky_store_desc": "Ouvrir le magasin Decky",
"settings_desc": "Ouvrir les paramètres de Decky"
},
"Testing": {
"download": "Télécharger"
}
}
+277
View File
@@ -0,0 +1,277 @@
{
"BranchSelect": {
"update_channel": {
"label": "Canale di aggiornamento",
"prerelease": "Prerilascio",
"stable": "Stabile",
"testing": "In prova"
}
},
"Developer": {
"5secreload": "Ricarico tra 5 secondi",
"disabling": "Disabilito i tools di React",
"enabling": "Abilito i tools di React"
},
"DropdownMultiselect": {
"button": {
"back": "Indietro"
}
},
"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}}",
"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",
"show_hidden": "Mostra nascosti"
},
"filter": {
"created_asce": "Creazione (meno recente)",
"created_desc": "Creazione (più recente)",
"modified_asce": "Modifica (meno recente)",
"modified_desc": "Modifica (più recente)",
"name_asce": "Z-A",
"name_desc": "A-Z",
"size_asce": "Dimensione (più piccolo)",
"size_desc": "Dimensione (più grande)"
},
"folder": {
"label": "Cartella",
"select": "Usa questa cartella",
"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",
"plugin_no_desc": "Nessuna descrizione fornita.",
"plugin_version_label": "Versione Plugin"
},
"PluginInstallModal": {
"install": {
"button_idle": "Installa",
"button_processing": "Installando",
"desc": "Sei sicuro di voler installare {{artifact}} {{version}}?",
"title": "Installa {{artifact}}"
},
"no_hash": "Questo plugin non ha un hash associato, lo stai installando a tuo rischio e pericolo.",
"reinstall": {
"button_idle": "Reinstalla",
"button_processing": "Reinstallando",
"desc": "Sei sicuro di voler reinstallare {{artifact}} {{version}}?",
"title": "Reinstalla {{artifact}}"
},
"update": {
"button_idle": "Aggiorna",
"button_processing": "Aggiornando",
"desc": "Sei sicuro di voler aggiornare {{artifact}} {{version}}?",
"title": "Aggiorna {{artifact}}"
}
},
"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_all_one": "Aggiorna un plugin",
"update_all_many": "Aggiorna {{count}} plugins",
"update_all_other": "Aggiorna {{count}} plugins",
"update_to": "Aggiorna a {{name}}",
"unfreeze": "Permetti aggiornamenti",
"freeze": "Congela aggiornamenti"
},
"PluginListLabel": {
"hidden": "Nascosto dal menu di accesso rapido"
},
"PluginLoader": {
"decky_title": "Decky",
"decky_update_available": "Disponibile aggiornamento a {{tag_name}}!",
"error": "Errore",
"plugin_error_uninstall": "Il plugin {{name}} ha causato un'eccezione che è descritta sopra. Questo tipicamente significa che il plugin deve essere aggiornato per funzionare sulla nuova versione di SteamUI. Controlla se è disponibile un aggiornamento o valutane la rimozione andando nelle impostazioni di Decky nella sezione Plugins.",
"plugin_load_error": {
"message": "Errore caricando il plugin {{name}}",
"toast": "Errore caricando {{name}}"
},
"plugin_uninstall": {
"button": "Rimuovi",
"desc": "Sei sicuro di voler rimuovere {{name}}?",
"title": "Rimuovi {{name}}"
},
"plugin_update_one": "Aggiornamento disponibile per 1 plugin!",
"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",
"label": "Permetti il debug remoto di CEF"
}
},
"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.",
"ip_label": "IP",
"label": "Abilita i DevTools di React"
},
"third_party_plugins": {
"button_install": "Installa",
"button_zip": "Seleziona",
"header": "Plugin di terze parti",
"label_desc": "URL",
"label_url": "Installa plugin da un'indirizzo web",
"label_zip": "Installa plugin da un file ZIP"
},
"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"
}
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Versione di Decky",
"header": "Riguardo a"
},
"beta": {
"header": "Partecipazione alla beta"
},
"developer_mode": {
"label": "Modalità sviluppatore"
},
"other": {
"header": "Altro"
},
"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",
"plugins_title": "Plugins",
"testing_title": "Testing"
},
"Store": {
"store_contrib": {
"desc": "Se desideri contribuire allo store di Decky, puoi trovare un template caricato su GitHub all'indirizzo SteamDeckHomebrew/decky-plugin-template. Informazioni riguardo sviluppo e distribuzione sono disponibili nel README.",
"label": "Contribuisci"
},
"store_filter": {
"label": "Filtra",
"label_def": "Tutto"
},
"store_search": {
"label": "Cerca"
},
"store_sort": {
"label": "Ordina",
"label_def": "Ultimo aggiornato (Più recente)"
},
"store_source": {
"desc": "Tutto il codice sorgente dei plugin è disponibile su GitHub all'indirizzo SteamDeckHomebrew/decky-plugin-database.",
"label": "Codice Sorgente"
},
"store_tabs": {
"about": "Riguardo a",
"alph_asce": "Alfabetico (Z a A)",
"alph_desc": "Alfabetico (A a Z)",
"title": "Sfoglia",
"date_desc": "Per più recente",
"date_asce": "Per più vecchio",
"downloads_desc": "Per più scaricato",
"downloads_asce": "Per meno scaricato"
},
"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": {
"label": "Negozio custom",
"url_label": "URL"
},
"store_channel": {
"custom": "Personalizzato",
"default": "Default",
"label": "Canale del negozio",
"testing": "In prova"
}
},
"Updater": {
"decky_updates": "Aggiornamento di Decky",
"no_patch_notes_desc": "nessuna patch notes per questa versione",
"patch_notes_desc": "Cambiamenti",
"updates": {
"check_button": "Cerca aggiornamenti",
"checking": "Controllando",
"cur_version": "Versione attuale: {{ver}}",
"install_button": "Installa aggiornamento",
"label": "Aggiornamenti",
"lat_version": "Aggiornato. Eseguendo {{ver}}",
"reloading": "Ricaricando",
"updating": "Aggiornando"
}
},
"TitleView": {
"settings_desc": "Apri le impostazioni di Decky",
"decky_store_desc": "Apri lo store di Decky"
},
"Testing": {
"download": "Scarica"
}
}
+253
View File
@@ -0,0 +1,253 @@
{
"BranchSelect": {
"update_channel": {
"stable": "安定",
"testing": "テスト",
"label": "アップデートチャンネル",
"prerelease": "プレリリース"
}
},
"DropdownMultiselect": {
"button": {
"back": "戻る"
}
},
"FilePickerIndex": {
"file": {
"select": "ファイルを選択"
},
"files": {
"all_files": "すべてのファイル",
"file_type": "ファイルタイプ",
"show_hidden": "非表示ファイルを表示する"
},
"filter": {
"name_asce": "Z-A",
"name_desc": "A-Z",
"size_asce": "サイズ(小さい順)",
"size_desc": "サイズ(大きい順)",
"created_asce": "作成日(古い順)",
"created_desc": "作成日(新しい順)",
"modified_asce": "更新日(古い順)",
"modified_desc": "更新日(新しい順)"
},
"folder": {
"label": "フォルダ",
"select": "このフォルダを使用",
"show_more": "その他のファイルを表示"
}
},
"MultiplePluginsInstallModal": {
"description": {
"install": "インストール {{name}} {{version}}",
"reinstall": "再インストール {{name}} {{version}}",
"update": "アップデート {{name}} {{version}}"
},
"ok_button": {
"idle": "確認",
"loading": "作業中"
},
"title": {
"install_other": "{{count}} 個のプラグインをインストール",
"mixed_other": "{{count}} 個のプラグインを修正",
"update_other": "{{count}} 個のプラグインをアップデート",
"reinstall_other": "{{count}} 個のプラグインを再インストール"
},
"confirm": "以下の変更を加えてもよろしいですか?"
},
"Developer": {
"enabling": "React DevToolsを有効",
"disabling": "React DevToolsを無効",
"5secreload": "5秒以内に再読み込みされます"
},
"PluginInstallModal": {
"install": {
"button_idle": "インストール",
"title": "{{artifact}} をインストール",
"button_processing": "インストール中",
"desc": "{{artifact}} {{version}} をインストールしてもよろしいですか?"
},
"no_hash": "このプラグインにはハッシュがありません。ご自身の責任でインストールしてください。",
"reinstall": {
"button_idle": "再インストール",
"button_processing": "再インストール中",
"desc": "{{artifact}} {{version}} を再インストールしてもよろしいですか?",
"title": "{{artifact}} を再インストール"
},
"update": {
"button_idle": "アップデート",
"title": "{{artifact}} をアップデート",
"desc": "{{artifact}} {{version}} をアップデートしてもよろしいですか?",
"button_processing": "アップデート中"
}
},
"PluginListIndex": {
"hide": "クイックアクセス: 非表示",
"no_plugin": "プラグインがインストールされていません!",
"reinstall": "再インストール",
"reload": "再読み込み",
"uninstall": "アンインストール",
"plugin_actions": "プラグインアクション",
"update_all_other": "{{count}} 個のプラグインをアップデート",
"show": "クイックアクセス: 表示",
"update_to": "{{name}} を更新"
},
"PluginListLabel": {
"hidden": "クイックアクセスメニューから表示されません"
},
"PluginLoader": {
"error": "エラー",
"plugin_load_error": {
"message": "プラグイン {{name}} の読み込みエラー",
"toast": "{{name}} の読み込みエラー"
},
"plugin_uninstall": {
"button": "アンインストール",
"desc": "{{name}} をアンインストールしてもよろしいですか?",
"title": "{{name}} をアンインストール"
},
"decky_title": "Decky",
"decky_update_available": "{{tag_name}} のアップデートが利用可能です!",
"plugin_update_other": "{{count}} 個のプラグインのアップデートが利用可能です!",
"plugin_error_uninstall": "{{name}} プラグインを読み込む際に上記のような例外が発生しました。 これは通常、SteamUIの最新バージョンに合ったプラグインのアップデートが必要な場合に発生します。Decky設定のプラグインセクションでアップデートがあるかどうかを確認するか、アンインストールをお試しください。"
},
"SettingsDeveloperIndex": {
"cef_console": {
"button": "コンソールを開く",
"label": "CEFコンソール",
"desc": "CEFコンソールを開きます。デバッグ目的でのみ使用してください。これらの項目は危険な可能性があるので、プラグイン開発者であるか、開発者のガイドに従う場合のみ使用する必要があります。"
},
"react_devtools": {
"ip_label": "IP",
"label": "React DevTools を有効化",
"desc": "React DevToolsを実行しているコンピューターへの接続を有効にします。この設定を変更すると、Steam が再ロードされます。有効にする前にIPアドレスを設定してください。"
},
"third_party_plugins": {
"button_install": "インストール",
"button_zip": "開く",
"header": "サードパーティプラグイン",
"label_desc": "URL",
"label_url": "URLからプラグインをインストール",
"label_zip": "ZIPファイルからプラグインをインストール"
},
"valve_internal": {
"desc1": "Valveの内部開発者メニューを有効にします。",
"desc2": "このメニューの機能が分からない場合、このメニューには触れないでください。",
"label": "Valve Internalを有効"
},
"header": "その他"
},
"PluginView": {
"hidden_other": "{{count}} 個のプラグインがこのリストから非表示になっています"
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Deckyバージョン",
"header": "情報"
},
"developer_mode": {
"label": "開発者モード"
},
"notifications": {
"header": "通知",
"plugin_updates_label": "プラグインのアップデートが利用可能な場合に通知",
"decky_updates_label": "Deckyのアップデートが利用可能な場合に通知"
},
"beta": {
"header": "ベータ版への参加"
},
"other": {
"header": "その他"
},
"updates": {
"header": "アップデート"
}
},
"SettingsIndex": {
"developer_title": "開発者",
"plugins_title": "プラグイン",
"general_title": "一般"
},
"Store": {
"store_filter": {
"label": "フィルター",
"label_def": "すべて"
},
"store_search": {
"label": "検索"
},
"store_sort": {
"label": "並べ替え",
"label_def": "直近のアップデート(新しい順)"
},
"store_source": {
"desc": "すべてのプラグインのソース コードは、GitHubのSteamDeckHomebrew/decky-plugin-databaseリポジトリで入手できます。",
"label": "ソースコード"
},
"store_tabs": {
"alph_asce": "アルファベット(Z to A)",
"alph_desc": "アルファベット(A to Z)",
"title": "閲覧",
"about": "概要"
},
"store_testing_warning": {
"label": "テストストア チャンネルへようこそ",
"desc": "このストアチャンネルを使用して、最先端のプラグイン バージョンをテストできます。 すべてのユーザーがプラグインを更新できるように、必ずGitHubにフィードバックを残してください。"
},
"store_contrib": {
"desc": "Decky Plugin Storeに貢献したい場合は、GitHubのSteamDeckHomebrew/decky-plugin-templateリポジトリを確認してください。 開発と配布に関する情報は README で入手できます。",
"label": "貢献"
},
"store_testing_cta": "Decky Loaderチームを支援するために、新しいプラグインのテストを検討してください!"
},
"StoreSelect": {
"custom_store": {
"label": "カスタムストア",
"url_label": "URL"
},
"store_channel": {
"custom": "カスタム",
"default": "デフォルト",
"label": "ストアチャンネル",
"testing": "テスト"
}
},
"TitleView": {
"decky_store_desc": "Deckyストアを開く",
"settings_desc": "Decky設定を開く"
},
"Updater": {
"decky_updates": "Deckyアップデート",
"no_patch_notes_desc": "このバージョンのパッチノートはありません",
"patch_notes_desc": "パッチノート",
"updates": {
"check_button": "アップデートを確認",
"checking": "確認中",
"cur_version": "現在のバージョン: {{ver}}",
"install_button": "アップデートをインストール",
"label": "アップデート",
"lat_version": "アップデート: {{ver}} を実行中",
"reloading": "再読み込み中",
"updating": "アップデート中"
}
},
"FilePickerError": {
"errors": {
"file_not_found": "指定されたパスは無効です。 内容をご確認の上、正しく入力し直してください。",
"unknown": "不明なエラーが発生しました。 エラー内容は次のとおりです: {{raw_error}}",
"perm_denied": "選択したパスへのアクセス権がありません。選択したフォルダ/ファイルのアクセス権がユーザー(Steam Deckのdeckユーザー)に合わせて正しく設定されていることを確認してください。"
}
},
"PluginCard": {
"plugin_version_label": "プラグインバージョン",
"plugin_no_desc": "説明はありません。",
"plugin_full_access": "このプラグインはSteam Deckの全てのアクセス権を持ちます。",
"plugin_install": "インストール"
},
"RemoteDebugging": {
"remote_cef": {
"label": "リモート CEF デバッグを許可する",
"desc": "ネットワーク上のすべてのユーザーにCEFデバッガへの非認証アクセスを許可します"
}
}
}
+263
View File
@@ -0,0 +1,263 @@
{
"BranchSelect": {
"update_channel": {
"label": "업데이트 배포 채널",
"stable": "안정",
"testing": "테스트",
"prerelease": "사전 출시"
}
},
"Developer": {
"disabling": "React DevTools 비활성화",
"enabling": "React DevTools 활성화",
"5secreload": "5초 내로 다시 로드 됩니다"
},
"FilePickerIndex": {
"folder": {
"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": {
"hidden_other": "플러그인 {{count}}개 숨김"
},
"PluginListLabel": {
"hidden": "빠른 액세스 메뉴에서 숨김"
},
"PluginCard": {
"plugin_install": "설치",
"plugin_no_desc": "플러그인 설명이 제공되지 않았습니다.",
"plugin_version_label": "플러그인 버전",
"plugin_full_access": "이 플러그인은 Steam Deck의 모든 접근 권한을 가집니다."
},
"PluginInstallModal": {
"install": {
"button_idle": "설치",
"button_processing": "설치 중",
"desc": "{{artifact}} {{version}}을(를) 설치하겠습니까?",
"title": "{{artifact}} 설치"
},
"reinstall": {
"button_idle": "재설치",
"button_processing": "재설치 중",
"desc": "{{artifact}} {{version}}을(를) 재설치하겠습니까?",
"title": "{{artifact}} 재설치"
},
"update": {
"button_idle": "업데이트",
"button_processing": "업데이트 중",
"title": "{{artifact}} 업데이트",
"desc": "{{artifact}} {{version}} 업데이트를 설치하겠습니까?"
},
"no_hash": "이 플러그인은 해시 확인을 하지 않습니다, 설치에 따른 위험은 사용자가 감수해야 합니다."
},
"MultiplePluginsInstallModal": {
"title": {
"mixed_other": "플러그인 {{count}}개 수정",
"update_other": "플러그인 {{count}}개 업데이트",
"reinstall_other": "플러그인 {{count}}개 재설치",
"install_other": "플러그인 {{count}}개 설치"
},
"ok_button": {
"idle": "확인",
"loading": "작업 중"
},
"confirm": "해당 수정을 적용하겠습니까?",
"description": {
"install": "{{name}} {{version}} 플러그인 설치",
"update": "{{name}}의 {{version}} 업데이트 설치",
"reinstall": "{{name}} {{version}} 재설치"
}
},
"PluginListIndex": {
"plugin_actions": "플러그인 동작",
"reinstall": "재설치",
"reload": "다시 로드",
"uninstall": "설치 제거",
"show": "빠른 액세스 메뉴: 표시",
"hide": "빠른 액세스 메뉴: 숨김",
"update_all_other": "플러그인 {{count}}개 업데이트",
"no_plugin": "설치된 플러그인이 없습니다!",
"update_to": "{{name}}(으)로 업데이트",
"freeze": "업데이트 일시 중지",
"unfreeze": "업데이트 허용"
},
"PluginLoader": {
"decky_title": "Decky",
"decky_update_available": "{{tag_name}} 업데이트를 설치할 수 있습니다!",
"error": "오류",
"plugin_load_error": {
"message": "{{name}} 플러그인 불러오기 오류",
"toast": "{{name}} 불러오기 오류"
},
"plugin_uninstall": {
"button": "설치 제거",
"desc": "{{name}}을(를) 설치 제거하겠습니까?",
"title": "{{name}} 설치 제거"
},
"plugin_update_other": "플러그인 {{count}}개를 업데이트 할 수 있습니다!",
"plugin_error_uninstall": "{{name}} 플러그인을 불러올 때 위와 같은 예외가 발생했습니다. 이는 보통 SteamUI 최신 버전에 맞는 플러그인 업데이트가 필요할 때 발생합니다. Decky 설정의 플러그인 섹션에서 업데이트가 있는지 확인하거나 설치 제거를 시도 해 보세요."
},
"RemoteDebugging": {
"remote_cef": {
"label": "리모트 CEF 디버그 허용",
"desc": "네트워크의 모든 사용자에게 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": "URL",
"label_url": "URL에서 플러그인 설치",
"label_zip": "ZIP 파일에서 플러그인 설치"
},
"valve_internal": {
"desc1": "Valve 내부 개발자 메뉴를 활성화합니다.",
"label": "Valve 내부 개발자 메뉴 활성화",
"desc2": "이 메뉴의 기능을 모른다면 어떤 것도 건드리지 마세요."
}
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Decky 버전",
"header": "정보"
},
"beta": {
"header": "베타 참가"
},
"developer_mode": {
"label": "개발자 모드"
},
"other": {
"header": "기타"
},
"updates": {
"header": "업데이트"
},
"notifications": {
"header": "알림",
"plugin_updates_label": "플러그인 업데이트 가능",
"decky_updates_label": "Decky 업데이트 가능"
}
},
"SettingsIndex": {
"developer_title": "개발자",
"general_title": "일반",
"plugins_title": "플러그인",
"testing_title": "테스트"
},
"Store": {
"store_contrib": {
"desc": "Decky 플러그인 스토어에 기여하고 싶다면 SteamDeckHomebrew/decky-plugin-template Github 저장소를 확인하세요. 개발 및 배포에 대한 정보는 README에서 확인할 수 있습니다.",
"label": "기여하기"
},
"store_filter": {
"label": "필터",
"label_def": "모두"
},
"store_search": {
"label": "검색"
},
"store_sort": {
"label": "정렬",
"label_def": "최근 업데이트 순"
},
"store_source": {
"desc": "모든 플러그인 소스 코드는 SteamDeckHomebrew/decky-plugin-database Github 저장소에서 확인할 수 있습니다.",
"label": "소스 코드"
},
"store_tabs": {
"about": "정보",
"alph_asce": "알파벳순 (Z-A)",
"alph_desc": "알파벳순 (A-Z)",
"title": "검색",
"downloads_asce": "다운로드 수 낮은 순",
"date_desc": "최신 순",
"date_asce": "오래된 순",
"downloads_desc": "다운로드 많은 순"
},
"store_testing_cta": "새로운 플러그인을 테스트하여 Decky Loader 팀을 도와주세요!",
"store_testing_warning": {
"desc": "이 스토어 채널을 사용하여 가장 최신 버전의 플러그인을 테스트할 수 있습니다. GitHub에 피드백을 남겨서 모든 사용자가 업데이트 할 수 있게 해주세요.",
"label": "테스트 스토어 채널에 오신 것을 환영합니다"
}
},
"StoreSelect": {
"custom_store": {
"label": "사용자 지정 스토어",
"url_label": "URL"
},
"store_channel": {
"custom": "사용자 지정",
"label": "스토어 배포 채널",
"default": "기본",
"testing": "테스트"
}
},
"Updater": {
"decky_updates": "Decky 업데이트",
"no_patch_notes_desc": "이 버전에는 패치 노트가 없습니다",
"patch_notes_desc": "패치 노트",
"updates": {
"check_button": "업데이트 확인",
"checking": "확인 중",
"cur_version": "현재 버전: {{ver}}",
"install_button": "업데이트 설치",
"label": "업데이트",
"lat_version": "최신 상태: {{ver}} 실행 중",
"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 스토어 열기"
},
"Testing": {
"download": "다운로드"
}
}
+270
View File
@@ -0,0 +1,270 @@
{
"BranchSelect": {
"update_channel": {
"prerelease": "Prerelease",
"stable": "Stabiel",
"label": "Updatekanaal",
"testing": "Testing"
}
},
"Developer": {
"5secreload": "Bezig met herstarten in 5 seconden",
"disabling": "Bezig met uitschakelen van React DevTools",
"enabling": "Bezig met inschakelen van React DevTools"
},
"DropdownMultiselect": {
"button": {
"back": "Terug"
}
},
"FilePickerError": {
"errors": {
"unknown": "Er is een onbekende fout opgetreden. De foutmelding is: {{raw_error}}",
"file_not_found": "Het opgegeven pad is niet geldig. Controleer het en voer het opnieuw correct in.",
"perm_denied": "U heeft geen toegang tot de opgegeven map. Controleer of uw gebruiker (deck op Steam Deck) de juiste permissies heeft om toegang te krijgen tot de opgegeven map/het opgegeven bestand."
}
},
"FilePickerIndex": {
"files": {
"all_files": "Alle bestanden",
"file_type": "Bestandstype",
"show_hidden": "Verborgen bestanden tonen"
},
"filter": {
"created_desc": "Aanmaakdatum (nieuwste)",
"modified_asce": "Gewijzigd op (oudste)",
"modified_desc": "Gewijzigd op (nieuwste)",
"name_asce": "Naam (Z-A)",
"name_desc": "Naam (A-Z)",
"size_asce": "Grootte (kleinste)",
"size_desc": "Grootte (grootste)",
"created_asce": "Aanmaakdatum (oudste)"
},
"folder": {
"label": "Map",
"select": "Deze map gebruiken",
"show_more": "Meer bestanden tonen"
},
"file": {
"select": "Dit bestand selecteren"
}
},
"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 snelle toegang"
},
"PluginCard": {
"plugin_install": "Installeren",
"plugin_no_desc": "Geen beschrijving gegeven.",
"plugin_version_label": "Plug-inversie",
"plugin_full_access": "Deze plug-in heeft volledige toegang tot uw 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, je installeert deze op eigen risico.",
"reinstall": {
"button_idle": "Opnieuw installeren",
"button_processing": "Bezig met opnieuw te installeren",
"desc": "Weet je zeker dat je {{artifact}} {{version}} opnieuw wilt installeren?",
"title": "Installeer {{artifact}} opnieuw"
},
"update": {
"button_idle": "Bijwerken",
"button_processing": "Bezig met bijwerken",
"title": "{{artifact}} bijwerken",
"desc": "Weet je zeker dat je {{artifact}} {{version}} wilt bijwerken?"
}
},
"MultiplePluginsInstallModal": {
"title": {
"mixed_one": "Wijzig 1 plug-in",
"mixed_other": "Wijzig {{count}} plug-ins",
"update_one": "Werk 1 plug-in bij",
"update_other": "Werk {{count}} plug-ins bij",
"install_one": "Installeer 1 plug-in",
"install_other": "Installeer {{count}} plug-ins",
"reinstall_one": "Installeer 1 plug-in opnieuw",
"reinstall_other": "Installeer {{count}} plug-ins opnieuw"
},
"ok_button": {
"idle": "Bevestigen",
"loading": "Bezig"
},
"confirm": "Weet je zeker dat je de volgende wijzigingen wilt aanbrengen?",
"description": {
"install": "Installeer {{name}} {{version}}",
"update": "Werk {{name}} bij naar {{version}}",
"reinstall": "Installeer {{name}} {{version}} opnieuw"
}
},
"PluginListIndex": {
"no_plugin": "Geen plug-ins geïnstalleerd!",
"plugin_actions": "Plug-inacties",
"reload": "Herstarten",
"uninstall": "Verwijderen",
"update_to": "Bijwerken naar {{name}}",
"hide": "Verberg in snelle toegang",
"update_all_one": "Werk 1 plug-in bij",
"update_all_other": "Werk {{count}} plug-ins bij",
"reinstall": "Opnieuw installeren",
"show": "Toon in snelle toegang",
"unfreeze": "Updates toestaan",
"freeze": "Updates bevriezen"
},
"PluginLoader": {
"decky_title": "Decky",
"error": "Fout",
"plugin_load_error": {
"message": "Fout bij het laden van plug-in {{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 plug-in!",
"plugin_update_other": "Updates beschikbaar voor {{count}} plug-ins!",
"decky_update_available": "Update naar {{tag_name}} beschikbaar!",
"plugin_error_uninstall": "Het laden van {{name}} veroorzaakte een fout zoals hierboven weergegeven. Dit betekent meestal dat de plug-in moet worden bijgewerkt 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-debugger toe aan iedereen in uw netwerk",
"label": "Externe CEF-debugging toestaan"
}
},
"SettingsDeveloperIndex": {
"cef_console": {
"button": "Console openen",
"label": "CEF-console",
"desc": "Opent de CEF-console. Alleen nuttig voor foutopsporingsdoeleinden. Dingen hier zijn potentieel gevaarlijk en mogen alleen worden gebruikt als je een ontwikkelaar van plug-ins bent, of hier door een ontwikkelaar naartoe wordt geleid."
},
"header": "Overige",
"react_devtools": {
"ip_label": "IP-adres",
"label": "React DevTools inschakelen",
"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 je het inschakelt."
},
"third_party_plugins": {
"header": "Plug-ins van derden",
"label_desc": "URL",
"label_url": "Installeer plug-in via een URL",
"label_zip": "Installeer plug-in via een ZIP-bestand",
"button_install": "Installeren",
"button_zip": "Bladeren"
},
"valve_internal": {
"desc1": "Schakelt het interne ontwikkelaarsmenu van Valve in.",
"desc2": "Pas niets in dit menu aan, tenzij je weet wat het doet.",
"label": "Valve Internal inschakelen"
}
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Decky-versie",
"header": "Over"
},
"beta": {
"header": "Beta-deelname"
},
"developer_mode": {
"label": "Ontwikkelaarsmodus"
},
"other": {
"header": "Overige"
},
"updates": {
"header": "Updates"
},
"notifications": {
"decky_updates_label": "Wanneer er een Decky-update beschikbaar is",
"header": "Meldingen",
"plugin_updates_label": "Wanneer er plug-in-updates beschikbaar zijn"
}
},
"SettingsIndex": {
"developer_title": "Ontwikkelaar",
"general_title": "Algemeen",
"plugins_title": "Plug-ins",
"testing_title": "Testen"
},
"Store": {
"store_filter": {
"label": "Filter",
"label_def": "Alles"
},
"store_search": {
"label": "Zoeken"
},
"store_sort": {
"label": "Sorteren",
"label_def": "Laatst bijgewerkt (nieuwste)"
},
"store_source": {
"label": "Broncode",
"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",
"date_desc": "Nieuwste eerst",
"downloads_asce": "Minste gedownload eerst",
"downloads_desc": "Meeste gedownload eerst",
"date_asce": "Oudste eerst"
},
"store_testing_cta": "Overweeg om nieuwe plug-ins te testen om het Decky Loader-team te helpen!",
"store_contrib": {
"desc": "Als je wilt bijdragen aan de Decky Plugin Store, kijk dan in de SteamDeckHomebrew/decky-plugin-template repository op GitHub. Informatie over ontwikkeling en distributie is beschikbaar in de README.",
"label": "Bijdragen"
},
"store_testing_warning": {
"label": "Welkom bij het Testing-winkelkanaal",
"desc": "Je kunt dit winkelkanaal gebruiken om nog in ontwikkeling zijnde plug-inversies te testen. Zorg ervoor dat je feedback geeft op GitHub, zodat de plug-in voor alle gebruikers kan worden bijgewerkt."
}
},
"StoreSelect": {
"custom_store": {
"label": "Aangepaste winkel",
"url_label": "URL"
},
"store_channel": {
"custom": "Aangepast",
"default": "Standaard",
"label": "Winkelkanaal",
"testing": "Testing"
}
},
"Updater": {
"patch_notes_desc": "Patch-opmerkingen",
"updates": {
"check_button": "Op updates controleren",
"checking": "Bezig met controleren op updates",
"cur_version": "Huidige versie: {{ver}}",
"install_button": "Bijwerken",
"label": "Updates",
"lat_version": "Bijwerkt: versie {{ver}}",
"reloading": "Bezig met herstarten",
"updating": "Bezig met bijwerken"
},
"decky_updates": "Decky-updates",
"no_patch_notes_desc": "geen patch-opmerkingen voor deze versie"
},
"TitleView": {
"decky_store_desc": "Decky Store openen",
"settings_desc": "Decky-instellingen openen"
},
"Testing": {
"download": "Downloaden"
}
}
+277
View File
@@ -0,0 +1,277 @@
{
"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}}",
"unfreeze": "Odblokuj aktualizacje",
"freeze": "Zablokuj aktualizacje"
},
"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",
"testing_title": "Testowanie"
},
"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",
"date_desc": "Od najnowszych",
"downloads_desc": "Najwięcej pobrań",
"downloads_asce": "Najmniej pobrań",
"date_asce": "Od najstarszych"
},
"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"
},
"Testing": {
"download": "Pobierz"
}
}
+268
View File
@@ -0,0 +1,268 @@
{
"BranchSelect": {
"update_channel": {
"prerelease": "Pré-lançamento",
"stable": "Estável",
"testing": "Em Teste",
"label": "Canal de Atualização"
}
},
"Developer": {
"5secreload": "Recarregando em 5 segundos",
"enabling": "Habilitando React DevTools",
"disabling": "Desabilitando React DevTools"
},
"FilePickerIndex": {
"folder": {
"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": {
"hidden": "Oculto no menu de acesso rápido"
},
"PluginCard": {
"plugin_full_access": "Este plugin tem acesso total ao seu Steam Deck.",
"plugin_install": "Instalar",
"plugin_no_desc": "Nenhuma descrição fornecida.",
"plugin_version_label": "Versão do plugin"
},
"PluginInstallModal": {
"install": {
"button_idle": "Instalar",
"button_processing": "Instalando",
"desc": "Você tem certeza que deseja instalar {{artifact}} {{version}}?",
"title": "Instalar {{artifact}}"
},
"reinstall": {
"button_idle": "Reinstalar",
"button_processing": "Reinstalando",
"desc": "Tem certeza que voce deseja reinstalar {{artifact}} {{version}}?",
"title": "Reinstalar {{artifact}}"
},
"update": {
"button_idle": "Atualizar",
"button_processing": "Atualizando",
"desc": "Tem certeza que voce deseja atualizar {{artifact}} {{version}}?",
"title": "Atualizar {{artifact}}"
},
"no_hash": "Este plugin não tem um hash, você o está instalando por sua conta em risco."
},
"MultiplePluginsInstallModal": {
"title": {
"mixed_one": "Modificar {{count}} plugin",
"mixed_many": "Modificar {{count}} plugins",
"mixed_other": "Modificar {{count}} plugins",
"update_one": "Atualizar 1 plugin",
"update_many": "Atualizar {{count}} plugins",
"update_other": "Atualizar {{count}} plugins",
"install_one": "Instalar 1 plugin",
"install_many": "Instalar {{count}} plugins",
"install_other": "Instalar {{count}} plugins",
"reinstall_one": "Reinstalar 1 plugin",
"reinstall_many": "Reinstalar {{count}} plugins",
"reinstall_other": "Reinstalar {{count}} plugins"
},
"ok_button": {
"idle": "Confirmar",
"loading": "Carregando"
},
"description": {
"install": "Instalar {{name}} {{version}}",
"update": "Atualizar {{name}} para {{version}}",
"reinstall": "Reinstalar {{name}} {{version}}"
},
"confirm": "Tem certeza que deseja fazer as seguintes modificações?"
},
"PluginListIndex": {
"no_plugin": "Nenhum plugin instalado!",
"plugin_actions": "Ações do plugin",
"reinstall": "Reinstalar",
"reload": "Recarregar",
"uninstall": "Desinstalar",
"update_to": "Atualizar para {{name}}",
"show": "Acesso Rápido: Mostrar",
"update_all_one": "Atualizar 1 plugin",
"update_all_many": "Atualizar {{count}} plugins",
"update_all_other": "Atualizar {{count}} plugins",
"hide": "Acesso Rápido: Ocultar",
"freeze": "Congelar updates"
},
"PluginLoader": {
"decky_title": "Decky",
"error": "Erro",
"plugin_load_error": {
"message": "Erro ao carregar o plugin {{name}}",
"toast": "Erro ao carregar {{name}}"
},
"plugin_uninstall": {
"button": "Desinstalar",
"desc": "Você tem certeza que deseja desinstalar {{name}}?",
"title": "Desinstalar {{name}}"
},
"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çã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!"
},
"RemoteDebugging": {
"remote_cef": {
"label": "Permitir Depuração CEF Demota",
"desc": "Permitir acesso não autenticato ao depurador CEF para qualquer um na sua rede"
}
},
"SettingsDeveloperIndex": {
"cef_console": {
"button": "Abrir o Console",
"label": "Console CEF",
"desc": "Abre o Console CEF. Somente útil para fins de depuração. O material aqui é potencialmente perigoso e só deve ser usado se você for um desenvolvedor de plugin, ou direcionado até aqui por um."
},
"header": "Outros",
"react_devtools": {
"desc": "Habilita a conexão a um computador executando React DevTools. Alterar essa configuração irá recarregar a Steam. Defina o endereço IP antes de habilitar.",
"ip_label": "IP",
"label": "Habilitar React DevTools"
},
"third_party_plugins": {
"button_install": "Instalar",
"button_zip": "Navegar",
"header": "Plugins de terceiros",
"label_url": "Instalar Plugin a partir da URL",
"label_zip": "Instalar Plugin a partir de um arquivo ZIP",
"label_desc": "URL"
},
"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.",
"label": "Habilitar Menu Interno da Valve"
}
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Versão do Decky",
"header": "Sobre"
},
"developer_mode": {
"label": "Modo Deselvolvedor"
},
"other": {
"header": "Outros"
},
"updates": {
"header": "Atualizações"
},
"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",
"plugins_title": "Plugins"
},
"Store": {
"store_contrib": {
"label": "Contribuindo",
"desc": "Se você deseja contribuir para a Loja de Plugins para o Decky, confira o repositório SteamDeckHomebrew/decky-plugin-template no GitHub. Informações sobre o desenvolvimento e distribuição estão disponíveis no README."
},
"store_filter": {
"label": "Filtros",
"label_def": "Todos"
},
"store_search": {
"label": "Buscar"
},
"store_sort": {
"label": "Ordenar",
"label_def": "Último atualizado (Mais recente)"
},
"store_source": {
"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": {
"about": "Sobre",
"alph_desc": "Alfabética (A - Z)",
"title": "Navegar",
"alph_asce": "Alfabética (Z - A)"
},
"store_testing_cta": "Por favor, considere testar os novos plugins para ajudar o time do Decky Loader!",
"store_testing_warning": {
"desc": "Você pode usar este canal da loja para testar versões avançadas do plugin. Certifique-se de deixar feedback no GitHub para que o plugin possa ser atualizado para todos os usuários.",
"label": "Bem-vindo ao Canal de Testes da Loja"
}
},
"StoreSelect": {
"custom_store": {
"label": "Loja Personalizada",
"url_label": "URL"
},
"store_channel": {
"custom": "Personalizada",
"default": "Padrão",
"label": "Canal da Loja",
"testing": "Em Teste"
}
},
"Updater": {
"no_patch_notes_desc": "nenhuma nota de alteração para esta versão",
"patch_notes_desc": "Notas de alteração",
"updates": {
"check_button": "Buscar Atualizações",
"checking": "Buscando",
"cur_version": "Versão atual: {{ver}}",
"install_button": "Instalar Atualização",
"label": "Atualizações",
"lat_version": "Atualizado: rodando {{ver}}",
"reloading": "Recarregando",
"updating": "Atualizando"
},
"decky_updates": "Atualizações do Decky"
},
"PluginView": {
"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."
}
},
"TitleView": {
"decky_store_desc": "Abrir Loja Decky",
"settings_desc": "Abrir Definições Decky"
}
}
+267
View File
@@ -0,0 +1,267 @@
{
"FilePickerIndex": {
"folder": {
"select": "Usar esta pasta",
"label": "Pasta",
"show_more": "Mostrar mais ficheiros"
},
"file": {
"select": "Selecionar este ficheiro"
},
"filter": {
"size_desc": "Tamanho (maior)",
"created_asce": "Criado (mais antigo)",
"created_desc": "Criado (mais recente)",
"modified_asce": "Modificado (mais antigo)",
"modified_desc": "Modificado (mais recente)",
"name_asce": "Z-A",
"name_desc": "A-Z",
"size_asce": "Tamanho (mais pequeno)"
},
"files": {
"file_type": "Tipo de ficheiro",
"show_hidden": "Mostrar ficheiros ocultos",
"all_files": "Todos os ficheiros"
}
},
"PluginView": {
"hidden_one": "1 plugin está oculto desta lista",
"hidden_many": "{{count}} plugins estão ocultos desta lista",
"hidden_other": "{{count}} plugins estão ocultos desta lista"
},
"PluginCard": {
"plugin_full_access": "Este plugin tem acesso total à tua Steam Deck.",
"plugin_install": "Instalar",
"plugin_version_label": "Versão do plugin",
"plugin_no_desc": "Não tem descrição."
},
"PluginInstallModal": {
"install": {
"button_idle": "Instalar",
"button_processing": "Instalação em curso",
"title": "Instalar {{artifact}}",
"desc": "De certeza que queres instalar {{artifact}} {{version}}?"
},
"reinstall": {
"button_idle": "Reinstalar",
"button_processing": "Reinstalação em curso",
"title": "Reinstalar {{artifact}}",
"desc": "De certeza que queres reinstalar {{artifact}} {{version}}?"
},
"update": {
"button_idle": "Actualizar",
"button_processing": "Actualização em curso",
"title": "Actualizar {{artifact}}",
"desc": "De certeza que queres actualizar {{artifact}} {{version}}?"
},
"no_hash": "Este plugin não tem um hash, estás a instalá-lo por tua conta e risco."
},
"MultiplePluginsInstallModal": {
"title": {
"mixed_one": "Alterar 1 plugin",
"mixed_many": "Alterar {{count}} plugins",
"mixed_other": "Alterar {{count}} plugins",
"update_one": "Actualizar 1 plugin",
"update_many": "Actualizar {{count}} plugins",
"update_other": "Actualizar {{count}} plugins",
"reinstall_one": "Reinstalar 1 plugin",
"reinstall_many": "Reinstalar {{count}} plugins",
"reinstall_other": "Reinstalar {{count}} plugins",
"install_one": "Instalar 1 plugin",
"install_many": "Instalar {{count}} plugins",
"install_other": "Instalar {{count}} plugins"
},
"ok_button": {
"idle": "Confirmar",
"loading": "Em curso"
},
"description": {
"install": "Instalar {{name}} {{version}}",
"update": "Actualizar {{name}} para {{version}}",
"reinstall": "Reinstalar {{name}} {{version}}"
},
"confirm": "De certeza que queres fazer as seguintes alterações?"
},
"PluginListIndex": {
"no_plugin": "Nenhum plugin instalado!",
"reinstall": "Reinstalar",
"uninstall": "Desinstalar",
"update_to": "Actualizar para {{name}}",
"update_all_one": "Actualizar 1 plugin",
"update_all_many": "Actualizar {{count}} plugins",
"update_all_other": "Actualizar {{count}} plugins",
"plugin_actions": "Operações de plugin",
"reload": "Recarregar",
"show": "Acesso rápido: Mostrar",
"hide": "Acesso rápido: Ocultar"
},
"BranchSelect": {
"update_channel": {
"stable": "Estável",
"testing": "Em teste",
"label": "Canal de actualização",
"prerelease": "Pré-lançamento"
}
},
"Developer": {
"5secreload": "Vai recarregar em 5 segundos",
"disabling": "Desactivando React DevTools",
"enabling": "Activando React DevTools"
},
"PluginListLabel": {
"hidden": "Oculto do menu de acesso rápido"
},
"PluginLoader": {
"decky_title": "Decky",
"error": "Erro",
"plugin_load_error": {
"message": "Erro ao carregar o plugin {{name}}",
"toast": "Erro ao carregar {{name}}"
},
"plugin_uninstall": {
"button": "Desinstalar",
"title": "Desinstalar {{name}}",
"desc": "De certeza que queres desinstalar {{name}}?"
},
"decky_update_available": "Está disponível uma nova versão de {{tag_name}} !",
"plugin_update_one": "1 plugin tem actualizações disponíveis!",
"plugin_update_many": "{{count}} plugins têm actualizações disponíveis!",
"plugin_update_other": "{{count}} plugins têm actualizações disponíveis!",
"plugin_error_uninstall": "Houve uma excepção ao carregar {{name}}, como mostrado em cima. Pode ter sido porque o plugin requere a última versão do SteamUI. Verifica se há uma actualização disponível ou desinstala o plugin nas definições do Decky."
},
"SettingsDeveloperIndex": {
"cef_console": {
"button": "Abrir consola",
"label": "Consola CEF",
"desc": "Abre a consola do CEF. Só é útil para efeitos de debugging. Pode ser perigosa e só deve ser usada se és um desenvolvedor de plugins, ou se foste aqui indicado por um desenvolvedor."
},
"header": "Outros",
"react_devtools": {
"desc": "Permite a conecção a um computador que está a correr React DevTools. Mudar esta definição vai recarregar o Steam. Define o endereço de IP antes de activar.",
"ip_label": "IP",
"label": "Activar React DevTools"
},
"third_party_plugins": {
"button_install": "Instalar",
"button_zip": "Navegar",
"header": "Plugins de terceiros",
"label_desc": "URl",
"label_url": "Instalar plugin a partir dum URL",
"label_zip": "Instalar plugin a partir dum ficheiro ZIP"
},
"valve_internal": {
"label": "Activar menu interno da Valve",
"desc1": "Activa o menu interno de programador da Valve.",
"desc2": "Não toques em nada deste menu se não souberes a sua função."
}
},
"RemoteDebugging": {
"remote_cef": {
"desc": "Permitir acesso não autenticado ao debugger do CEF a qualquer pessoa na tua rede",
"label": "Permitir debugging remoto do CEF"
}
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Versão do Decky",
"header": "Sobre"
},
"beta": {
"header": "Participação na versão Beta"
},
"developer_mode": {
"label": "Modo de programador"
},
"other": {
"header": "Outros"
},
"updates": {
"header": "Actualizações"
},
"notifications": {
"decky_updates_label": "Atualização Decky disponível",
"header": "Notificações",
"plugin_updates_label": "Atualizações de plugins disponíveis"
}
},
"SettingsIndex": {
"developer_title": "Programador",
"general_title": "Geral",
"plugins_title": "Plugins"
},
"Store": {
"store_contrib": {
"label": "Contribuir",
"desc": "Se queres contribuir com um novo plugin, vai ao repositório SteamDeckHomebrew/decky-plugin-template no GitHub. No README, podes encontrar mais informação sobre desenvolvimento e distribuição."
},
"store_filter": {
"label": "Filtro",
"label_def": "Todos"
},
"store_search": {
"label": "Procurar"
},
"store_sort": {
"label": "Ordenar",
"label_def": "Última actualização (mais recente)"
},
"store_source": {
"label": "Código fonte",
"desc": "O código fonte de cada plugin está disponível no repositório SteamDeckHomebrew/decky-plugin-database no GitHub."
},
"store_tabs": {
"about": "Sobre",
"alph_asce": "Alfabeticamente (Z-A)",
"alph_desc": "Alfabeticamente (A-Z)",
"title": "Navegar"
},
"store_testing_cta": "Testa novos plugins e ajuda a equipa do Decky Loader!",
"store_testing_warning": {
"desc": "Pode usar esta versão da loja para testar versões experimentais de plugins. Certifique-se de deixar feedback no GitHub para que o plugin possa ser atualizado para todos os utilizadores.",
"label": "Bem-vindo ao Canal de Testes da Loja"
}
},
"StoreSelect": {
"custom_store": {
"url_label": "URL",
"label": "Loja personalizada"
},
"store_channel": {
"custom": "Personalizada",
"default": "Standard",
"testing": "Em teste",
"label": "Canal de loja"
}
},
"Updater": {
"decky_updates": "Actualizações do Decky",
"no_patch_notes_desc": "sem registo de alterações desta versão",
"patch_notes_desc": "Registo de alterações",
"updates": {
"check_button": "Procurar actualizações",
"checking": "Busca de actualizações em curso",
"cur_version": "Versão actual: {{ver}}",
"label": "Actualizações",
"lat_version": "Actualizado: a correr {{ver}}",
"updating": "Actualização em curso",
"reloading": "Recarregar",
"install_button": "Instalar actualização"
}
},
"FilePickerError": {
"errors": {
"perm_denied": "Não tem acesso ao diretório especificado. Por favor, verifique se o seu utilizador (deck na Steam Deck) possui as permissões correspondentes para aceder à pasta/ficheiro especificado.",
"unknown": "Ocorreu um erro desconhecido. O erro é: {{raw_error}}",
"file_not_found": "O caminho especificado não é válido. Por favor, verifique e insira-o corretamente."
}
},
"TitleView": {
"decky_store_desc": "Abrir a Loja Decky",
"settings_desc": "Abrir as Definições Decky"
},
"DropdownMultiselect": {
"button": {
"back": "Voltar"
}
}
}
+277
View File
@@ -0,0 +1,277 @@
{
"MultiplePluginsInstallModal": {
"title": {
"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": "Обновить {{count}} плагин",
"update_all_few": "Обновить {{count}} плагинов",
"update_all_many": "Обновить {{count}} плагинов",
"hide": "Быстрый доступ: Скрыть",
"reload": "Перезагрузить",
"uninstall": "Удалить",
"update_to": "Обновить на {{name}}",
"show": "Быстрый доступ: Показать",
"plugin_actions": "Действия с плагинами",
"no_plugin": "Не установлено ни одного плагина!",
"reinstall": "Переустановить",
"freeze": "Остановить обновления",
"unfreeze": "Разрешить обновления"
},
"PluginLoader": {
"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": "{{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)",
"date_asce": "Сначала старые",
"date_desc": "Сначала новые",
"downloads_asce": "Наименее загружаемые сначала",
"downloads_desc": "Наиболее загружаемые сначала"
},
"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": "Плагины",
"testing_title": "Тестирование"
},
"TitleView": {
"decky_store_desc": "Открыть магазин Decky",
"settings_desc": "Открыть настройки Decky"
},
"Testing": {
"download": "Загрузить"
}
}
+131
View File
@@ -0,0 +1,131 @@
{
"SettingsDeveloperIndex": {
"react_devtools": {
"ip_label": "IP",
"label": "Aktivizo React DevTools"
},
"third_party_plugins": {
"button_zip": "Kërko",
"header": "Shtesa të Huaj",
"button_install": "Instalo",
"label_desc": "URL",
"label_url": "Instalo Shtes Nga URL",
"label_zip": "Instalo Shtes Nga ZIP"
}
},
"BranchSelect": {
"update_channel": {
"stable": "Fiksuar",
"label": "Kanali Përditësimet"
}
},
"FilePickerIndex": {
"folder": {
"select": "Përdore këtë folder"
}
},
"PluginCard": {
"plugin_install": "Instalo",
"plugin_version_label": "Versioni Shteses"
},
"PluginInstallModal": {
"install": {
"button_idle": "Instalo",
"button_processing": "Instalohet",
"desc": "Je i sigurt që don ta instalojsh {{artifact}} {{version}}?",
"title": "Instalo {{artifact}}"
},
"no_hash": "Ky shtesë nuk ka hash, ti e instalon me rrezikun tuaj.",
"reinstall": {
"button_idle": "Riinstalo",
"button_processing": "Riinstalohet",
"desc": "Je i sigurt a don ta riinstalojsh {{artifact}} {{version}}?",
"title": "Riinstalo {{artifact}}"
},
"update": {
"button_processing": "Përditësohet",
"desc": "Je i sigurt a don ta përditësojsh {{artifact}} {{version}}?",
"title": "Përditëso {{artifact}}"
}
},
"PluginLoader": {
"decky_title": "Decky",
"plugin_uninstall": {
"title": "Çinstalo {{name}}",
"button": "Çinstalo",
"desc": "Je i sigurt që don ta çinstalojsh {{name}}?"
},
"error": "Gabim",
"plugin_error_uninstall": "Ju lutem shko nga {{name}} në Decky menu nëse don ta çinstalojsh këtë shtese.",
"plugin_update_one": "",
"plugin_update_other": ""
},
"PluginListIndex": {
"no_plugin": "Nuk ka shtesa të instaluar!",
"uninstall": "Çinstalo",
"update_all_one": "",
"update_all_other": ""
},
"SettingsGeneralIndex": {
"other": {
"header": "Të Tjera"
},
"about": {
"decky_version": "Versioni Decky"
},
"updates": {
"header": "Përmirësimet"
}
},
"SettingsIndex": {
"developer_title": "Zhvillues",
"general_title": "Gjeneral"
},
"Store": {
"store_sort": {
"label": "Rendit"
},
"store_tabs": {
"title": "Kërko"
},
"store_contrib": {
"label": "Kontributi"
},
"store_filter": {
"label": "Filtro",
"label_def": "Të Gjitha"
},
"store_search": {
"label": "Kërko"
},
"store_source": {
"label": "Kodin Burimor"
}
},
"StoreSelect": {
"store_channel": {
"label": "Kanali Dyqanit"
}
},
"Updater": {
"updates": {
"cur_version": "Versioni e tanishëme: {{ver}}"
}
},
"MultiplePluginsInstallModal": {
"title": {
"mixed_one": "",
"mixed_other": "",
"update_one": "",
"update_other": "",
"reinstall_one": "",
"reinstall_other": "",
"install_one": "",
"install_other": ""
}
},
"PluginView": {
"hidden_one": "",
"hidden_other": ""
}
}
+68
View File
@@ -0,0 +1,68 @@
{
"BranchSelect": {
"update_channel": {
"prerelease": "Förhandsversion",
"stable": "Stabil",
"testing": "Testning",
"label": "Uppdateringskanal"
}
},
"Developer": {
"5secreload": "Omladdning på 5 sekunder",
"disabling": "Inaktivera React DevTools",
"enabling": "Aktivera React DevTools"
},
"DropdownMultiselect": {
"button": {
"back": "Tillbaka"
}
},
"FilePickerError": {
"errors": {
"file_not_found": "Den angivna sökvägen är inte giltig. Kontrollera den och ange den korrekt igen.",
"unknown": "Ett okänt fel har inträffat. Det råa felet är: {{raw_error}}",
"perm_denied": "Du har inte tillgång till den angivna katalogen. Kontrollera om din användare (deck på Steam Deck) har motsvarande behörighet för att komma åt den angivna mappen/filen."
}
},
"FilePickerIndex": {
"file": {
"select": "Välj denna fil"
},
"files": {
"all_files": "Alla Filer",
"file_type": "Filtyp",
"show_hidden": "Visa dolda filer"
},
"filter": {
"created_asce": "Skapad (Äldst)",
"created_desc": "Skapad (nyast)",
"modified_asce": "Modifierad (Äldst)",
"modified_desc": "Modifierad (nyaste)",
"name_asce": "Z-A",
"name_desc": "A-Z",
"size_asce": "Storlek (minst)",
"size_desc": "Storlek (Störst)"
},
"folder": {
"label": "Mapp",
"select": "Använd denna mapp",
"show_more": "Visa fler filer"
}
},
"MultiplePluginsInstallModal": {
"description": {
"install": "Installera {{name}} {{version}}",
"reinstall": "Installera om {{name}} {{version}}",
"update": "Uppdatera {{name}} {{version}}"
},
"ok_button": {
"idle": "Bekräfta",
"loading": "Arbetar"
},
"title": {
"install_one": "Install 1 tillägg",
"install_other": "Installerar {{count}} tillägg"
},
"confirm": "Är du säker på att du vill göra följande ändringar?"
}
}
+225
View File
@@ -0,0 +1,225 @@
{
"BranchSelect": {
"update_channel": {
"prerelease": "Önsürüm",
"stable": "Stabil",
"testing": "Test",
"label": "Güncelleme Kanalı"
}
},
"DropdownMultiselect": {
"button": {
"back": "Geri"
}
},
"FilePickerIndex": {
"file": {
"select": "Bu dosyayı seçin"
},
"files": {
"all_files": "Tüm Dosyalar",
"file_type": "Dosya Türü",
"show_hidden": "Gizli Dosyaları Göster"
},
"filter": {
"created_asce": "Oluşturuldu (En Eski)",
"created_desc": "Oluşturuldu (En Yeni)",
"modified_asce": "Değiştirildi (En Eski)",
"modified_desc": "Değiştirildi (En Yeni)",
"name_asce": "Z-A",
"name_desc": "A-Z",
"size_asce": "Boyut (En Küçük)",
"size_desc": "Boyut (En Büyük)"
},
"folder": {
"label": "Klasör",
"select": "Bu klasörü kullan",
"show_more": "Daha fazla dosya göster"
}
},
"MultiplePluginsInstallModal": {
"confirm": "Aşağıdaki değişiklikleri yapmak istediğinizden emin misiniz?",
"description": {
"install": "Yükle {{name}} {{version}}",
"reinstall": "Yeniden yükle {{name}} {{version}}"
},
"ok_button": {
"idle": "Onayla",
"loading": "Çalışıyor"
},
"title": {
"reinstall_one": "1 eklentiyi yeniden yükle",
"reinstall_other": "{{count}} eklentiyi yeniden yükle",
"install_one": "1 eklenti yükle",
"install_other": "{{count}} eklenti yükle"
}
},
"PluginCard": {
"plugin_full_access": "Bu eklenti Steam Deck'inize tam erişime sahiptir.",
"plugin_install": "Yükle",
"plugin_version_label": "Eklenti Versiyonu"
},
"PluginInstallModal": {
"install": {
"button_idle": "Yükle",
"button_processing": "Yükleniyor",
"title": "Yükle {{artifact}}",
"desc": "Yüklemek istediğinizden emin misiniz {{artifact}} {{version}}?"
},
"reinstall": {
"button_idle": "Yeniden Yükle",
"desc": "Yeniden yüklemek istediğinizden emin misiniz {{artifact}} {{version}}?",
"title": "Yeniden Yükle {{artifact}}",
"button_processing": "Yeniden Yükleniyor"
},
"update": {
"button_idle": "Güncelle",
"button_processing": "Güncelleniyor",
"title": "Güncelle {{artifact}}",
"desc": "Güncellemek istediğinizden emin misiniz {{artifact}} {{version}}?"
}
},
"PluginListIndex": {
"freeze": "Güncellemeleri durdur",
"hide": "Hızlı erişim: Gizle",
"no_plugin": "Yüklü eklenti yok!",
"plugin_actions": "Eklenti İşlemleri",
"reinstall": "Yeniden Yükle",
"show": "Hızlı erişim: Göster",
"unfreeze": "Güncellemelere izin ver",
"uninstall": "Kaldır",
"update_all_one": "1 eklentiyi güncelle",
"update_all_other": "{{count}} eklentiyi güncelle"
},
"PluginListLabel": {
"hidden": "Hızlı erişim menüsünden gizlenmiş"
},
"PluginLoader": {
"decky_title": "Decky",
"decky_update_available": "{{tag_name}} güncellemesi mevcut!",
"error": "Hata",
"plugin_load_error": {
"toast": "{{name}} yüklenirken hata oluştu",
"message": "{{name}} eklentisi yüklenirken bir hata oluştu"
},
"plugin_uninstall": {
"button": "Kaldır",
"desc": "{{name}} kaldırmak istediğinizden emin misiniz?",
"title": "Kaldır {{name}}"
},
"plugin_update_one": "1 eklenti için güncelleme mevcut!",
"plugin_update_other": "{{count}} eklenti için güncelleme mevcut!"
},
"SettingsDeveloperIndex": {
"cef_console": {
"button": "Konsolu Aç"
},
"header": "Diğer",
"react_devtools": {
"ip_label": "IP"
},
"third_party_plugins": {
"button_install": "Yükle",
"button_zip": "Gözat",
"header": "Üçüncü Parti Eklentiler",
"label_desc": "URL",
"label_url": "URL'den Eklenti Yükle",
"label_zip": "ZIP Dosyasından Eklenti Yükle"
}
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Decky Versiyonu",
"header": "Hakkında"
},
"beta": {
"header": "Betaya katılım"
},
"developer_mode": {
"label": "Geliştirici modu"
},
"notifications": {
"decky_updates_label": "Decky güncellemesi mevcut",
"header": "Bildirimler",
"plugin_updates_label": "Eklenti güncellemesi mevcut"
},
"other": {
"header": "Diğer"
},
"updates": {
"header": "Güncellemeler"
}
},
"SettingsIndex": {
"developer_title": "Geliştirici",
"general_title": "Genel",
"plugins_title": "Eklentiler"
},
"Store": {
"store_contrib": {
"label": "Katkıda Bulunma"
},
"store_filter": {
"label": "Filtre",
"label_def": "Tümü"
},
"store_search": {
"label": "Ara"
},
"store_sort": {
"label": "Sırala",
"label_def": "Son Güncellenme (En Yeni)"
},
"store_source": {
"label": "Kaynak Kodu"
},
"store_tabs": {
"about": "Hakkında",
"alph_asce": "Alfabetik (Z'den A'ya)",
"alph_desc": "Alfabetik (A'dan Z'ye)",
"date_asce": "Önce En Eski",
"date_desc": "Önce En Yeni",
"downloads_desc": "Önce En Çok İndirilen",
"title": "Gözat",
"downloads_asce": "Önce En Az İndirilen"
}
},
"StoreSelect": {
"custom_store": {
"url_label": "URL"
},
"store_channel": {
"custom": "Özel",
"default": "Varsayılan"
}
},
"TitleView": {
"decky_store_desc": "Decky Mağazasını Aç",
"settings_desc": "Decky Ayarlarını Aç"
},
"Updater": {
"decky_updates": "Decky Güncellemeleri",
"patch_notes_desc": "Yama Notları",
"no_patch_notes_desc": "bu sürüm için yama notları mevcut değil",
"updates": {
"check_button": "Güncellemeleri Kontrol Et",
"checking": "Kontrol ediliyor",
"cur_version": "Mevcut Versiyon: {{ver}}",
"install_button": "Güncellemeyi Yükle",
"label": "Güncellemeler",
"updating": "Güncelleniyor"
}
},
"Testing": {
"download": "İndir"
},
"FilePickerError": {
"errors": {
"file_not_found": "Belirtilen yol geçerli değil. Lütfen yolu kontrol edin ve doğru şekilde yeniden girin."
}
},
"PluginView": {
"hidden_one": "1 eklenti bu listeden gizlenmiştir",
"hidden_other": "{{count}} eklenti bu listeden gizlenmiştir"
}
}
+222
View File
@@ -0,0 +1,222 @@
{
"BranchSelect": {
"update_channel": {
"prerelease": "Передреліз",
"testing": "Тестовий",
"label": "Канал оновлень",
"stable": "Стабільний"
}
},
"Developer": {
"5secreload": "Перезавантаження за 5 секунд",
"enabling": "Увімкнення React DevTools",
"disabling": "Вимкнення React DevTools"
},
"FilePickerIndex": {
"folder": {
"select": "Використовувати цю папку"
}
},
"PluginListLabel": {
"hidden": "Приховано з меню швидкого доступу"
},
"PluginCard": {
"plugin_full_access": "Цей плагін має повний доступ до вашого Steam Deck.",
"plugin_install": "Встановити",
"plugin_no_desc": "Опис не надано.",
"plugin_version_label": "Версія плагіна"
},
"PluginInstallModal": {
"install": {
"button_idle": "Встановити",
"button_processing": "Встановлення",
"title": "Встановити {{artifact}}",
"desc": "Ви впевнені, що хочете встановити {{artifact}} {{version}}?"
},
"reinstall": {
"button_idle": "Перевстановити",
"desc": "Ви впевнені, що хочете перевстановити {{artifact}} {{version}}?",
"title": "Перевстановити {{artifact}}",
"button_processing": "Перевстановлення"
},
"update": {
"button_idle": "Оновити",
"button_processing": "Оновлення",
"title": "Оновити {{artifact}}",
"desc": "Ви впевнені, що хочете оновити {{artifact}} {{version}}?"
},
"no_hash": "Цей плагін не має хешу, ви встановлюєте його на власний ризик."
},
"MultiplePluginsInstallModal": {
"title": {
"mixed_one": "Модифікувати 1 плагін",
"mixed_few": "Модифікувати {{count}} плагінів",
"mixed_many": "",
"reinstall_one": "Перевстановити 1 плагін",
"reinstall_few": "Перевстановити {{count}} плагінів",
"reinstall_many": "Перевстановити {{count}} плагінів",
"update_one": "Оновити 1 плагін",
"update_few": "Оновити {{count}} плагінів",
"update_many": "Оновити {{count}} плагінів",
"install_one": "Встановити 1 плагін",
"install_few": "Встановити {{count}} плагінів",
"install_many": "Встановити {{count}} плагінів"
},
"ok_button": {
"idle": "Підтвердити",
"loading": "Опрацювання"
},
"description": {
"install": "Встановити {{name}} {{version}}",
"update": "Оновити {{name}} до {{version}}",
"reinstall": "Перевстановити {{name}} {{version}}"
},
"confirm": "Ви впевнені, що хочете застосувати такі модифікації?"
},
"PluginListIndex": {
"no_plugin": "Плагінів не встановлено!",
"plugin_actions": "Дії плагінів",
"reinstall": "Перевстановити",
"reload": "Перезавантажити",
"update_to": "Оновити {{name}}",
"show": "Швидкий доступ: Показати",
"hide": "Швидкий доступ: Приховати",
"uninstall": "Видалити",
"update_all_one": "Оновити 1 плагін",
"update_all_few": "Оновити {{count}} плагінів",
"update_all_many": "Оновити {{count}} плагінів"
},
"PluginLoader": {
"decky_title": "Decky",
"decky_update_available": "Доступне оновлення до {{tag_name}}!",
"error": "Помилка",
"plugin_load_error": {
"message": "Помилка завантаження плагіна {{name}}",
"toast": "Помилка завантаження {{name}}"
},
"plugin_uninstall": {
"desc": "Ви впевнені, що хочете видалити {{name}}?",
"title": "Видалити {{name}}",
"button": "Видалення"
},
"plugin_error_uninstall": "Завантаження {{name}} спровокувало помилку показану вище. Зазвичай це означає, що плагін вимагає оновлення до нової версії SteamUI. Перевірте чи таке оновлення доступне або виконайте його видалення у налаштуваннях Decky, у секції Плагіни.",
"plugin_update_one": "Доступне оновлення для 1 плагіна!",
"plugin_update_few": "Доступне оновлення для {{count}} плагінів!",
"plugin_update_many": "Доступне оновлення для {{count}} плагінів!"
},
"SettingsDeveloperIndex": {
"cef_console": {
"button": "Відкрити консоль",
"label": "CEF-консоль",
"desc": "Відкрити CEF-консоль. Корисно тільки для дебагу. Ця штука потенційно небезпечна і повинна використовувати виключно якщо ви розробник плагіна, або якщо розробник спрямував вас сюди."
},
"header": "Інше",
"react_devtools": {
"desc": "Вмикає доступ до компʼютера із запущеним React DevTools. Зміна цього налаштування перезавантажить Steam. Вкажіть IP перед увімкненням.",
"label": "Увімкнути React DevTools",
"ip_label": "IP"
},
"third_party_plugins": {
"button_install": "Встановити",
"header": "Сторонні плагіни",
"label_desc": "URL",
"label_url": "Встановити плагін з URL",
"label_zip": "Встановити плагін з ZIP-файлу",
"button_zip": "Огляд"
},
"valve_internal": {
"desc1": "Вмикає внутрішнє розробницьке меню Valve.",
"label": "Увімкнути Valve Internal",
"desc2": "Нічого не торкайтесь у цьому меню, якщо не розумієте, що ви робите."
}
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Версія Decky",
"header": "Про нас"
},
"beta": {
"header": "Участь у Beta"
},
"developer_mode": {
"label": "Розробницький режим"
},
"other": {
"header": "Інше"
},
"updates": {
"header": "Оновлення"
}
},
"SettingsIndex": {
"developer_title": "Розробник",
"general_title": "Загальне",
"plugins_title": "Плагіни"
},
"Store": {
"store_contrib": {
"label": "Зробити внесок",
"desc": "Якщо ви бажаєте додати щось у Decky Plugin Store, завітайте у репозиторій 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": "За алфавітом (Z до A)",
"alph_desc": "За алфавітом (A до Z)",
"title": "Огляд"
},
"store_testing_cta": "Розгляньте можливість тестування нових плагінів, щоб допомогти команді Decky Loader!"
},
"StoreSelect": {
"custom_store": {
"label": "Власний магазин",
"url_label": "URL"
},
"store_channel": {
"custom": "Власний",
"default": "За замовчуванням",
"testing": "Тестування",
"label": "Канал магазину"
}
},
"Updater": {
"decky_updates": "Оновлення Decky",
"no_patch_notes_desc": "Немає нотаток до цієї версії",
"patch_notes_desc": "Перелік змін",
"updates": {
"checking": "Перевірка",
"cur_version": "Поточна версія: {{ver}}",
"install_button": "Встановити оновлення",
"label": "Оновлення",
"reloading": "Перезавантаження",
"updating": "Оновлення",
"check_button": "Перевірити оновлення",
"lat_version": "Оновлено: використовується {{ver}}"
}
},
"PluginView": {
"hidden_one": "{{count}} плагін приховано з цього списку",
"hidden_few": "{{count}} плагінів приховано з цього списку",
"hidden_many": "{{count}} плагінів приховано з цього списку"
},
"RemoteDebugging": {
"remote_cef": {
"desc": "Дозволити доступ до CEF-дебагера без аутентифікації для будь-кого у вашій мережі",
"label": "Дозволити віддалений CEF-дебагінг"
}
}
}
+263
View File
@@ -0,0 +1,263 @@
{
"BranchSelect": {
"update_channel": {
"prerelease": "发布候选",
"stable": "稳定",
"testing": "测试",
"label": "更新通道"
}
},
"Developer": {
"5secreload": "5 秒钟后重新加载",
"disabling": "正在禁用 React DevTools",
"enabling": "正在启用 React DevTools"
},
"FilePickerIndex": {
"folder": {
"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": {
"plugin_install": "安装",
"plugin_no_desc": "无描述提供。",
"plugin_version_label": "插件版本",
"plugin_full_access": "此插件可以完全访问你的 Steam Deck。"
},
"PluginInstallModal": {
"install": {
"button_idle": "安装",
"button_processing": "安装中",
"desc": "你确定要安装 {{artifact}} {{version}} 吗?",
"title": "安装 {{artifact}}"
},
"reinstall": {
"button_idle": "重新安装",
"button_processing": "正在重新安装",
"desc": "你确定要重新安装 {{artifact}} {{version}} 吗?",
"title": "重新安装 {{artifact}}"
},
"update": {
"button_idle": "更新",
"button_processing": "正在更新",
"desc": "你确定要更新 {{artifact}} {{version}} 吗?",
"title": "更新 {{artifact}}"
},
"no_hash": "此插件没有哈希校验值,你需要自行承担安装风险。"
},
"PluginListIndex": {
"no_plugin": "没有安装插件!",
"plugin_actions": "插件操作",
"reinstall": "重新安装",
"reload": "重新加载",
"uninstall": "卸载",
"update_to": "更新 {{name}}",
"update_all_other": "更新 {{count}} 个插件",
"show": "在快速访问菜单中显示",
"hide": "在快速访问菜单中隐藏",
"unfreeze": "允许更新",
"freeze": "暂停更新"
},
"PluginLoader": {
"decky_title": "Decky",
"error": "错误",
"plugin_error_uninstall": "加载 {{name}} 时引起了上述异常。这通常意味着插件需要更新以适应 SteamUI 的新版本。请检查插件是否有更新,或在 Decky 设置中的插件部分将其移除。",
"plugin_load_error": {
"message": "加载插件 {{name}} 错误",
"toast": "加载插件 {{name}} 发生了错误"
},
"plugin_uninstall": {
"button": "卸载",
"title": "卸载 {{name}}",
"desc": "你确定要卸载 {{name}} 吗?"
},
"decky_update_available": "新版本 {{tag_name}} 可用!",
"plugin_update_other": "{{count}} 个插件有更新!"
},
"RemoteDebugging": {
"remote_cef": {
"desc": "允许你的网络中的任何人无需身份验证即可访问 CEF 调试器",
"label": "允许 CEF 远程调试"
}
},
"SettingsDeveloperIndex": {
"react_devtools": {
"ip_label": "IP",
"label": "启用 React DevTools",
"desc": "允许连接到运行着 React DevTools 的计算机。更改此设置将重新加载 Steam。请在启用前设置 IP 地址。"
},
"third_party_plugins": {
"button_install": "安装",
"button_zip": "浏览文件",
"header": "第三方插件",
"label_desc": "URL",
"label_url": "从 URL 安装插件",
"label_zip": "从 ZIP 压缩文件安装插件"
},
"valve_internal": {
"desc1": "启用 Valve 内部开发者菜单。",
"desc2": "除非你知道你在干什么,否则请不要修改此菜单中的任何内容。",
"label": "启用 Valve 内部开发者"
},
"cef_console": {
"button": "打开控制台",
"label": "CEF 控制台",
"desc": "打开 CEF 控制台。仅在调试目的下使用。这列选项均有风险,请仅在您是插件开发者或是在插件开发者指导时访问使用。"
},
"header": "其他"
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Decky 版本",
"header": "关于"
},
"beta": {
"header": "参与测试"
},
"developer_mode": {
"label": "开发者模式"
},
"other": {
"header": "其他"
},
"updates": {
"header": "更新"
},
"notifications": {
"header": "通知",
"decky_updates_label": "Decky 更新可用",
"plugin_updates_label": "插件更新可用"
}
},
"SettingsIndex": {
"developer_title": "开发者",
"general_title": "通用",
"plugins_title": "插件",
"testing_title": "测试"
},
"Store": {
"store_contrib": {
"label": "贡献",
"desc": "如果你想要提交你的插件到 Decky 插件商店,请访问 GitHub 上的 SteamDeckHomebrew/decky-plugin-template 存储库。有关开发和分发插件的信息,请查看 README 文件。"
},
"store_filter": {
"label": "过滤器",
"label_def": "全部"
},
"store_search": {
"label": "搜索"
},
"store_sort": {
"label": "排序",
"label_def": "最后更新 (最新)"
},
"store_source": {
"label": "源代码",
"desc": "所有插件的源代码都可从 GitHub 上的 SteamDeckHomebrew/decky-plugin-database 存储库中获得。"
},
"store_tabs": {
"about": "关于",
"alph_asce": "字母排序 (Z 到 A)",
"alph_desc": "字母排序 (A 到 Z)",
"title": "浏览",
"downloads_desc": "下载量倒序",
"date_asce": "更新时间正序",
"date_desc": "更新时间倒序",
"downloads_asce": "下载量正序"
},
"store_testing_cta": "请考虑测试新插件以帮助 Decky Loader 团队!",
"store_testing_warning": {
"desc": "你可以使用该商店频道以体验最新版本的插件。 请在插件 Github 页面留言以使插件可以正式面向所有用户。",
"label": "欢迎来到商店测试频道"
}
},
"StoreSelect": {
"store_channel": {
"default": "默认",
"label": "商店通道",
"testing": "测试",
"custom": "自定义"
},
"custom_store": {
"label": "自定义商店",
"url_label": "URL"
}
},
"Updater": {
"decky_updates": "Decky 更新",
"no_patch_notes_desc": "此版本没有补丁说明",
"patch_notes_desc": "补丁说明",
"updates": {
"check_button": "检查更新",
"checking": "检查中",
"cur_version": "当前版本: {{ver}}",
"install_button": "安装更新",
"label": "更新",
"lat_version": "已是最新版本: {{ver}} 运行中",
"reloading": "重新加载中",
"updating": "更新中"
}
},
"MultiplePluginsInstallModal": {
"title": {
"mixed_other": "更改 {{count}} 个插件",
"update_other": "更新 {{count}} 个插件",
"reinstall_other": "重装 {{count}} 个插件",
"install_other": "安装 {{count}} 个插件"
},
"ok_button": {
"idle": "确认",
"loading": "工作中"
},
"confirm": "确定要进行以下修改吗?",
"description": {
"install": "安装 {{name}} {{version}}",
"update": "更新 {{name}} to {{version}}",
"reinstall": "重装 {{name}} {{version}}"
}
},
"PluginListLabel": {
"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 设置"
},
"Testing": {
"download": "下载"
}
}
+263
View File
@@ -0,0 +1,263 @@
{
"BranchSelect": {
"update_channel": {
"testing": "測試版",
"label": "更新頻道",
"prerelease": "預發佈",
"stable": "穩定版"
}
},
"Developer": {
"5secreload": "5 秒後重新載入",
"disabling": "正在停用 React DevTools",
"enabling": "正在啟用 React DevTools"
},
"FilePickerIndex": {
"folder": {
"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": {
"plugin_install": "安裝",
"plugin_no_desc": "未提供描述。",
"plugin_version_label": "外掛程式版本",
"plugin_full_access": "此外掛程式擁有您的 Steam Deck 的完整存取權。"
},
"PluginInstallModal": {
"install": {
"button_idle": "安裝",
"button_processing": "正在安裝",
"title": "安裝 {{artifact}}",
"desc": "您確定要安裝 {{artifact}} {{version}} 嗎?"
},
"reinstall": {
"button_idle": "重新安裝",
"button_processing": "正在重新安裝",
"desc": "您確定要重新安裝 {{artifact}} {{version}} 嗎?",
"title": "重新安裝 {{artifact}}"
},
"update": {
"button_idle": "更新",
"button_processing": "正在更新",
"desc": "您確定要更新 {{artifact}} {{version}} 嗎?",
"title": "更新 {{artifact}}"
},
"no_hash": "此外掛程式沒有提供 hash 驗證,安裝可能有風險。"
},
"PluginListIndex": {
"no_plugin": "未安裝外掛程式!",
"plugin_actions": "外掛程式操作",
"uninstall": "解除安裝",
"update_to": "更新到 {{name}}",
"reinstall": "重新安裝",
"reload": "重新載入",
"show": "快速存取:顯示",
"hide": "快速存取:隱藏",
"update_all_other": "更新 {{count}} 個外掛程式",
"freeze": "禁止更新",
"unfreeze": "允許更新"
},
"PluginLoader": {
"decky_title": "Decky",
"error": "錯誤",
"plugin_error_uninstall": "載入 {{name}} 導致上述異常。這通常意味著該外掛程式需要針對新版本的 SteamUI 進行更新。在 Decky 設定中檢查是否存在更新,或評估刪除此外掛程式。",
"plugin_load_error": {
"message": "載入外掛程式 {{name}} 發生錯誤",
"toast": "{{name}} 載入出錯"
},
"plugin_uninstall": {
"button": "解除安裝",
"title": "解除安裝 {{name}}",
"desc": "您確定要解除安裝 {{name}} 嗎?"
},
"decky_update_available": "可更新至版本 {{tag_name}}",
"plugin_update_other": "可更新 {{count}} 個外掛程式!"
},
"RemoteDebugging": {
"remote_cef": {
"desc": "允許您的網路中的任何人未經認證地存取 CEF 偵錯器",
"label": "允許 CEF 遠端偵錯"
}
},
"SettingsDeveloperIndex": {
"third_party_plugins": {
"button_zip": "瀏覽",
"label_desc": "網址",
"label_url": "從網址安裝外掛程式",
"label_zip": "從 ZIP 檔案安裝外掛程式",
"button_install": "安裝",
"header": "第三方外掛程式"
},
"valve_internal": {
"desc2": "除非您知道它的作用,否則不要碰這個選單中的任何東西。",
"desc1": "啟用 Valve 內建開發人員選單。",
"label": "啟用 Valve 內建"
},
"react_devtools": {
"desc": "啟用與執行 React DevTools 的電腦的連接。改變這個設定將重新載入 Steam。啟用前必須設定 IP 位址。",
"ip_label": "IP",
"label": "啟用 React DevTools"
},
"header": "其他",
"cef_console": {
"button": "開啟控制台",
"label": "CEF 控制台",
"desc": "開啟 CEF 控制台。僅用於偵錯。這裡的東西有潛在的風險,只有當您是一個外掛程式開發者或者被外掛程式開發者引導到這裡時,才應該使用。"
}
},
"SettingsGeneralIndex": {
"about": {
"header": "關於",
"decky_version": "Decky 版本"
},
"beta": {
"header": "參與測試"
},
"developer_mode": {
"label": "開發人員模式"
},
"other": {
"header": "其他"
},
"updates": {
"header": "更新"
},
"notifications": {
"decky_updates_label": "Decky 可更新",
"header": "通知",
"plugin_updates_label": "外掛程式有更新"
}
},
"SettingsIndex": {
"developer_title": "開發人員",
"general_title": "一般",
"plugins_title": "外掛程式",
"testing_title": "測試"
},
"Store": {
"store_contrib": {
"label": "貢獻",
"desc": "如果您想為 Decky 外掛程式商店做貢獻,請查看 GitHub 上的 SteamDeckHomebrew/decky-plugin-template 儲存庫。README 中提供了有關開發和發佈的資訊。"
},
"store_filter": {
"label": "過濾",
"label_def": "全部"
},
"store_search": {
"label": "搜尋"
},
"store_sort": {
"label": "排序",
"label_def": "最後更新 (最新)"
},
"store_source": {
"label": "原始碼",
"desc": "所有外掛程式原始碼可以在 GitHub 的 SteamDeckHomebrew/decky-plugin-database 儲存庫查看。"
},
"store_tabs": {
"about": "關於",
"alph_asce": "依字母排序 (Z 到 A)",
"alph_desc": "依字母排序 (A 到 Z)",
"title": "瀏覽",
"downloads_desc": "下載量高到低",
"downloads_asce": "下載量低到高",
"date_asce": "日期舊到新",
"date_desc": "日期新到舊"
},
"store_testing_cta": "請考慮測試新的外掛程式來幫助 Decky Loader 團隊!",
"store_testing_warning": {
"label": "歡迎來到測試頻道",
"desc": "您可以使用此商店頻道來體驗測試外掛版本。請務必在 GitHub 上留下回饋,以便為所有用戶更新該外掛程式。"
}
},
"StoreSelect": {
"custom_store": {
"label": "自訂商店",
"url_label": "網址"
},
"store_channel": {
"custom": "自訂",
"default": "預設",
"label": "商店頻道",
"testing": "測試"
}
},
"Updater": {
"decky_updates": "Decky 更新",
"no_patch_notes_desc": "這個版本沒有更新日誌",
"patch_notes_desc": "更新日誌",
"updates": {
"checking": "正在檢查",
"install_button": "安裝更新",
"label": "更新",
"lat_version": "已是最新:執行 {{ver}}",
"reloading": "正在重新載入",
"check_button": "檢查更新",
"cur_version": "目前版本:{{ver}}",
"updating": "正在更新"
}
},
"PluginView": {
"hidden_other": "{{count}} 個外掛程式已隱藏"
},
"PluginListLabel": {
"hidden": "已從快速存取選單中移除"
},
"MultiplePluginsInstallModal": {
"title": {
"mixed_other": "修改 {{count}} 個外掛程式",
"update_other": "更新 {{count}} 個外掛程式",
"reinstall_other": "重新安裝 {{count}} 個外掛程式",
"install_other": "安裝 {{count}} 個外掛程式"
},
"ok_button": {
"idle": "確定",
"loading": "執行中"
},
"confirm": "您確定要進行以下的修改嗎?",
"description": {
"install": "安裝 {{name}} {{version}}",
"update": "更新 {{name}} 的版本到 {{version}}",
"reinstall": "重新安裝 {{name}} {{version}}"
}
},
"FilePickerError": {
"errors": {
"perm_denied": "您沒有瀏覽此目錄的權限。請檢查您的使用者(Steam Deck 中的 deck 帳號)有權限瀏覽特定的資料夾或檔案。",
"unknown": "發生未知錯誤。錯誤詳細資料:{{raw_error}}",
"file_not_found": "指定路徑無效。請檢查並輸入正確路徑。"
}
},
"DropdownMultiselect": {
"button": {
"back": "返回"
}
},
"TitleView": {
"decky_store_desc": "開啟 Decky 商店",
"settings_desc": "開啟 Decky 設定"
},
"Testing": {
"download": "下載"
}
}
+3 -218
View File
@@ -1,219 +1,4 @@
# Change PyInstaller files permissions
import sys
from subprocess import call
if hasattr(sys, "_MEIPASS"):
call(["chmod", "-R", "755", sys._MEIPASS])
# Full imports
from asyncio import new_event_loop, set_event_loop, sleep
from logging import basicConfig, getLogger
from os import getenv, path
from traceback import format_exc
import aiohttp_cors
# Partial imports
from aiohttp import client_exceptions
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,
get_homebrew_path,
get_user,
get_user_group,
stop_systemd_unit,
start_systemd_unit,
)
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
USER = get_user()
GROUP = get_user_group()
HOMEBREW_PATH = get_homebrew_path()
CONFIG = {
"plugin_path": getenv("PLUGIN_PATH", path.join(HOMEBREW_PATH, "plugins")),
"chown_plugin_path": getenv("CHOWN_PLUGIN_PATH", "1") == "1",
"server_host": getenv("SERVER_HOST", "127.0.0.1"),
"server_port": int(getenv("SERVER_PORT", "1337")),
"live_reload": getenv("LIVE_RELOAD", "1") == "1",
"log_level": {"CRITICAL": 50, "ERROR": 40, "WARNING": 30, "INFO": 20, "DEBUG": 10}[
getenv("LOG_LEVEL", "INFO")
],
}
basicConfig(
level=CONFIG["log_level"], format="[%(module)s][%(levelname)s]: %(message)s"
)
logger = getLogger("Main")
def chown_plugin_dir():
code_chown = call(["chown", "-R", USER + ":" + GROUP, CONFIG["plugin_path"]])
code_chmod = call(["chmod", "-R", "555", CONFIG["plugin_path"]])
if code_chown != 0 or code_chmod != 0:
logger.error(
f"chown/chmod exited with a non-zero exit code (chown: {code_chown}, chmod:"
f" {code_chmod})"
)
if CONFIG["chown_plugin_path"] is 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, CONFIG["plugin_path"], self.loop, CONFIG["live_reload"]
)
self.plugin_browser = PluginBrowser(
CONFIG["plugin_path"], self.plugin_loader.plugins, self.plugin_loader
)
self.settings = SettingsManager("loader", path.join(HOMEBREW_PATH, "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(start_systemd_unit(REMOTE_DEBUGGER_UNIT))
else:
self.loop.create_task(stop_systemd_unit(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();")
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=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 Exception:
logger.info("Failed to inject JavaScript into tab\n" + format_exc())
pass
def run(self):
return run_app(
self.web_app,
host=CONFIG["server_host"],
port=CONFIG["server_port"],
loop=self.loop,
access_log=None,
)
# This file is needed to make the relative imports in src/ work properly.
if __name__ == "__main__":
loop = new_event_loop()
set_event_loop(loop)
PluginManager(loop).run()
from src.main import main
main()
-246
View File
@@ -1,246 +0,0 @@
import multiprocessing
from asyncio import (
Lock,
get_event_loop,
new_event_loop,
open_unix_connection,
set_event_loop,
sleep,
start_unix_server,
IncompleteReadError,
LimitOverrunError,
)
from importlib.util import module_from_spec, spec_from_file_location
from json import dumps, load, loads
from logging import getLogger
from traceback import format_exc
from os import path, setgid, setuid, environ
from signal import SIGINT, signal
from sys import exit
from time import time
import helpers
multiprocessing.set_start_method("fork")
BUFFER_LIMIT = 2**20 # 1 MiB
class PluginWrapper:
def __init__(self, file, plugin_directory, plugin_path) -> None:
self.file = file
self.plugin_path = plugin_path
self.plugin_directory = plugin_directory
self.reader = None
self.writer = None
self.socket_addr = f"/tmp/plugin_socket_{time()}"
self.method_call_lock = Lock()
self.version = None
json = load(
open(
path.join(plugin_path, plugin_directory, "plugin.json"),
"r",
encoding="utf-8",
)
)
if path.isfile(path.join(plugin_path, plugin_directory, "package.json")):
package_json = load(
open(
path.join(plugin_path, plugin_directory, "package.json"),
"r",
encoding="utf-8",
)
)
self.version = package_json["version"]
self.legacy = False
self.main_view_html = json["main_view_html"] if "main_view_html" in json else ""
self.tile_view_html = json["tile_view_html"] if "tile_view_html" in json else ""
self.legacy = self.main_view_html or self.tile_view_html
self.name = json["name"]
self.author = json["author"]
self.flags = json["flags"]
self.log = getLogger("plugin")
self.passive = not path.isfile(self.file)
def __str__(self) -> str:
return self.name
def _init(self):
try:
signal(SIGINT, lambda s, f: exit(0))
set_event_loop(new_event_loop())
if self.passive:
return
setgid(0 if "root" in self.flags else helpers.get_user_group_id())
setuid(0 if "root" in self.flags else helpers.get_user_id())
# export a bunch of environment variables to help plugin developers
environ["HOME"] = helpers.get_home_path(
"root" if "root" in self.flags else helpers.get_user()
)
environ["USER"] = "root" if "root" in self.flags else helpers.get_user()
environ["DECKY_VERSION"] = helpers.get_loader_version()
environ["DECKY_USER"] = helpers.get_user()
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(environ["DECKY_PLUGIN_SETTINGS_DIR"])
environ["DECKY_PLUGIN_RUNTIME_DIR"] = path.join(
environ["DECKY_HOME"], "data", self.plugin_directory
)
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(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
environ["DECKY_PLUGIN_AUTHOR"] = self.author
spec = spec_from_file_location("_", self.file)
module = module_from_spec(spec)
spec.loader.exec_module(module)
self.Plugin = module.Plugin
if hasattr(self.Plugin, "_main"):
get_event_loop().create_task(self.Plugin._main(self.Plugin))
get_event_loop().create_task(self._setup_socket())
get_event_loop().run_forever()
except Exception:
self.log.error("Failed to start " + self.name + "!\n" + format_exc())
exit(0)
async def _unload(self):
try:
self.log.info(
"Attempting to unload with plugin "
+ self.name
+ '\'s "_unload" function.\n'
)
if hasattr(self.Plugin, "_unload"):
await self.Plugin._unload(self.Plugin)
self.log.info("Unloaded " + self.name + "\n")
else:
self.log.info(
'Could not find "_unload" in ' + self.name + "'s main.py" + "\n"
)
except Exception:
self.log.error("Failed to unload " + self.name + "!\n" + format_exc())
exit(0)
async def _setup_socket(self):
self.socket = await start_unix_server(
self._listen_for_method_call, path=self.socket_addr, limit=BUFFER_LIMIT
)
async def _listen_for_method_call(self, reader, writer):
while True:
line = bytearray()
while True:
try:
line.extend(await reader.readuntil())
except LimitOverrunError:
line.extend(await reader.read(reader._limit))
continue
except IncompleteReadError as err:
line.extend(err.partial)
break
else:
break
data = loads(line.decode("utf-8"))
if "stop" in data:
self.log.info("Calling Loader unload function.")
await self._unload()
get_event_loop().stop()
while get_event_loop().is_running():
await sleep(0)
get_event_loop().close()
return
d = {"res": None, "success": True}
try:
d["res"] = await getattr(self.Plugin, data["method"])(
self.Plugin, **data["args"]
)
except Exception as e:
d["res"] = str(e)
d["success"] = False
finally:
writer.write((dumps(d, ensure_ascii=False) + "\n").encode("utf-8"))
await writer.drain()
async def _open_socket_if_not_exists(self):
if not self.reader:
retries = 0
while retries < 10:
try:
self.reader, self.writer = await open_unix_connection(
self.socket_addr, limit=BUFFER_LIMIT
)
return True
except Exception:
await sleep(2)
retries += 1
return False
else:
return True
def start(self):
if self.passive:
return self
multiprocessing.Process(target=self._init).start()
return self
def stop(self):
if self.passive:
return
async def _(self):
if await self._open_socket_if_not_exists():
self.writer.write(
(dumps({"stop": True}, ensure_ascii=False) + "\n").encode("utf-8")
)
await self.writer.drain()
self.writer.close()
get_event_loop().create_task(_(self))
async def execute_method(self, method_name, kwargs):
if self.passive:
raise RuntimeError(
"This plugin is passive (aka does not implement main.py)"
)
async with self.method_call_lock:
if await self._open_socket_if_not_exists():
self.writer.write(
(
dumps(
{"method": method_name, "args": kwargs}, ensure_ascii=False
)
+ "\n"
).encode("utf-8")
)
await self.writer.drain()
line = bytearray()
while True:
try:
line.extend(await self.reader.readuntil())
except LimitOverrunError:
line.extend(await self.reader.read(self.reader._limit))
continue
except IncompleteReadError as err:
line.extend(err.partial)
break
else:
break
res = loads(line.decode("utf-8"))
if not res["success"]:
raise Exception(res["res"])
return res["res"]
-15
View File
@@ -1,15 +0,0 @@
[flake8]
max-line-length = 88
[tool.ruff]
ignore = [
# Ignore line length check and let Black handle it
"E501",
# Ignore SyntaxError due to ruff not supporting pattern matching
# https://github.com/charliermarsh/ruff/issues/282
"E999",
]
# Assume Python 3.10.
target-version = "py310"
+3
View File
@@ -0,0 +1,3 @@
{
"strict": ["*"]
}
+5
View File
@@ -0,0 +1,5 @@
aiohttp==3.9.0
aiohttp-jinja2==1.5.1
aiohttp_cors==0.7.0
watchdog==2.1.7
certifi==2023.7.22
+308
View File
@@ -0,0 +1,308 @@
# Full imports
import json
# import pprint
# from pprint import pformat
# Partial imports
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, listdir, access, mkdir
from re import sub
from shutil import rmtree
from time import time
from zipfile import ZipFile
from enum import IntEnum
from typing import Dict, List, TypedDict
# Local modules
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: 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: 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: Dict[str, PluginInstallContext | List[PluginInstallContext]] = {}
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_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: str):
rv = False
try:
packageJsonPath = path.join(pluginBasePath, 'package.json')
pluginBinPath = path.join(pluginBasePath, 'bin')
if access(packageJsonPath, R_OK):
with open(packageJsonPath, "r", encoding="utf-8") as f:
packageJson = json.load(f)
if "remote_binary" in packageJson and len(packageJson["remote_binary"]) > 0:
# create bin directory if needed.
chmod(pluginBasePath, 777)
if access(pluginBasePath, W_OK):
if not path.exists(pluginBinPath):
mkdir(pluginBinPath)
if not access(pluginBinPath, W_OK):
chmod(pluginBinPath, 777)
rv = True
for remoteBinary in packageJson["remote_binary"]:
# Required Fields. If any Remote Binary is missing these fail the install.
binName = remoteBinary["name"]
binURL = remoteBinary["url"]
binHash = remoteBinary["sha256hash"]
if not await download_remote_binary_to_path(binURL, binHash, path.join(pluginBinPath, binName)):
rv = False
raise Exception(f"Error Downloading Remote Binary {binName}@{binURL} with hash {binHash} to {path.join(pluginBinPath, binName)}")
chown(self.plugin_path)
chmod(pluginBasePath, 555)
else:
rv = True
logger.debug(f"No Remote Binaries to Download")
except Exception as e:
rv = False
logger.debug(str(e))
return rv
"""Return the filename (only) for the specified plugin"""
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:
plugin = json.load(f)
if plugin['name'] == name:
return folder
except:
logger.debug(f"skipping {folder}")
async def uninstall_plugin(self, name: str):
if self.loader.watcher:
self.loader.watcher.disabled = True
tab = await get_gamepadui_tab()
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)
logger.debug("calling frontend unload for %s" % str(name))
res = await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')")
logger.debug("result of unload from UI: %s", res)
# plugins_snapshot = self.plugins.copy()
# snapshot_string = pformat(plugins_snapshot)
# logger.debug("current plugins: %s", snapshot_string)
if name in self.plugins:
logger.debug("Plugin %s was found", name)
self.plugins[name].stop(uninstall=True)
logger.debug("Plugin %s was stopped", name)
del self.plugins[name]
logger.debug("Plugin %s was removed from the dictionary", name)
self.cleanup_plugin_settings(name)
logger.debug("removing files %s" % str(name))
rmtree(plugin_dir)
except FileNotFoundError:
logger.warning(f"Plugin {name} not installed, skipping uninstallation")
except Exception as e:
logger.error(f"Plugin {name} in {plugin_dir} was not uninstalled")
logger.error(f"Error at {str(e)}", exc_info=e)
if self.loader.watcher:
self.loader.watcher.disabled = False
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
# Check if the file is a local file or a URL
if artifact.startswith("file://"):
logger.info(f"Installing {name} from local ZIP file (Version: {version})")
res_zip = BytesIO(open(artifact[7:], "rb").read())
else:
logger.info(f"Installing {name} from URL (Version: {version})")
async with ClientSession() as client:
logger.debug(f"Fetching {artifact}")
res = await client.get(artifact, ssl=get_ssl_context())
if res.status == 200:
logger.debug("Got 200. Reading...")
data = await res.read()
logger.debug(f"Read {len(data)} bytes")
res_zip = BytesIO(data)
else:
logger.fatal(f"Could not fetch from URL. {await res.text()}")
storeUrl = ""
match self.settings.getSetting("store", 0):
case 0: storeUrl = "https://plugins.deckbrew.xyz/plugins" # default
case 1: storeUrl = "https://testing.deckbrew.xyz/plugins" # testing
case 2: storeUrl = self.settings.getSetting("store-url", "https://plugins.deckbrew.xyz/plugins") # custom
case _: storeUrl = "https://plugins.deckbrew.xyz/plugins"
logger.info(f"Incrementing installs for {name} from URL {storeUrl} (version {version})")
async with ClientSession() as client:
res = await client.post(storeUrl+f"/{name}/versions/{version}/increment?isUpdate={isInstalled}", ssl=get_ssl_context())
if res.status != 200:
logger.error(f"Server did not accept install count increment request. code: {res.status}")
if res_zip and version == "dev":
with ZipFile(res_zip) as plugin_zip:
plugin_json_list = [file for file in plugin_zip.namelist() if file.endswith("/plugin.json") and file.count("/") == 1]
if len(plugin_json_list) == 0:
logger.fatal("No plugin.json found in plugin ZIP")
return
elif len(plugin_json_list) > 1:
logger.fatal("Multiple plugin.json found in plugin ZIP")
return
else:
name = sub(r"/.+$", "", plugin_json_list[0])
try:
pluginFolderPath = self.find_plugin_folder(name)
if pluginFolderPath:
isInstalled = True
except:
logger.error(f"Failed to determine if {name} is already installed, continuing anyway.")
# Check to make sure we got the file
if res_zip is None:
logger.fatal(f"Could not fetch {artifact}")
return
# If plugin is installed, uninstall it
if isInstalled:
try:
logger.debug("Uninstalling existing plugin...")
await self.uninstall_plugin(name)
except:
logger.error(f"Plugin {name} could not be uninstalled.")
# Install the plugin
logger.debug("Unzipping...")
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:
logger.info(f"Installed {name} (Version: {version})")
if name in self.loader.plugins:
self.loader.plugins[name].stop()
self.loader.plugins.pop(name, None)
await sleep(1)
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:
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: 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: 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([
f"{{ name: '{req['name']}', version: '{req['version']}', hash: '{req['hash']}', install_type: {req['install_type']}}}" for req in requests
])
tab = await get_gamepadui_tab()
await tab.open_websocket()
await tab.evaluate_js(f"DeckyPluginLoader.addMultiplePluginsInstallPrompt('{request_id}', [{js_requests_parameter}])")
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: str):
self.install_requests.pop(request_id)
def cleanup_plugin_settings(self, name: str):
"""Removes any settings related to a plugin. Propably called when a plugin is uninstalled.
Args:
name (string): The name of the plugin
"""
frozen_plugins = self.settings.getSetting("frozenPlugins", [])
if name in frozen_plugins:
frozen_plugins.remove(name)
self.settings.setSetting("frozenPlugins", frozen_plugins)
hidden_plugins = self.settings.getSetting("hiddenPlugins", [])
if name in hidden_plugins:
hidden_plugins.remove(name)
self.settings.setSetting("hiddenPlugins", hidden_plugins)
plugin_order = self.settings.getSetting("pluginOrder", [])
if name in plugin_order:
plugin_order.remove(name)
self.settings.setSetting("pluginOrder", plugin_order)
logger.debug("Removed any settings for plugin %s", name)
+6
View File
@@ -0,0 +1,6 @@
from enum import Enum
class UserType(Enum):
HOST_USER = 1
EFFECTIVE_USER = 2
ROOT = 3
+153
View File
@@ -0,0 +1,153 @@
import re
import ssl
import uuid
import os
import subprocess
from hashlib import sha256
from io import BytesIO
import certifi
from aiohttp.web import Request, Response, middleware
from aiohttp.typedefs import Handler
from aiohttp import ClientSession
from . import localplatform
from .customtypes import UserType
from logging import getLogger
REMOTE_DEBUGGER_UNIT = "steam-web-debug-portforward.service"
# global vars
csrf_token = str(uuid.uuid4())
ssl_ctx = ssl.create_default_context(cafile=certifi.where())
assets_regex = re.compile("^/plugins/.*/assets/.*")
frontend_regex = re.compile("^/frontend/.*")
logger = getLogger("Main")
def get_ssl_context():
return ssl_ctx
def get_csrf_token():
return csrf_token
@middleware
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)
# Get the default homebrew path unless a home_path is specified. home_path argument is deprecated
def get_homebrew_path() -> str:
return localplatform.get_unprivileged_path()
# Recursively create path and chown as user
def mkdir_as_user(path: str):
path = os.path.realpath(path)
os.makedirs(path, exist_ok=True)
localplatform.chown(path)
# Fetches the version of loader
def get_loader_version() -> str:
try:
with open(os.path.join(os.getcwd(), ".loader.version"), "r", encoding="utf-8") as version_file:
return version_file.readline().strip()
except Exception as e:
logger.warn(f"Failed to execute get_loader_version(): {str(e)}")
return "unknown"
# returns the appropriate system python paths
def get_system_pythonpaths() -> list[str]:
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))"],
# 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: str, binHash: str, path: str) -> bool:
rv = False
try:
if os.access(os.path.dirname(path), os.W_OK):
async with ClientSession() as client:
res = await client.get(url, ssl=get_ssl_context())
if res.status == 200:
data = BytesIO(await res.read())
remoteHash = sha256(data.getbuffer()).hexdigest()
if binHash == remoteHash:
data.seek(0)
with open(path, 'wb') as f:
f.write(data.getbuffer())
rv = True
else:
raise Exception(f"Fatal Error: Hash Mismatch for remote binary {path}@{url}")
else:
rv = False
except:
rv = False
return rv
# Deprecated
def set_user():
pass
# Deprecated
def set_user_group() -> str:
return get_user_group()
#########
# Below is legacy code, provided for backwards compatibility. This will break on windows
#########
# Get the user id hosting the plugin loader
def get_user_id() -> int:
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() # 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() # pyright: ignore [reportPrivateUsage]
# Get the effective user of the running process
def get_effective_user() -> str:
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() # 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() # pyright: ignore [reportPrivateUsage]
# Get the user owner of the given 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, 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() # pyright: ignore [reportPrivateUsage]
# Get the default home path unless a user is specified
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:
return await localplatform.service_active(unit_name)
async def stop_systemd_unit(unit_name: str) -> bool:
return await localplatform.service_stop(unit_name)
async def start_systemd_unit(unit_name: str) -> bool:
return await localplatform.service_start(unit_name)
+165 -201
View File
@@ -2,7 +2,7 @@
from asyncio import sleep
from logging import getLogger
from typing import List
from typing import Any, Callable, List, TypedDict, Dict
from aiohttp import ClientSession
from aiohttp.client_exceptions import ClientConnectorError, ClientOSError
@@ -13,38 +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()
async def _send_devtools_cmd(self, dc, receive=True):
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: Dict[str, Any], receive: bool = True):
if self.websocket:
self.cmd_id += 1
dc["id"] = self.cmd_id
@@ -56,57 +61,42 @@ 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()
res = await self._send_devtools_cmd(
{
"method": "Runtime.evaluate",
"params": {
"expression": js,
"userGesture": True,
"awaitPromise": run_async,
},
},
get_result,
)
res = await self._send_devtools_cmd({
"method": "Runtime.evaluate",
"params": {
"expression": js,
"userGesture": True,
"awaitPromise": run_async
}
}, get_result)
finally:
if manage_socket:
await self.close_websocket()
return res
async def has_global_var(self, var_name, manage_socket=True):
res = await self.evaluate_js(
f"window['{var_name}'] !== null && window['{var_name}'] !== undefined",
False,
manage_socket,
)
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 (
"result" not in res
or "result" not in res["result"]
or "value" not in res["result"]["result"]
):
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()
res = await self._send_devtools_cmd(
{
"method": "Page.close",
},
False,
)
res = await self._send_devtools_cmd({
"method": "Page.close",
}, False)
finally:
if manage_socket:
@@ -117,43 +107,33 @@ class Tab:
"""
Enables page domain notifications.
"""
await self._send_devtools_cmd(
{
"method": "Page.enable",
},
False,
)
await self._send_devtools_cmd({
"method": "Page.enable",
}, False)
async def disable(self):
"""
Disables page domain notifications.
"""
await self._send_devtools_cmd(
{
"method": "Page.disable",
},
False,
)
await self._send_devtools_cmd({
"method": "Page.disable",
}, False)
async def refresh(self, manage_socket=False):
async def refresh(self, manage_socket: bool = True):
try:
if manage_socket:
await self.open_websocket()
await self._send_devtools_cmd(
{
"method": "Page.reload",
},
False,
)
await self._send_devtools_cmd({
"method": "Page.reload",
}, False)
finally:
if manage_socket:
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
"""
@@ -161,70 +141,66 @@ class Tab:
if manage_socket:
await self.open_websocket()
await self._send_devtools_cmd({"method": "Debugger.enable"}, True)
await self._send_devtools_cmd({
"method": "Debugger.enable"
}, True)
await self._send_devtools_cmd(
{
"method": "Runtime.evaluate",
"params": {
"expression": "location.reload();",
"userGesture": True,
"awaitPromise": False,
},
},
False,
)
await self._send_devtools_cmd({
"method": "Runtime.evaluate",
"params": {
"expression": "location.reload();",
"userGesture": True,
"awaitPromise": False
}
}, False)
breakpoint_res = await self._send_devtools_cmd(
{
"method": "Debugger.setInstrumentationBreakpoint",
"params": {"instrumentation": "beforeScriptExecution"},
},
True,
)
breakpoint_res = await self._send_devtools_cmd({
"method": "Debugger.setInstrumentationBreakpoint",
"params": {
"instrumentation": "beforeScriptExecution"
}
}, 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(
{
"method": "Runtime.evaluate",
"params": {
"expression": js,
"userGesture": True,
"awaitPromise": False,
},
},
False,
)
await self._send_devtools_cmd(
{
"method": "Debugger.removeBreakpoint",
await self._send_devtools_cmd({
"method": "Runtime.evaluate",
"params": {
"breakpointId": breakpoint_res["result"]["breakpointId"]
},
},
False,
)
"expression": js,
"userGesture": True,
"awaitPromise": False
}
}, False)
for x in range(4):
await self._send_devtools_cmd({"method": "Debugger.resume"}, False)
await self._send_devtools_cmd({
"method": "Debugger.removeBreakpoint",
"params": {
"breakpointId": breakpoint_res["result"]["breakpointId"]
}
}, False)
await self._send_devtools_cmd({"method": "Debugger.disable"}, True)
for _ in range(4):
await self._send_devtools_cmd({
"method": "Debugger.resume"
}, False)
await self._send_devtools_cmd({
"method": "Debugger.disable"
}, True)
finally:
if manage_socket:
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:
@@ -259,44 +235,35 @@ class Tab:
"""
try:
wrappedjs = (
"""
function scriptFunc() {{
wrappedjs = """
function scriptFunc() {
{js}
}}
if (document.readyState === 'loading') {{
addEventListener('DOMContentLoaded', () => {{
}
if (document.readyState === 'loading') {
addEventListener('DOMContentLoaded', () => {
scriptFunc();
}});
}} else {{
});
} else {
scriptFunc();
}}
""".format(
js=js
)
if add_dom_wrapper
else js
)
}
""".format(js=js) if add_dom_wrapper else js
if manage_socket:
await self.open_websocket()
res = await self._send_devtools_cmd(
{
"method": "Page.addScriptToEvaluateOnNewDocument",
"params": {"source": wrappedjs},
},
get_result,
)
res = await self._send_devtools_cmd({
"method": "Page.addScriptToEvaluateOnNewDocument",
"params": {
"source": wrappedjs
}
}, get_result)
finally:
if manage_socket:
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`
@@ -310,33 +277,27 @@ class Tab:
if manage_socket:
await self.open_websocket()
await self._send_devtools_cmd(
{
"method": "Page.removeScriptToEvaluateOnNewDocument",
"params": {"identifier": script_id},
},
False,
)
await self._send_devtools_cmd({
"method": "Page.removeScriptToEvaluateOnNewDocument",
"params": {
"identifier": script_id
}
}, False)
finally:
if manage_socket:
await self.close_websocket()
async def has_element(self, element_name, manage_socket=True):
res = await self.evaluate_js(
f"document.getElementById('{element_name}') != null", False, manage_socket
)
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 (
"result" not in res
or "result" not in res["result"]
or "value" not in res["result"]["result"]
):
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())
@@ -348,19 +309,27 @@ class Tab:
document.head.append(style);
style.textContent = `{style}`;
}})()
""",
False,
manage_socket,
)
""", False, manage_socket)
assert result is not None
if "exceptionDetails" in result["result"]:
return {"success": False, "result": result["result"]}
return {
"success": False,
"result": result["result"]
}
return {"success": True, "result": css_id}
return {
"success": True,
"result": css_id
}
except Exception as e:
return {"success": False, "result": e}
return {
"success": False,
"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"""
@@ -370,24 +339,28 @@ class Tab:
if (style.nodeName.toLowerCase() == 'style')
style.parentNode.removeChild(style);
}})()
""",
False,
manage_socket,
)
""", False, manage_socket)
assert result is not None
if "exceptionDetails" in result["result"]:
return {"success": False, "result": result}
return {
"success": False,
"result": result
}
return {"success": True}
return {
"success": True
}
except Exception as e:
return {"success": False, "result": e}
return {
"success": False,
"result": e
}
async def get_steam_resource(self, url):
res = await self.evaluate_js(
f'(async function test() {{ return await (await fetch("{url}")).text()'
" })()",
True,
)
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):
@@ -423,52 +396,43 @@ 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:
raise ValueError("Tab not found by lambda")
raise ValueError(f"Tab not found by lambda")
return tab
SHARED_CTX_NAMES = ["SharedJSContext", "Steam Shared Context presented by Valve™", "Steam", "SP"]
CLOSEABLE_URLS = ["about:blank", "data:text/html,%3Cbody%3E%3C%2Fbody%3E"] # Closing anything other than these *really* likes to crash Steam
DO_NOT_CLOSE_URLS = ["Valve Steam Gamepad/default", "Valve%20Steam%20Gamepad"] # Steam Big Picture Mode tab
def tab_is_gamepadui(t: Tab) -> bool:
return "https://steamloopback.host/routes/" in t.url and (
t.title == "Steam Shared Context presented by Valve™"
or t.title == "Steam"
or t.title == "SP"
)
return "https://steamloopback.host/routes/" in t.url and t.title in SHARED_CTX_NAMES
async def get_gamepadui_tab() -> Tab:
tabs = await get_tabs()
tab = next((i for i in tabs if tab_is_gamepadui(i)), None)
if not tab:
raise ValueError("GamepadUI Tab not found")
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)
async def close_old_tabs():
tabs = await get_tabs()
for t in tabs:
if not t.title or (
t.title != "Steam Shared Context presented by Valve™"
and t.title != "Steam"
and t.title != "SP"
):
if not t.title or (t.title not in SHARED_CTX_NAMES and any(url in t.url for url in CLOSEABLE_URLS) and not any(url in t.url for url in DO_NOT_CLOSE_URLS)):
logger.debug("Closing tab: " + getattr(t, "title", "Untitled"))
await t.close()
await sleep(0.5)
+238
View File
@@ -0,0 +1,238 @@
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, cast
from aiohttp import web
from os.path import exists
from watchdog.events import RegexMatchingEventHandler, DirCreatedEvent, DirModifiedEvent, FileCreatedEvent, FileModifiedEvent # type: ignore
from watchdog.observers import Observer # type: ignore
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: 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: 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: DirCreatedEvent | FileCreatedEvent):
src_path = cast(str, event.src_path) #type: ignore # this is the correct type for this is in later versions of watchdog
if "__pycache__" in src_path:
return
# check to make sure this isn't a directory
if path.isdir(src_path):
return
# get the directory name of the plugin so that we can find its "main.py" and reload it; the
# file that changed is not necessarily the one that needs to be reloaded
self.logger.debug(f"file created: {src_path}")
self.maybe_reload(src_path)
def on_modified(self, event: DirModifiedEvent | FileModifiedEvent):
src_path = cast(str, event.src_path) # type: ignore
if "__pycache__" in src_path:
return
# check to make sure this isn't a directory
if path.isdir(src_path):
return
# get the directory name of the plugin so that we can find its "main.py" and reload it; the
# file that changed is not necessarily the one that needs to be reloaded
self.logger.debug(f"file modified: {src_path}")
self.maybe_reload(src_path)
class Loader:
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: Plugins = {}
self.watcher = None
self.live_reload = live_reload
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) # type: ignore
self.observer.start()
self.loop.create_task(self.enable_reload_wait())
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),
web.get("/plugins/{plugin_name}/frontend_bundle", self.handle_frontend_bundle),
web.post("/plugins/{plugin_name}/methods/{method_name}", self.handle_plugin_method_call),
web.get("/plugins/{plugin_name}/assets/{path:.*}", self.handle_plugin_frontend_assets),
web.post("/plugins/{plugin_name}/reload", self.handle_backend_reload_request),
# The following is legacy plugin code.
web.get("/plugins/load_main/{name}", self.load_plugin_main_view),
web.get("/plugins/plugin_resource/{name}/{path:.+}", self.handle_sub_route),
web.get("/steam_resource/{path:.+}", self.get_steam_resource)
])
async def enable_reload_wait(self):
if self.live_reload:
await sleep(10)
if self.watcher:
self.logger.info("Hot reload enabled")
self.watcher.disabled = False
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: web.Request):
req_lang = request.match_info["path"]
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: 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])
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"})
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: 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:
if not "debug" in plugin.flags and refresh:
self.logger.info(f"Plugin {plugin.name} is already loaded and has requested to not be re-loaded")
return
else:
self.plugins[plugin.name].stop()
self.plugins.pop(plugin.name, None)
if plugin.passive:
self.logger.info(f"Plugin {plugin.name} is passive")
self.plugins[plugin.name] = plugin.start()
self.logger.info(f"Loaded {plugin.name}")
if not batch:
self.loop.create_task(self.dispatch_plugin(plugin.name if not plugin.legacy else "$LEGACY_" + plugin.name, plugin.version))
except Exception as e:
self.logger.error(f"Could not load {file}. {e}")
print_exc()
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}')")
def import_plugins(self):
self.logger.info(f"import plugins from {self.plugin_path}")
directories = [i for i in listdir(self.plugin_path) if path.isdir(path.join(self.plugin_path, i)) and path.isfile(path.join(self.plugin_path, i, "plugin.json"))]
for directory in directories:
self.logger.info(f"found plugin: {directory}")
self.import_plugin(path.join(self.plugin_path, directory, "main.py"), directory, False, True)
async def handle_reloads(self):
while True:
args = await self.reload_queue.get()
self.import_plugin(*args) # type: ignore
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: Any = method_info["args"]
except JSONDecodeError:
args = {}
try:
if method_name.startswith("_"):
raise RuntimeError("Tried to call private method")
res["result"] = await plugin.execute_method(method_name, args)
res["success"] = True
except Exception as e:
res["result"] = str(e)
res["success"] = False
return web.json_response(res)
"""
The following methods are used to load legacy plugins, which are considered deprecated.
I made the choice to re-add them so that the first iteration/version of the react loader
can work as a drop-in replacement for the stable branch of the PluginLoader, so that we
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: 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()
ret = f"""
<script src="/legacy/library.js"></script>
<script>window.plugin_name = '{plugin.name}' </script>
<base href="http://127.0.0.1:1337/plugins/plugin_resource/{plugin.name}/">
{template_data}
"""
return web.Response(text=ret, content_type="text/html")
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)
ret = ""
file_path = path.join(self.plugin_path, plugin.plugin_directory, route_path)
with open(file_path, "r", encoding="utf-8") as resource_data:
ret = resource_data.read()
return web.Response(text=ret)
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: web.Request):
plugin_name : str = request.match_info["plugin_name"]
plugin = self.plugins[plugin_name]
await self.reload_queue.put((plugin.file, plugin.plugin_directory))
return web.Response(status=200)
+52
View File
@@ -0,0 +1,52 @@
import platform, os
ON_WINDOWS = platform.system() == "Windows"
ON_LINUX = not ON_WINDOWS
if ON_WINDOWS:
from .localplatformwin import *
from . import localplatformwin as localplatform
else:
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'''
return localplatform.get_privileged_path()
def get_unprivileged_path() -> str:
'''Get path accessible by non-elevated user. Holds plugin configuration, plugin data and plugin logs. Externally referred to as the 'Homebrew' directory'''
return localplatform.get_unprivileged_path()
def get_unprivileged_user() -> str:
'''Get user that should own files made in unprivileged path'''
return localplatform.get_unprivileged_user()
def get_chown_plugin_path() -> bool:
return os.getenv("CHOWN_PLUGIN_PATH", "1") == "1"
def get_server_host() -> str:
return os.getenv("SERVER_HOST", "127.0.0.1")
def get_server_port() -> int:
return int(os.getenv("SERVER_PORT", "1337"))
def get_live_reload() -> bool:
return os.getenv("LIVE_RELOAD", "1") == "1"
def get_keep_systemd_service() -> bool:
return os.getenv("KEEP_SYSTEMD_SERVICE", "0") == "1"
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
+220
View File
@@ -0,0 +1,220 @@
import os, pwd, grp, sys, logging
from subprocess import call, run, DEVNULL, PIPE, STDOUT
from .customtypes import UserType
logger = logging.getLogger("localplatform")
# Get the user id hosting the plugin loader
def _get_user_id() -> int:
return pwd.getpwnam(_get_user()).pw_uid
# Get the user hosting the plugin loader
def _get_user() -> str:
return get_unprivileged_user()
# Get the effective user id of the running process
def _get_effective_user_id() -> int:
return os.geteuid()
# Get the effective user of the running process
def _get_effective_user() -> str:
return pwd.getpwuid(_get_effective_user_id()).pw_name
# Get the effective user group id of the running process
def _get_effective_user_group_id() -> int:
return os.getegid()
# Get the effective user group of the running process
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) -> str:
return pwd.getpwuid(os.stat(file_path).st_uid).pw_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
def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool = True) -> bool:
user_str = ""
if user == UserType.HOST_USER:
user_str = _get_user()+":"+_get_user_group()
elif user == UserType.EFFECTIVE_USER:
user_str = _get_effective_user()+":"+_get_effective_user_group()
elif user == UserType.ROOT:
user_str = "root:root"
else:
raise Exception("Unknown User Type")
result = call(["chown", "-R", user_str, path] if recursive else ["chown", user_str, path])
return result == 0
def chmod(path : str, permissions : int, recursive : bool = True) -> bool:
if _get_effective_user_id() != 0:
return True
try:
octal_permissions = int(str(permissions), 8)
if recursive:
for root, dirs, files in os.walk(path):
for d in dirs:
os.chmod(os.path.join(root, d), octal_permissions)
for d in files:
os.chmod(os.path.join(root, d), octal_permissions)
os.chmod(path, octal_permissions)
except:
return False
return True
def folder_owner(path : str) -> UserType|None:
user_owner = _get_user_owner(path)
if (user_owner == _get_user()):
return UserType.HOST_USER
elif (user_owner == _get_effective_user()):
return UserType.EFFECTIVE_USER
else:
return None
def get_home_path(user : UserType = UserType.HOST_USER) -> str:
user_name = "root"
if user == UserType.HOST_USER:
user_name = _get_user()
elif user == UserType.EFFECTIVE_USER:
user_name = _get_effective_user()
elif user == UserType.ROOT:
pass
else:
raise Exception("Unknown User Type")
return pwd.getpwnam(user_name).pw_dir
def get_username() -> str:
return _get_user()
def setgid(user : UserType = UserType.HOST_USER):
user_id = 0
if user == UserType.HOST_USER:
user_id = _get_user_group_id()
elif user == UserType.ROOT:
pass
else:
raise Exception("Unknown user type")
os.setgid(user_id)
def setuid(user : UserType = UserType.HOST_USER):
user_id = 0
if user == UserType.HOST_USER:
user_id = _get_user_id()
elif user == UserType.ROOT:
pass
else:
raise Exception("Unknown user type")
os.setuid(user_id)
async def service_active(service_name : str) -> bool:
res = run(["systemctl", "is-active", service_name], stdout=DEVNULL, stderr=DEVNULL)
return res.returncode == 0
async def service_restart(service_name : str) -> bool:
call(["systemctl", "daemon-reload"])
cmd = ["systemctl", "restart", service_name]
res = run(cmd, stdout=PIPE, stderr=STDOUT)
return res.returncode == 0
async def service_stop(service_name : str) -> bool:
if not await service_active(service_name):
# Service isn't running. pretend we stopped it
return True
cmd = ["systemctl", "stop", service_name]
res = run(cmd, stdout=PIPE, stderr=STDOUT)
return res.returncode == 0
async def service_start(service_name : str) -> bool:
if await service_active(service_name):
# Service is running. pretend we started it
return True
cmd = ["systemctl", "start", service_name]
res = run(cmd, stdout=PIPE, stderr=STDOUT)
return res.returncode == 0
async def restart_webhelper() -> bool:
logger.info("Restarting steamwebhelper")
# TODO move to pkill
res = run(["killall", "-s", "SIGTERM", "steamwebhelper"], stdout=DEVNULL, stderr=DEVNULL)
return res.returncode == 0
def get_privileged_path() -> str:
path = os.getenv("PRIVILEGED_PATH")
if path == None:
path = get_unprivileged_path()
return path
def _parent_dir(path : str | None) -> str | None:
if path == None:
return None
if path.endswith('/'):
path = path[:-1]
return os.path.dirname(path)
def get_unprivileged_path() -> str:
path = os.getenv("UNPRIVILEGED_PATH")
if path == None:
path = _parent_dir(os.getenv("PLUGIN_PATH"))
if path == None:
logger.debug("Unprivileged path is not properly configured. Making something up!")
# Expected path of loader binary is /home/deck/homebrew/service/PluginLoader
path = _parent_dir(_parent_dir(os.path.realpath(sys.argv[0])))
if path != None and not os.path.exists(path):
path = None
if path == None:
logger.warn("Unprivileged path is not properly configured. Defaulting to /home/deck/homebrew")
path = "/home/deck/homebrew" # We give up
return path
def get_unprivileged_user() -> str:
user = os.getenv("UNPRIVILEGED_USER")
if user == None:
# Lets hope we can extract it from the unprivileged dir
dir = os.path.realpath(get_unprivileged_path())
pws = sorted(pwd.getpwall(), reverse=True, key=lambda pw: len(pw.pw_dir))
for pw in pws:
if dir.startswith(os.path.realpath(pw.pw_dir)):
user = pw.pw_name
break
if user == None:
logger.warn("Unprivileged user is not properly configured. Defaulting to 'deck'")
user = 'deck'
return user
+53
View File
@@ -0,0 +1,53 @@
from .customtypes import UserType
import os, sys
def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool = True) -> bool:
return True # Stubbed
def chmod(path : str, permissions : int, recursive : bool = True) -> bool:
return True # Stubbed
def folder_owner(path : str) -> UserType|None:
return UserType.HOST_USER # Stubbed
def get_home_path(user : UserType = UserType.HOST_USER) -> str:
return os.path.expanduser("~") # Mostly stubbed
def setgid(user : UserType = UserType.HOST_USER):
pass # Stubbed
def setuid(user : UserType = UserType.HOST_USER):
pass # Stubbed
async def service_active(service_name : str) -> bool:
return True # Stubbed
async def service_stop(service_name : str) -> bool:
return True # Stubbed
async def service_start(service_name : str) -> bool:
return True # Stubbed
async def service_restart(service_name : str) -> bool:
if service_name == "plugin_loader":
sys.exit(42)
return True # Stubbed
def get_username() -> str:
return os.getlogin()
def get_privileged_path() -> str:
'''On windows, privileged_path is equal to unprivileged_path'''
return get_unprivileged_path()
def get_unprivileged_path() -> str:
path = os.getenv("UNPRIVILEGED_PATH")
if path == None:
path = os.getenv("PRIVILEGED_PATH", os.path.join(os.path.expanduser("~"), "homebrew"))
return path
def get_unprivileged_user() -> str:
return os.getenv("UNPRIVILEGED_USER", os.getlogin())
+139
View File
@@ -0,0 +1,139 @@
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: 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.
Method should be async
'''
self.socket_addr = f"/tmp/plugin_socket_{time.time()}"
self.on_new_message = on_new_message
self.socket = None
self.reader = None
self.writer = None
async def setup_server(self):
self.socket = await asyncio.start_unix_server(self._listen_for_method_call, path=self.socket_addr, limit=BUFFER_LIMIT)
async def _open_socket_if_not_exists(self):
if not self.reader:
retries = 0
while retries < 10:
try:
self.reader, self.writer = await asyncio.open_unix_connection(self.socket_addr, limit=BUFFER_LIMIT)
return True
except:
await asyncio.sleep(2)
retries += 1
return False
else:
return True
async def get_socket_connection(self):
if not await self._open_socket_if_not_exists():
return None, None
return self.reader, self.writer
async def close_socket_connection(self):
if self.writer != None:
self.writer.close()
self.reader = None
async def read_single_line(self) -> str|None:
reader, _ = await self.get_socket_connection()
try:
assert reader
except AssertionError:
return
return await self._read_single_line(reader)
async def write_single_line(self, message : str):
_, writer = await self.get_socket_connection()
try:
assert writer
except AssertionError:
return
await self._write_single_line(writer, message)
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)) # type: ignore
continue
except asyncio.IncompleteReadError as err:
line.extend(err.partial)
break
else:
break
return line.decode("utf-8")
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: asyncio.StreamReader, writer: asyncio.StreamWriter):
while True:
line = await self._read_single_line(reader)
try:
res = await self.on_new_message(line)
except Exception:
return
if res != None:
await self._write_single_line(writer, res)
class PortSocket (UnixSocket):
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.
Method should be async
'''
super().__init__(on_new_message)
self.host = "127.0.0.1"
self.port = random.sample(range(40000, 60000), 1)[0]
async def setup_server(self):
self.socket = await asyncio.start_server(self._listen_for_method_call, host=self.host, port=self.port, limit=BUFFER_LIMIT)
async def _open_socket_if_not_exists(self):
if not self.reader:
retries = 0
while retries < 10:
try:
self.reader, self.writer = await asyncio.open_connection(host=self.host, port=self.port, limit=BUFFER_LIMIT)
return True
except:
await asyncio.sleep(2)
retries += 1
return False
else:
return True
if ON_WINDOWS:
class LocalSocket (PortSocket): # type: ignore
pass
else:
class LocalSocket (UnixSocket):
pass
+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, ON_LINUX, get_log_level, get_live_reload,
get_server_port, get_server_host, get_chown_plugin_path,
get_privileged_path, restart_webhelper)
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
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 ON_LINUX and await tab.has_global_var("deckyHasLoaded", False):
await restart_webhelper()
await tab.evaluate_js("try{if (window.deckyHasLoaded){setTimeout(() => SteamClient.Browser.RestartJSContext(), 100)}else{window.deckyHasLoaded = true;(async()=>{try{while(!window.webpackChunksteamui){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()
+185
View File
@@ -0,0 +1,185 @@
import multiprocessing
from asyncio import (Lock, get_event_loop, new_event_loop,
set_event_loop, sleep)
from importlib.util import module_from_spec, spec_from_file_location
from json import dumps, load, loads
from logging import getLogger
from traceback import format_exc
from os import path, environ
from signal import SIGINT, signal
from sys import exit, path as syspath, modules as sysmodules
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: 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 = LocalSocket(self._on_new_message)
self.version = None
json = load(open(path.join(plugin_path, plugin_directory, "plugin.json"), "r", encoding="utf-8"))
if path.isfile(path.join(plugin_path, plugin_directory, "package.json")):
package_json = load(open(path.join(plugin_path, plugin_directory, "package.json"), "r", encoding="utf-8"))
self.version = package_json["version"]
self.legacy = False
self.main_view_html = json["main_view_html"] if "main_view_html" in json else ""
self.tile_view_html = json["tile_view_html"] if "tile_view_html" in json else ""
self.legacy = self.main_view_html or self.tile_view_html
self.name = json["name"]
self.author = json["author"]
self.flags = json["flags"]
self.log = getLogger("plugin")
self.passive = not path.isfile(self.file)
def __str__(self) -> str:
return self.name
def _init(self):
try:
signal(SIGINT, lambda s, f: exit(0))
set_event_loop(new_event_loop())
if self.passive:
return
setgid(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
setuid(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
# export a bunch of environment variables to help plugin developers
environ["HOME"] = get_home_path(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
environ["USER"] = "root" if "root" in self.flags else get_username()
environ["DECKY_VERSION"] = helpers.get_loader_version()
environ["DECKY_USER"] = get_username()
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
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"))
#TODO: FIX IN A LESS CURSED WAY
keys = [key.replace("src.", "") for key in sysmodules if key.startswith("src.")]
for key in keys:
sysmodules[key] = sysmodules["src"].__dict__[key]
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
if hasattr(self.Plugin, "_migration"):
get_event_loop().run_until_complete(self.Plugin._migration(self.Plugin))
if hasattr(self.Plugin, "_main"):
get_event_loop().create_task(self.Plugin._main(self.Plugin))
get_event_loop().create_task(self.socket.setup_server())
get_event_loop().run_forever()
except:
self.log.error("Failed to start " + self.name + "!\n" + format_exc())
exit(0)
async def _unload(self):
try:
self.log.info("Attempting to unload with plugin " + self.name + "'s \"_unload\" function.\n")
if hasattr(self.Plugin, "_unload"):
await self.Plugin._unload(self.Plugin)
self.log.info("Unloaded " + self.name + "\n")
else:
self.log.info("Could not find \"_unload\" in " + self.name + "'s main.py" + "\n")
except:
self.log.error("Failed to unload " + self.name + "!\n" + format_exc())
exit(0)
async def _uninstall(self):
try:
self.log.info("Attempting to uninstall with plugin " + self.name + "'s \"_uninstall\" function.\n")
if hasattr(self.Plugin, "_uninstall"):
await self.Plugin._uninstall(self.Plugin)
self.log.info("Uninstalled " + self.name + "\n")
else:
self.log.info("Could not find \"_uninstall\" in " + self.name + "'s main.py" + "\n")
except:
self.log.error("Failed to uninstall " + self.name + "!\n" + format_exc())
exit(0)
async def _on_new_message(self, message : str) -> str|None:
data = loads(message)
if "stop" in data:
self.log.info("Calling Loader unload function.")
await self._unload()
if data.get('uninstall'):
self.log.info("Calling Loader uninstall function.")
await self._uninstall()
get_event_loop().stop()
while get_event_loop().is_running():
await sleep(0)
get_event_loop().close()
raise Exception("Closing message listener")
# 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:
d["res"] = str(e)
d["success"] = False
finally:
return dumps(d, ensure_ascii=False)
def start(self):
if self.passive:
return self
multiprocessing.Process(target=self._init).start()
return self
def stop(self, uninstall: bool = False):
if self.passive:
return
async def _(self: PluginWrapper):
await self.socket.write_single_line(dumps({ "stop": True, "uninstall": uninstall }, ensure_ascii=False))
await self.socket.close_socket_connection()
get_event_loop().create_task(_(self))
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()
await self.socket.write_single_line(dumps({ "method": method_name, "args": kwargs }, ensure_ascii=False))
line = await self.socket.read_single_line()
if line != None:
res = loads(line)
if not res["success"]:
raise Exception(res["res"])
return res["res"]
+19 -22
View File
@@ -1,45 +1,42 @@
from json import dump, load
from os import mkdir, path, listdir, rename
from shutil import chown
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,
get_user,
get_user_group,
get_user_owner,
)
from .helpers import get_homebrew_path
class SettingsManager:
def __init__(self, name, settings_directory=None) -> None:
USER = get_user()
GROUP = get_user_group()
def __init__(self, name: str, settings_directory: str | None = None) -> None:
wrong_dir = get_homebrew_path()
if settings_directory is None:
if settings_directory == None:
settings_directory = path.join(wrong_dir, "settings")
self.path = path.join(settings_directory, name + ".json")
# Create the folder with the correct permission
#Create the folder with the correct permission
if not path.exists(settings_directory):
mkdir(settings_directory)
chown(settings_directory, USER, GROUP)
# Copy all old settings file in the root directory to the correct folder
#Copy all old settings file in the root directory to the correct folder
for file in listdir(wrong_dir):
if file.endswith(".json"):
rename(path.join(wrong_dir, file), path.join(settings_directory, file))
rename(path.join(wrong_dir,file),
path.join(settings_directory, file))
self.path = path.join(settings_directory, name + ".json")
# If the owner of the settings directory is not the user, then set it as the user:
if get_user_owner(settings_directory) != USER:
chown(settings_directory, USER, GROUP)
self.settings = {}
#If the owner of the settings directory is not the user, then set it as the user:
expected_user = UserType.HOST_USER if get_chown_plugin_path() else UserType.ROOT
if folder_owner(settings_directory) != expected_user:
chown(settings_directory, expected_user, False)
self.settings: Dict[str, Any] = {}
try:
open(self.path, "x", encoding="utf-8")
except FileExistsError:
except FileExistsError as _:
self.read()
pass
@@ -55,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):
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()
+312
View File
@@ -0,0 +1,312 @@
from __future__ import annotations
from asyncio import sleep
from json.decoder import JSONDecodeError
from logging import getLogger
import os
from os import getcwd, path, remove
import shutil
from typing import List, TYPE_CHECKING, TypedDict
import zipfile
from aiohttp import ClientSession, web
from . import helpers
from .injector import get_gamepadui_tab
from .localplatform import (
ON_LINUX,
ON_WINDOWS,
chmod,
get_keep_systemd_service,
get_selinux,
service_restart,
)
from .settings import SettingsManager
if TYPE_CHECKING:
from .main import PluginManager
logger = getLogger("Updater")
class RemoteVerAsset(TypedDict):
name: str
browser_download_url: str
class RemoteVer(TypedDict):
tag_name: str
prerelease: bool
assets: List[RemoteVerAsset]
class TestingVersion(TypedDict):
id: int
name: str
link: str
head_sha: str
class Updater:
def __init__(self, context: PluginManager) -> None:
self.context = context
self.settings = self.context.settings
# Exposes updater methods to frontend
self.updater_methods = {
"get_branch": self._get_branch,
"get_version": self.get_version,
"do_update": self.do_update,
"do_restart": self.do_restart,
"check_for_updates": self.check_for_updates,
"get_testing_versions": self.get_testing_versions,
"download_testing_version": self.download_testing_version
}
self.remoteVer: RemoteVer | None = None
self.allRemoteVers: List[RemoteVer] = []
self.localVer = helpers.get_loader_version()
try:
self.currentBranch = self.get_branch(self.context.settings)
except:
self.currentBranch = 0
logger.error("Current branch could not be determined, defaulting to \"Stable\"")
if context:
context.web_app.add_routes([
web.post("/updater/{method_name}", self._handle_server_method_call)
])
context.loop.create_task(self.version_reloader())
async def _handle_server_method_call(self, request: web.Request):
method_name = request.match_info["method_name"]
try:
args = await request.json()
except JSONDecodeError:
args = {}
res = {}
try:
r = await self.updater_methods[method_name](**args) # type: ignore
res["result"] = r
res["success"] = True
except Exception as e:
res["result"] = str(e)
res["success"] = False
return web.json_response(res)
def get_branch(self, manager: SettingsManager):
ver = manager.getSetting("branch", -1)
logger.debug("current branch: %i" % ver)
if ver == -1:
logger.info("Current branch is not set, determining branch from version...")
if self.localVer.startswith("v") and "-pre" in self.localVer:
logger.info("Current version determined to be pre-release")
manager.setSetting('branch', 1)
return 1
else:
logger.info("Current version determined to be stable")
manager.setSetting('branch', 0)
return 0
return ver
async def _get_branch(self, manager: SettingsManager):
return self.get_branch(manager)
# retrieve relevant service file's url for each branch
def get_service_url(self):
logger.debug("Getting service URL")
branch = self.get_branch(self.context.settings)
match branch:
case 0:
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-release.service"
case 1 | 2:
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service"
case _:
logger.error("You have an invalid branch set... Defaulting to prerelease service, please send the logs to the devs!")
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service"
return str(url)
async def get_version(self):
return {
"current": self.localVer,
"remote": self.remoteVer,
"all": self.allRemoteVers,
"updatable": self.localVer != "unknown"
}
async def check_for_updates(self):
logger.debug("checking for updates")
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: 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))
elif selectedBranch == 1:
logger.debug("release type: pre-release")
remoteVersions = list(filter(lambda ver:ver["tag_name"].startswith("v"), remoteVersions))
else:
logger.error("release type: NOT FOUND")
raise ValueError("no valid branch found")
self.allRemoteVers = remoteVersions
logger.debug("determining release type to find, branch is %i" % selectedBranch)
if selectedBranch == 0:
logger.debug("release type: release")
self.remoteVer = next(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), None)
elif selectedBranch == 1:
logger.debug("release type: pre-release")
self.remoteVer = next(filter(lambda ver:ver["tag_name"].startswith("v"), remoteVersions), None)
else:
logger.error("release type: NOT FOUND")
raise ValueError("no valid branch found")
logger.info("Updated remote version information")
tab = await get_gamepadui_tab()
await tab.evaluate_js(f"window.DeckyPluginLoader.notifyUpdates()", False, True, False)
return await self.get_version()
async def version_reloader(self):
await sleep(30)
while True:
try:
await self.check_for_updates()
except:
pass
await sleep(60 * 60 * 6) # 6 hours
async def download_decky_binary(self, download_url: str, version: str, is_zip: bool = False):
download_filename = "PluginLoader" if ON_LINUX else "PluginLoader.exe"
download_temp_filename = download_filename + ".new"
tab = await get_gamepadui_tab()
await tab.open_websocket()
async with ClientSession() as web:
logger.debug("Downloading binary")
async with web.request("GET", download_url, ssl=helpers.get_ssl_context(), allow_redirects=True) as res:
total = int(res.headers.get('content-length', 0))
with open(path.join(getcwd(), download_temp_filename), "wb") as out:
progress = 0
raw = 0
async for c in res.content.iter_chunked(512):
out.write(c)
if total != 0:
raw += len(c)
new_progress = round((raw / total) * 100)
if progress != new_progress:
self.context.loop.create_task(tab.evaluate_js(f"window.DeckyUpdater.updateProgress({new_progress})", False, False, False))
progress = new_progress
with open(path.join(getcwd(), ".loader.version"), "w", encoding="utf-8") as out:
out.write(version)
if ON_LINUX:
remove(path.join(getcwd(), download_filename))
if (is_zip):
with zipfile.ZipFile(path.join(getcwd(), download_temp_filename), 'r') as file:
file.getinfo(download_filename).filename = download_filename + ".unzipped"
file.extract(download_filename)
remove(path.join(getcwd(), download_temp_filename))
shutil.move(path.join(getcwd(), download_filename + ".unzipped"), path.join(getcwd(), download_filename))
else:
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)
await self.do_restart()
await tab.close_websocket()
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"
for x in self.remoteVer["assets"]:
if x["name"] == download_filename:
download_url = x["browser_download_url"]
break
if download_url == None:
raise Exception("Download url not found")
service_url = self.get_service_url()
logger.debug("Retrieved service URL")
async with ClientSession() as web:
if ON_LINUX and not get_keep_systemd_service():
logger.debug("Downloading systemd service")
# download the relevant systemd service depending upon branch
async with web.request("GET", service_url, ssl=helpers.get_ssl_context(), allow_redirects=True) as res:
logger.debug("Downloading service file")
data = await res.content.read()
logger.debug(str(data))
service_file_path = path.join(getcwd(), "plugin_loader.service")
try:
with open(path.join(getcwd(), "plugin_loader.service"), "wb") as out:
out.write(data)
except Exception as e:
logger.error(f"Error at %s", exc_info=e)
with open(path.join(getcwd(), "plugin_loader.service"), "r", encoding="utf-8") as service_file:
service_data = service_file.read()
service_data = service_data.replace("${HOMEBREW_FOLDER}", helpers.get_homebrew_path())
with open(path.join(getcwd(), "plugin_loader.service"), "w", encoding="utf-8") as service_file:
service_file.write(service_data)
logger.debug("Saved service file")
logger.debug("Copying service file over current file.")
shutil.copy(service_file_path, "/etc/systemd/system/plugin_loader.service")
if not os.path.exists(path.join(getcwd(), ".systemd")):
os.mkdir(path.join(getcwd(), ".systemd"))
shutil.move(service_file_path, path.join(getcwd(), ".systemd")+"/plugin_loader.service")
await self.download_decky_binary(download_url, version)
async def do_restart(self):
await service_restart("plugin_loader")
async def get_testing_versions(self) -> List[TestingVersion]:
result: List[TestingVersion] = []
async with ClientSession() as web:
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/pulls",
headers={'X-GitHub-Api-Version': '2022-11-28'}, params={'state':'open'}, ssl=helpers.get_ssl_context()) as res:
open_prs = await res.json()
for pr in open_prs:
result.append({
"id": int(pr['number']),
"name": pr['title'],
"link": pr['html_url'],
"head_sha": pr['head']['sha'],
})
return result
async def download_testing_version(self, pr_id: int, sha_id: str):
down_id = ''
#Get all the associated workflow run for the given sha_id code hash
async with ClientSession() as web:
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/actions/runs",
headers={'X-GitHub-Api-Version': '2022-11-28'}, params={'head_sha': sha_id}, ssl=helpers.get_ssl_context()) as res:
works = await res.json()
#Iterate over the workflow_run to get the two builds if they exists
for work in works['workflow_runs']:
if ON_WINDOWS and work['name'] == 'Builder Win':
down_id=work['id']
break
elif ON_LINUX and work['name'] == 'Builder':
down_id=work['id']
break
if down_id != '':
async with ClientSession() as web:
async with web.request("GET", f"https://api.github.com/repos/SteamDeckHomebrew/decky-loader/actions/runs/{down_id}/artifacts",
headers={'X-GitHub-Api-Version': '2022-11-28'}, ssl=helpers.get_ssl_context()) as res:
jresp = await res.json()
#If the request found at least one artifact to download...
if int(jresp['total_count']) != 0:
# this assumes that the artifact we want is the first one!
down_link = f"https://nightly.link/SteamDeckHomebrew/decky-loader/actions/artifacts/{jresp['artifacts'][0]['id']}.zip"
#Then fetch it and restart itself
await self.download_decky_binary(down_link, f'PR-{pr_id}' , True)
+373
View File
@@ -0,0 +1,373 @@
from __future__ import annotations
from os import stat_result
import uuid
from json.decoder import JSONDecodeError
from os.path import splitext
import re
from traceback import format_exc
from stat import FILE_ATTRIBUTE_HIDDEN # type: ignore
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 pathlib import Path
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: PluginManager) -> None:
self.context = context
self.util_methods: Dict[str, Callable[..., Coroutine[Any, Any, Any]]] = {
"ping": self.ping,
"http_request": self.http_request,
"install_plugin": self.install_plugin,
"install_plugins": self.install_plugins,
"cancel_plugin_install": self.cancel_plugin_install,
"confirm_plugin_install": self.confirm_plugin_install,
"uninstall_plugin": self.uninstall_plugin,
"execute_in_tab": self.execute_in_tab,
"inject_css_into_tab": self.inject_css_into_tab,
"remove_css_from_tab": self.remove_css_from_tab,
"allow_remote_debugging": self.allow_remote_debugging,
"disallow_remote_debugging": self.disallow_remote_debugging,
"set_setting": self.set_setting,
"get_setting": self.get_setting,
"filepicker_ls": self.filepicker_ls,
"disable_rdt": self.disable_rdt,
"enable_rdt": self.enable_rdt,
"get_tab_id": self.get_tab_id,
"get_user_info": self.get_user_info,
}
self.logger = getLogger("Utilities")
self.rdt_proxy_server = None
self.rdt_script_id = None
self.rdt_proxy_task = None
if context:
context.web_app.add_routes([
web.post("/methods/{method_name}", self._handle_server_method_call)
])
async def _handle_server_method_call(self, request: web.Request):
method_name = request.match_info["method_name"]
try:
args = await request.json()
except JSONDecodeError:
args = {}
res = {}
try:
r = await self.util_methods[method_name](**args)
res["result"] = r
res["success"] = True
except Exception as e:
res["result"] = str(e)
res["success"] = False
return web.json_response(res)
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,
version=version,
hash=hash,
install_type=install_type
)
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: str):
return await self.context.plugin_browser.confirm_plugin_install(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: str):
return await self.context.plugin_browser.uninstall_plugin(name)
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()
return {
"status": res.status,
"headers": dict(res.headers),
"body": text
}
async def ping(self, **kwargs: Any):
return "pong"
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,
"result": result["result"]
}
return {
"success": True,
"result": result["result"]["result"].get("value")
}
except Exception as e:
return {
"success": False,
"result": e
}
async def inject_css_into_tab(self, tab: str, style: str):
try:
css_id = str(uuid.uuid4())
result = await inject_to_tab(tab,
f"""
(function() {{
const style = document.createElement('style');
style.id = "{css_id}";
document.head.append(style);
style.textContent = `{style}`;
}})()
""", False)
if result and "exceptionDetails" in result["result"]:
return {
"success": False,
"result": result["result"]
}
return {
"success": True,
"result": css_id
}
except Exception as e:
return {
"success": False,
"result": e
}
async def remove_css_from_tab(self, tab: str, css_id: str):
try:
result = await inject_to_tab(tab,
f"""
(function() {{
let style = document.getElementById("{css_id}");
if (style.nodeName.toLowerCase() == 'style')
style.parentNode.removeChild(style);
}})()
""", False)
if result and "exceptionDetails" in result["result"]:
return {
"success": False,
"result": result
}
return {
"success": True
}
except Exception as e:
return {
"success": False,
"result": e
}
async def get_setting(self, key: str, default: Any):
return self.context.settings.getSetting(key, default)
async def set_setting(self, key: str, value: Any):
return self.context.settings.setSetting(key, value)
async def allow_remote_debugging(self):
await service_start(helpers.REMOTE_DEBUGGER_UNIT)
return True
async def disallow_remote_debugging(self):
await service_stop(helpers.REMOTE_DEBUGGER_UNIT)
return True
async def filepicker_ls(self,
path : str | None = None,
include_files: bool = True,
include_folders: bool = True,
include_ext: list[str] = [],
include_hidden: bool = False,
order_by: str = "name_asc",
filter_for: str | None = None,
page: int = 1,
max: int = 1000):
if path == None:
path = get_home_path()
path_obj = Path(path).resolve()
files: List[FilePickerObj] = []
folders: List[FilePickerObj] = []
#Resolving all files/folders in the requested directory
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) # 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 len(include_ext) == 0 or 'all_files' in include_ext \
or splitext(file.name)[1].lstrip('.').upper() in (ext.upper() for ext 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 = list(filter(lambda file: re.search(filter_for, file["file"].name) != None, files))
except re.error:
files = list(filter(lambda file: file["file"].name.find(filter_for) != -1, files))
# Ordering logic
ord_arg = order_by.split("_")
ord = ord_arg[0]
rev = True if ord_arg[1] == "asc" else False
match ord:
case 'name':
files.sort(key=lambda x: x['file'].name.casefold(), reverse = rev)
folders.sort(key=lambda x: x['file'].name.casefold(), reverse = rev)
case 'modified':
files.sort(key=lambda x: x['filest'].st_mtime, reverse = not rev)
folders.sort(key=lambda x: x['filest'].st_mtime, reverse = not rev)
case 'created':
files.sort(key=lambda x: x['filest'].st_ctime, reverse = not rev)
folders.sort(key=lambda x: x['filest'].st_ctime, reverse = not rev)
case 'size':
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 = [{
"isdir": x['is_dir'],
"name": str(x['file'].name),
"realpath": str(x['file']),
"size": x['filest'].st_size,
"modified": x['filest'].st_mtime,
"created": x['filest'].st_ctime,
} for x in folders + files ]
return {
"realpath": str(path),
"files": all[(page-1)*max:(page)*max],
"total": len(all),
}
# Based on https://stackoverflow.com/a/46422554/13174603
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: StreamReader, local_writer: StreamWriter):
try:
remote_reader, remote_writer = await open_connection(
ip, port)
pipe1 = pipe(local_reader, remote_writer)
pipe2 = pipe(remote_reader, local_writer)
await gather(pipe1, pipe2)
finally:
local_writer.close()
self.rdt_proxy_server = start_server(handle_client, "127.0.0.1", port)
self.rdt_proxy_task = self.context.loop.create_task(self.rdt_proxy_server)
def stop_rdt_proxy(self):
if self.rdt_proxy_server != None:
self.rdt_proxy_server.close()
if self.rdt_proxy_task:
self.rdt_proxy_task.cancel()
async def _enable_rdt(self):
# TODO un-hardcode port
try:
self.stop_rdt_proxy()
ip = self.context.settings.getSetting("developer.rdt.ip", None)
if ip != None:
self.logger.info("Connecting to React DevTools at " + ip)
async with ClientSession() as web:
res = await web.request("GET", "http://" + ip + ":8097", ssl=helpers.get_ssl_context())
script = """
if (!window.deckyHasConnectedRDT) {
window.deckyHasConnectedRDT = true;
// This fixes the overlay when hovering over an element in RDT
Object.defineProperty(window, '__REACT_DEVTOOLS_TARGET_WINDOW__', {
enumerable: true,
configurable: true,
get: function() {
return (GamepadNavTree?.m_context?.m_controller || FocusNavController)?.m_ActiveContext?.ActiveWindow || window;
}
});
""" + await res.text() + "\n}"
if res.status != 200:
self.logger.error("Failed to connect to React DevTools at " + ip)
return False
self.start_rdt_proxy(ip, 8097)
self.logger.info("Connected to React DevTools, loading script")
tab = await get_gamepadui_tab()
# RDT needs to load before React itself to work.
await close_old_tabs()
result = await tab.reload_and_evaluate(script)
self.logger.info(result)
except Exception:
self.logger.error("Failed to connect to React DevTools")
self.logger.error(format_exc())
async def enable_rdt(self):
self.context.loop.create_task(self._enable_rdt())
async def disable_rdt(self):
self.logger.info("Disabling React DevTools")
tab = await get_gamepadui_tab()
self.rdt_script_id = None
await close_old_tabs()
await tab.evaluate_js("SteamClient.Browser.RestartJSContext();", False, True, False)
self.logger.info("React DevTools disabled")
async def get_user_info(self) -> Dict[str, str]:
return {
"username": get_username(),
"path": get_home_path()
}
async def get_tab_id(self, name: str):
return (await get_tab(name)).id
-258
View File
@@ -1,258 +0,0 @@
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 subprocess import call
from aiohttp import ClientSession, web
import helpers
from injector import get_gamepadui_tab, inject_to_tab
from settings import SettingsManager
logger = getLogger("Updater")
class Updater:
def __init__(self, context) -> None:
self.context = context
self.settings = self.context.settings
# Exposes updater methods to frontend
self.updater_methods = {
"get_branch": self._get_branch,
"get_version": self.get_version,
"do_update": self.do_update,
"do_restart": self.do_restart,
"check_for_updates": self.check_for_updates,
}
self.remoteVer = None
self.allRemoteVers = None
try:
self.localVer = helpers.get_loader_version()
except:
self.localVer = False
try:
self.currentBranch = self.get_branch(self.context.settings)
except:
self.currentBranch = 0
logger.error(
'Current branch could not be determined, defaulting to "Stable"'
)
if context:
context.web_app.add_routes(
[web.post("/updater/{method_name}", self._handle_server_method_call)]
)
context.loop.create_task(self.version_reloader())
async def _handle_server_method_call(self, request):
method_name = request.match_info["method_name"]
try:
args = await request.json()
except JSONDecodeError:
args = {}
res = {}
try:
r = await self.updater_methods[method_name](**args)
res["result"] = r
res["success"] = True
except Exception as e:
res["result"] = str(e)
res["success"] = False
return web.json_response(res)
def get_branch(self, manager: SettingsManager):
ver = manager.getSetting("branch", -1)
logger.debug("current branch: %i" % ver)
if ver == -1:
logger.info("Current branch is not set, determining branch from version...")
if self.localVer.startswith("v") and self.localVer.find("-pre"):
logger.info("Current version determined to be pre-release")
return 1
else:
logger.info("Current version determined to be stable")
return 0
return ver
async def _get_branch(self, manager: SettingsManager):
return self.get_branch(manager)
# retrieve relevant service file's url for each branch
def get_service_url(self):
logger.debug("Getting service URL")
branch = self.get_branch(self.context.settings)
match branch:
case 0:
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-release.service"
case 1 | 2:
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service"
case _:
logger.error(
"You have an invalid branch set... Defaulting to prerelease"
" service, please send the logs to the devs!"
)
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service"
return str(url)
async def get_version(self):
if self.localVer:
return {
"current": self.localVer,
"remote": self.remoteVer,
"all": self.allRemoteVers,
"updatable": self.localVer != None,
}
else:
return {
"current": "unknown",
"remote": self.remoteVer,
"all": self.allRemoteVers,
"updatable": False,
}
async def check_for_updates(self):
logger.debug("checking for updates")
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()
self.allRemoteVers = remoteVersions
logger.debug("determining release type to find, branch is %i" % selectedBranch)
if selectedBranch == 0:
logger.debug("release type: release")
self.remoteVer = next(
filter(
lambda ver: ver["tag_name"].startswith("v")
and not ver["prerelease"]
and ver["tag_name"],
remoteVersions,
),
None,
)
elif selectedBranch == 1:
logger.debug("release type: pre-release")
self.remoteVer = next(
filter(
lambda ver: ver["prerelease"]
and ver["tag_name"].startswith("v")
and ver["tag_name"].find("-pre"),
remoteVersions,
),
None,
)
else:
logger.error("release type: NOT FOUND")
raise ValueError("no valid branch found")
logger.info("Updated remote version information")
tab = await get_gamepadui_tab()
await tab.evaluate_js(
f"window.DeckyPluginLoader.notifyUpdates()", False, True, False
)
return await self.get_version()
async def version_reloader(self):
await sleep(30)
while True:
try:
await self.check_for_updates()
except:
pass
await sleep(60 * 60 * 6) # 6 hours
async def do_update(self):
logger.debug("Starting update.")
version = self.remoteVer["tag_name"]
download_url = self.remoteVer["assets"][0]["browser_download_url"]
service_url = self.get_service_url()
logger.debug("Retrieved service URL")
tab = await get_gamepadui_tab()
await tab.open_websocket()
async with ClientSession() as web:
logger.debug("Downloading systemd service")
# download the relevant systemd service depending upon branch
async with web.request(
"GET", service_url, ssl=helpers.get_ssl_context(), allow_redirects=True
) as res:
logger.debug("Downloading service file")
data = await res.content.read()
logger.debug(str(data))
service_file_path = path.join(getcwd(), "plugin_loader.service")
try:
with open(path.join(getcwd(), "plugin_loader.service"), "wb") as out:
out.write(data)
except Exception as e:
logger.error(f"Error at %s", exc_info=e)
with open(
path.join(getcwd(), "plugin_loader.service"), "r", encoding="utf-8"
) as service_file:
service_data = service_file.read()
service_data = service_data.replace(
"${HOMEBREW_FOLDER}", helpers.get_homebrew_path()
)
with open(
path.join(getcwd(), "plugin_loader.service"), "w", encoding="utf-8"
) as service_file:
service_file.write(service_data)
logger.debug("Saved service file")
logger.debug("Copying service file over current file.")
shutil.copy(service_file_path, "/etc/systemd/system/plugin_loader.service")
if not os.path.exists(path.join(getcwd(), ".systemd")):
os.mkdir(path.join(getcwd(), ".systemd"))
shutil.move(
service_file_path,
path.join(getcwd(), ".systemd") + "/plugin_loader.service",
)
logger.debug("Downloading binary")
async with web.request(
"GET", download_url, ssl=helpers.get_ssl_context(), allow_redirects=True
) as res:
total = int(res.headers.get("content-length", 0))
# we need to not delete the binary until we have downloaded the new binary!
try:
remove(path.join(getcwd(), "PluginLoader"))
except:
pass
with open(path.join(getcwd(), "PluginLoader"), "wb") as out:
progress = 0
raw = 0
async for c in res.content.iter_chunked(512):
out.write(c)
raw += len(c)
new_progress = round((raw / total) * 100)
if progress != new_progress:
self.context.loop.create_task(
tab.evaluate_js(
f"window.DeckyUpdater.updateProgress({new_progress})",
False,
False,
False,
)
)
progress = new_progress
with open(
path.join(getcwd(), ".loader.version"), "w", encoding="utf-8"
) as out:
out.write(version)
call(["chmod", "+x", path.join(getcwd(), "PluginLoader")])
logger.info("Updated loader installation.")
await tab.evaluate_js("window.DeckyUpdater.finish()", False, False)
await self.do_restart()
await tab.close_websocket()
async def do_restart(self):
call(["systemctl", "daemon-reload"])
call(["systemctl", "restart", "plugin_loader"])
-263
View File
@@ -1,263 +0,0 @@
import uuid
import os
from json.decoder import JSONDecodeError
from traceback import format_exc
from asyncio import start_server, gather, open_connection
from aiohttp import ClientSession, web
from logging import getLogger
from injector import inject_to_tab, get_gamepadui_tab, close_old_tabs
import helpers
class Utilities:
def __init__(self, context) -> None:
self.context = context
self.util_methods = {
"ping": self.ping,
"http_request": self.http_request,
"install_plugin": self.install_plugin,
"cancel_plugin_install": self.cancel_plugin_install,
"confirm_plugin_install": self.confirm_plugin_install,
"uninstall_plugin": self.uninstall_plugin,
"execute_in_tab": self.execute_in_tab,
"inject_css_into_tab": self.inject_css_into_tab,
"remove_css_from_tab": self.remove_css_from_tab,
"allow_remote_debugging": self.allow_remote_debugging,
"disallow_remote_debugging": self.disallow_remote_debugging,
"set_setting": self.set_setting,
"get_setting": self.get_setting,
"filepicker_ls": self.filepicker_ls,
"disable_rdt": self.disable_rdt,
"enable_rdt": self.enable_rdt,
}
self.logger = getLogger("Utilities")
self.rdt_proxy_server = None
self.rdt_script_id = None
self.rdt_proxy_task = None
if context:
context.web_app.add_routes(
[web.post("/methods/{method_name}", self._handle_server_method_call)]
)
async def _handle_server_method_call(self, request):
method_name = request.match_info["method_name"]
try:
args = await request.json()
except JSONDecodeError:
args = {}
res = {}
try:
r = await self.util_methods[method_name](**args)
res["result"] = r
res["success"] = True
except Exception as e:
res["result"] = str(e)
res["success"] = False
return web.json_response(res)
async def install_plugin(
self, artifact="", name="No name", version="dev", hash=False
):
return await self.context.plugin_browser.request_plugin_install(
artifact=artifact, name=name, version=version, hash=hash
)
async def confirm_plugin_install(self, request_id):
return await self.context.plugin_browser.confirm_plugin_install(request_id)
def cancel_plugin_install(self, request_id):
return self.context.plugin_browser.cancel_plugin_install(request_id)
async def uninstall_plugin(self, name):
return await self.context.plugin_browser.uninstall_plugin(name)
async def http_request(self, method="", url="", **kwargs):
async with ClientSession() as web:
res = await web.request(
method, url, ssl=helpers.get_ssl_context(), **kwargs
)
text = await res.text()
return {"status": res.status, "headers": dict(res.headers), "body": text}
async def ping(self, **kwargs):
return "pong"
async def execute_in_tab(self, tab, run_async, code):
try:
result = await inject_to_tab(tab, code, run_async)
if "exceptionDetails" in result["result"]:
return {"success": False, "result": result["result"]}
return {"success": True, "result": result["result"]["result"].get("value")}
except Exception as e:
return {"success": False, "result": e}
async def inject_css_into_tab(self, tab, style):
try:
css_id = str(uuid.uuid4())
result = await inject_to_tab(
tab,
f"""
(function() {{
const style = document.createElement('style');
style.id = "{css_id}";
document.head.append(style);
style.textContent = `{style}`;
}})()
""",
False,
)
if "exceptionDetails" in result["result"]:
return {"success": False, "result": result["result"]}
return {"success": True, "result": css_id}
except Exception as e:
return {"success": False, "result": e}
async def remove_css_from_tab(self, tab, css_id):
try:
result = await inject_to_tab(
tab,
f"""
(function() {{
let style = document.getElementById("{css_id}");
if (style.nodeName.toLowerCase() == 'style')
style.parentNode.removeChild(style);
}})()
""",
False,
)
if "exceptionDetails" in result["result"]:
return {"success": False, "result": result}
return {"success": True}
except Exception as e:
return {"success": False, "result": e}
async def get_setting(self, key, default):
return self.context.settings.getSetting(key, default)
async def set_setting(self, key, value):
return self.context.settings.setSetting(key, value)
async def allow_remote_debugging(self):
await helpers.start_systemd_unit(helpers.REMOTE_DEBUGGER_UNIT)
return True
async def disallow_remote_debugging(self):
await helpers.stop_systemd_unit(helpers.REMOTE_DEBUGGER_UNIT)
return True
async def filepicker_ls(self, path, include_files=True):
# def sorter(file): # Modification time
# if os.path.isdir(os.path.join(path, file)) or os.path.isfile(os.path.join(path, file)):
# return os.path.getmtime(os.path.join(path, file))
# return 0
# file_names = sorted(os.listdir(path), key=sorter, reverse=True) # TODO provide more sort options
file_names = sorted(os.listdir(path)) # Alphabetical
files = []
for file in file_names:
full_path = os.path.join(path, file)
is_dir = os.path.isdir(full_path)
if is_dir or include_files:
files.append(
{
"isdir": is_dir,
"name": file,
"realpath": os.path.realpath(full_path),
}
)
return {"realpath": os.path.realpath(path), "files": files}
# Based on https://stackoverflow.com/a/46422554/13174603
def start_rdt_proxy(self, ip, port):
async def pipe(reader, writer):
try:
while not reader.at_eof():
writer.write(await reader.read(2048))
finally:
writer.close()
async def handle_client(local_reader, local_writer):
try:
remote_reader, remote_writer = await open_connection(ip, port)
pipe1 = pipe(local_reader, remote_writer)
pipe2 = pipe(remote_reader, local_writer)
await gather(pipe1, pipe2)
finally:
local_writer.close()
self.rdt_proxy_server = start_server(handle_client, "127.0.0.1", port)
self.rdt_proxy_task = self.context.loop.create_task(self.rdt_proxy_server)
def stop_rdt_proxy(self):
if self.rdt_proxy_server:
self.rdt_proxy_server.close()
self.rdt_proxy_task.cancel()
async def _enable_rdt(self):
# TODO un-hardcode port
try:
self.stop_rdt_proxy()
ip = self.context.settings.getSetting("developer.rdt.ip", None)
if ip is not None:
self.logger.info("Connecting to React DevTools at " + ip)
async with ClientSession() as web:
res = await web.request(
"GET", "http://" + ip + ":8097", ssl=helpers.get_ssl_context()
)
script = (
"""
if (!window.deckyHasConnectedRDT) {
window.deckyHasConnectedRDT = true;
// This fixes the overlay when hovering over an element in RDT
Object.defineProperty(window, '__REACT_DEVTOOLS_TARGET_WINDOW__', {
enumerable: true,
configurable: true,
get: function() {
return FocusNavController?.m_ActiveContext?.ActiveWindow || window;
}
});
"""
+ await res.text()
+ "\n}"
)
if res.status != 200:
self.logger.error("Failed to connect to React DevTools at " + ip)
return False
self.start_rdt_proxy(ip, 8097)
self.logger.info("Connected to React DevTools, loading script")
tab = await get_gamepadui_tab()
# RDT needs to load before React itself to work.
await close_old_tabs()
result = await tab.reload_and_evaluate(script)
self.logger.info(result)
except Exception:
self.logger.error("Failed to connect to React DevTools")
self.logger.error(format_exc())
async def enable_rdt(self):
self.context.loop.create_task(self._enable_rdt())
async def disable_rdt(self):
self.logger.info("Disabling React DevTools")
tab = await get_gamepadui_tab()
self.rdt_script_id = None
await close_old_tabs()
await tab.evaluate_js("location.reload();", False, True, False)
self.logger.info("React DevTools disabled")
-335
View File
@@ -1,335 +0,0 @@
#!/bin/bash
## Before using this script, enable sshd on the deck and setup an sshd key between the deck and your dev in sshd_config.
## This script defaults to port 22 unless otherwise specified, and cannot run without a sudo password or LAN IP.
## You will need to specify the path to the ssh key if using key connection exclusively.
## TODO: document latest changes to wiki
## Pre-parse arugments for ease of use
CLONEFOLDER=${1:-""}
INSTALLFOLDER=${2:-""}
DECKIP=${3:-""}
SSHPORT=${4:-""}
PASSWORD=${5:-""}
SSHKEYLOC=${6:-""}
LOADERBRANCH=${7:-""}
LIBRARYBRANCH=${8:-""}
TEMPLATEBRANCH=${9:-""}
LATEST=${10:-""}
## gather options into an array
OPTIONSARRAY=("$CLONEFOLDER" "$INSTALLFOLDER" "$DECKIP" "$SSHPORT" "$PASSWORD" "$SSHKEYLOC" "$LOADERBRANCH" "$LIBRARYBRANCH" "$TEMPLATEBRANCH" "$LATEST")
## iterate through options array to check their presence
count=0
for OPTION in ${OPTIONSARRAY[@]}; do
! [[ "$OPTION" == "" ]] && count=$(($count+1))
# printf "OPTION=$OPTION\n"
done
setfolder() {
if [[ "$2" == "clone" ]]; then
local ACTION="clone"
local DEFAULT="git"
elif [[ "$2" == "install" ]]; then
local ACTION="install"
local DEFAULT="dev"
fi
if [[ "$ACTION" == "clone" ]]; then
printf "Enter the directory in /home/user/ to ${ACTION} to.\n"
printf "The ${ACTION} directory would be: ${HOME}/${DEFAULT}\n"
read -p "Enter your ${ACTION} directory: " CLONEFOLDER
if ! [[ "$CLONEFOLDER" =~ ^[[:alnum:]]+$ ]]; then
printf "Folder name not provided. Using default, '${DEFAULT}'.\n"
CLONEFOLDER="${DEFAULT}"
fi
elif [[ "$ACTION" == "install" ]]; then
printf "Enter the directory in /home/deck/homebrew to ${ACTION} pluginloader to.\n"
printf "The ${ACTION} directory would be: /home/deck/homebrew/${DEFAULT}/pluginloader\n"
printf "It is highly recommended that you use the default folder path seen above, just press enter at the next prompt.\n"
read -p "Enter your ${ACTION} directory: " INSTALLFOLDER
if ! [[ "$INSTALLFOLDER" =~ ^[[:alnum:]]+$ ]]; then
printf "Folder name not provided. Using default, '${DEFAULT}'.\n"
INSTALLFOLDER="${DEFAULT}"
fi
else
printf "Folder type could not be determined, exiting\n"
exit 1
fi
}
checkdeckip() {
### check that ip is provided
if [[ "$1" == "" ]]; then
printf "An ip address must be provided, exiting.\n"
exit 1
fi
### check to make sure it's a potentially valid ipv4 address
if ! [[ $1 =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
printf "A valid ip address must be provided, exiting.\n"
exit 1
fi
}
checksshport() {
### check to make sure a port was specified
if [[ "$1" == "" ]]; then
printf "ssh port not provided. Using default, '22'.\n"
SSHPORT="22"
fi
### check for valid ssh port
if [[ $1 -le 0 ]]; then
printf "A valid ssh port must be provided, exiting.\n"
exit 1
fi
}
checksshkey() {
### check if ssh key is present at location provided
if [[ "$1" == "" ]]; then
SSHKEYLOC="$HOME/.ssh/id_rsa"
printf "ssh key was not provided. Defaulting to $SSHKEYLOC if it exists.\n"
fi
### check if sshkey is present at location
if ! [[ -e "$1" ]]; then
SSHKEYLOC=""
printf "ssh key does not exist. This script will use password authentication.\n"
fi
}
checkpassword() {
### check to make sure a password for 'deck' was specified
if [[ "$1" == "" ]]; then
printf "Remote deck user password was not provided, exiting.\n"
exit 1
fi
}
clonefromto() {
# printf "repo=$1\n"
# printf "outdir=$2\n"
# printf "branch=$3\n"
printf "Repository: $1\n"
git clone $1 $2 &> '/dev/null'
CODE=$?
# printf "CODE=${CODE}"
if [[ $CODE -eq 128 ]]; then
cd $2
git fetch --all &> '/dev/null'
fi
if [[ -z $3 ]]; then
printf "Enter the desired branch for repository "$1" :\n"
local OUT="$(git branch -r | sed '/\/HEAD/d')"
# $OUT="$($OUT > )"
printf "$OUT\nbranch: "
read BRANCH
else
printf "on branch: $3\n"
BRANCH="$3"
fi
if ! [[ -z ${BRANCH} ]]; then
git checkout $BRANCH &> '/dev/null'
fi
if [[ ${LATEST} == "true" ]]; then
git pull --all
elif [[ ${LATEST} == "true" ]]; then
printf "Assuming user not pulling latest commits.\n"
else
printf "Pull latest commits? (y/N): "
read PULL
case ${PULL:0:1} in
y|Y )
printf "Pulling latest commits.\n"
git pull --all
;;
* )
printf "Not pulling latest commits.\n"
;;
esac
if ! [[ "$PULL" =~ ^[[:alnum:]]+$ ]]; then
printf "Assuming user not pulling latest commits.\n"
fi
fi
}
pnpmtransbundle() {
cd $1
if [[ "$2" == "library" ]]; then
npm install --quiet &> '/dev/null'
npm run build --quiet &> '/dev/null'
sudo npm link --quiet &> '/dev/null'
elif [[ "$2" == "frontend" ]]; then
pnpm i &> '/dev/null'
pnpm run build &> '/dev/null'
elif [[ "$2" == "template" ]]; then
pnpm i &> '/dev/null'
pnpm run build &> '/dev/null'
fi
}
if ! [[ $count -gt 9 ]] ; then
printf "Installing Steam Deck Plugin Loader contributor/developer (for Steam Deck)...\n"
printf "THIS SCRIPT ASSUMES YOU ARE RUNNING IT ON A PC, NOT THE DECK!
Not planning to contribute to or develop for PluginLoader?
If so, you should not be using this script.\n
If you have a release/nightly installed this script will disable it.\n"
printf "This script requires you to have nodejs installed. (If nodejs doesn't bundle npm on your OS/distro, then npm is required as well).\n"
fi
if ! [[ $count -gt 0 ]] ; then
read -p "Press any key to continue"
fi
printf "\n"
## User chooses preffered clone & install directories
if [[ "$CLONEFOLDER" == "" ]]; then
setfolder "$CLONEFOLDER" "clone"
fi
if [[ "$INSTALLFOLDER" == "" ]]; then
setfolder "$INSTALLFOLDER" "install"
fi
CLONEDIR="$HOME/$CLONEFOLDER"
INSTALLDIR="/home/deck/homebrew/$INSTALLFOLDER"
## Input ip address, port, password and sshkey
### DECKIP already been parsed?
if [[ "$DECKIP" == "" ]]; then
### get ip address of deck from user
read -p "Enter the ip address of your Steam Deck: " DECKIP
fi
### validate DECKIP
checkdeckip "$DECKIP"
### SSHPORT already been parsed?
if [[ "$SSHPORT" == "" ]]; then
### get ssh port from user
read -p "Enter the ssh port of your Steam Deck: " SSHPORT
fi
### validate SSHPORT
checksshport "$SSHPORT"
### PASSWORD already been parsed?
if [[ "$PASSWORD" == "" ]]; then
### prompt the user for their deck's password
printf "Enter the password for the Steam Deck user 'deck' : "
read -s PASSWORD
printf "\n"
fi
### validate PASSWORD
checkpassword "$PASSWORD"
### SSHKEYLOC already been parsed?
if [[ "$SSHKEYLOC" == "" ]]; then
### prompt the user for their ssh key
read -p "Enter the directory for your ssh key, for ease of connection : " SSHKEYLOC
fi
### validate SSHKEYLOC
checksshkey "$SSHKEYLOC"
if [[ "$SSHKEYLOC" == "" ]]; then
IDENINVOC=""
else
IDENINVOC="-i ${SSHKEYLOC}"
fi
## Create folder structure
printf "Cloning git repositories.\n"
mkdir -p ${CLONEDIR} &> '/dev/null'
### remove folders just in case
# rm -r ${CLONEDIR}/pluginloader
# rm -r ${CLONEDIR}/pluginlibrary
# rm -r ${CLONEDIR}/plugintemplate
clonefromto "https://github.com/SteamDeckHomebrew/PluginLoader" ${CLONEDIR}/pluginloader "$LOADERBRANCH"
clonefromto "https://github.com/SteamDeckHomebrew/decky-frontend-lib" ${CLONEDIR}/pluginlibrary "$LIBRARYBRANCH"
clonefromto "https://github.com/SteamDeckHomebrew/decky-plugin-template" ${CLONEDIR}/plugintemplate "$TEMPLATEBRANCH"
## install python dependencies to deck
printf "\nInstalling python dependencies.\n"
rsync -azp --rsh="ssh -p $SSHPORT $IDENINVOC" ${CLONEDIR}/pluginloader/requirements.txt deck@${DECKIP}:${INSTALLDIR}/pluginloader/requirements.txt &> '/dev/null'
ssh deck@${DECKIP} -p ${SSHPORT} ${IDENINVOC} "python -m ensurepip && python -m pip install --upgrade pip && python -m pip install --upgrade setuptools && python -m pip install -r $INSTALLDIR/pluginloader/requirements.txt" &> '/dev/null'
## Transpile and bundle typescript
[ "$UID" -eq 0 ] || printf "Input password to proceed with install.\n"
sudo npm install -g pnpm &> '/dev/null'
type pnpm &> '/dev/null'
PNPMLIVES=$?
if ! [[ "$PNPMLIVES" -eq 0 ]]; then
printf "pnpm does not appear to be installed, exiting.\n"
exit 1
fi
printf "Transpiling and bundling typescript.\n"
pnpmtransbundle ${CLONEDIR}/pluginlibrary/ "library"
pnpmtransbundle ${CLONEDIR}/pluginloader/frontend "frontend"
pnpmtransbundle ${CLONEDIR}/plugintemplate "template"
## Transfer relevant files to deck
printf "Copying relevant files to install directory\n\n"
ssh deck@${DECKIP} -p ${SSHPORT} ${IDENINVOC} "mkdir -p $INSTALLDIR/pluginloader && mkdir -p $INSTALLDIR/plugins" &> '/dev/null'
### copy files for PluginLoader
rsync -avzp --rsh="ssh -p $SSHPORT $IDENINVOC" --exclude='.git/' --exclude='.github/' --exclude='.vscode/' --exclude='frontend/' --exclude='dist/' --exclude='contrib/' --exclude='*.log' --exclude='requirements.txt' --exclude='backend/__pycache__/' --exclude='.gitignore' --delete ${CLONEDIR}/pluginloader/* deck@${DECKIP}:${INSTALLDIR}/pluginloader &> '/dev/null'
if ! [[ $? -eq 0 ]]; then
printf "Error occurred when copying $CLONEDIR/pluginloader/ to $INSTALLDIR/pluginloader/\n"
printf "Check that your Steam Deck is active, ssh is enabled and running and is accepting connections.\n"
exit 1
fi
### copy files for plugin template
rsync -avzp --rsh="ssh -p $SSHPORT $IDENINVOC" --exclude='.git/' --exclude='.github/' --exclude='.vscode/' --exclude='node_modules/' --exclude='src/' --exclude='*.log' --exclude='.gitignore' --exclude='pnpm-lock.yaml' --exclude='package.json' --exclude='rollup.config.js' --exclude='tsconfig.json' --delete ${CLONEDIR}/plugintemplate deck@${DECKIP}:${INSTALLDIR}/plugins &> '/dev/null'
if ! [[ $? -eq 0 ]]; then
printf "Error occurred when copying $CLONEDIR/plugintemplate to $INSTALLDIR/plugins\n"
exit 1
fi
## TODO: direct contributors to wiki for this info?
printf "Run these commands to deploy your local changes to the deck:\n"
printf "'rsync -avzp --mkpath --rsh=""\"ssh -p $SSHPORT $IDENINVOC\""" --exclude='.git/' --exclude='.github/' --exclude='.vscode/' --exclude='frontend/' --exclude='dist/' --exclude='contrib/' --exclude='*.log' --exclude='requirements.txt' --exclude='backend/__pycache__/' --exclude='.gitignore' --delete $CLONEDIR/pluginloader/* deck@$DECKIP:$INSTALLDIR/pluginloader/'\n"
printf "'rsync -avzp --mkpath --rsh=""\"ssh -p $SSHPORT $IDENINVOC\""" --exclude='.git/' --exclude='.github/' --exclude='.vscode/' --exclude='node_modules/' --exclude='src/' --exclude='*.log' --exclude='.gitignore' --exclude='package-lock.json' --delete $CLONEDIR/pluginname deck@$DECKIP:$INSTALLDIR/plugins'\n\n"
printf "Run in console or in a script this command to run your development version:\n'ssh deck@$DECKIP -p $SSHPORT $IDENINVOC 'export PLUGIN_PATH=$INSTALLDIR/plugins; export CHOWN_PLUGIN_PATH=0; echo 'steam' | sudo -SE python3 $INSTALLDIR/pluginloader/backend/main.py'\n"
## Disable Releases versions if they exist
### ssh into deck and disable PluginLoader release/nightly service
printf "Connecting via ssh to disable any PluginLoader release versions.\n"
printf "Script will exit after this. All done!\n"
ssh deck@${DECKIP} -p ${SSHPORT} ${IDENINVOC} "printf $PASSWORD | sudo -S systemctl disable --now plugin_loader; echo $?" &> '/dev/null'
-168
View File
@@ -1,168 +0,0 @@
#!/bin/bash
## Pre-parse arugments for ease of use
CLONEFOLDER=${1:-""}
LOADERBRANCH=${2:-""}
LIBRARYBRANCH=${3:-""}
TEMPLATEBRANCH=${4:-""}
LATEST=${5:-""}
## gather options into an array
OPTIONSARRAY=("$CLONEFOLDER" "$LOADERBRANCH" "$LIBRARYBRANCH" "$TEMPLATEBRANCH" "$LATEST")
## iterate through options array to check their presence
count=0
for OPTION in ${OPTIONSARRAY[@]}; do
! [[ "$OPTION" == "" ]] && count=$(($count+1))
# printf "OPTION=$OPTION\n"
done
clonefromto() {
# printf "repo=$1\n"
# printf "outdir=$2\n"
# printf "branch=$3\n"
printf "Repository: $1\n"
git clone $1 $2 &> '/dev/null'
CODE=$?
# printf "CODE=${CODE}"
if [[ $CODE -eq 128 ]]; then
cd $2
git fetch --all &> '/dev/null'
fi
if [[ -z $3 ]]; then
printf "Enter the desired branch for repository "$1" :\n"
local OUT="$(git branch -r | sed '/\/HEAD/d')"
# $OUT="$($OUT > )"
printf "$OUT\nbranch: "
read BRANCH
else
printf "on branch: $3\n"
BRANCH="$3"
fi
if ! [[ -z ${BRANCH} ]]; then
git checkout $BRANCH &> '/dev/null'
fi
if [[ ${LATEST} == "true" ]]; then
git pull --all
elif [[ ${LATEST} == "true" ]]; then
printf "Assuming user not pulling latest commits.\n"
else
printf "Pull latest commits? (y/N): "
read PULL
case ${PULL:0:1} in
y|Y )
printf "Pulling latest commits.\n"
git pull --all
;;
* )
printf "Not pulling latest commits.\n"
;;
esac
if ! [[ "$PULL" =~ ^[[:alnum:]]+$ ]]; then
printf "Assuming user not pulling latest commits.\n"
fi
fi
}
pnpmtransbundle() {
cd $1
if [[ "$2" == "library" ]]; then
npm install --quiet &> '/dev/null'
npm run build --quiet &> '/dev/null'
sudo npm link --quiet &> '/dev/null'
elif [[ "$2" == "frontend" ]]; then
pnpm i &> '/dev/null'
pnpm run build &> '/dev/null'
elif [[ "$2" == "template" ]]; then
pnpm i &> '/dev/null'
pnpm run build &> '/dev/null'
fi
}
if ! [[ $count -gt 4 ]] ; then
printf "Installing Steam Deck Plugin Loader contributor/developer (no Steam Deck)..."
printf "\nTHIS SCRIPT ASSUMES YOU ARE RUNNING IT ON A PC, NOT THE DECK!
Not planning to contribute to or develop for PluginLoader?
Then you should not be using this script.\n"
printf "\nThis script requires you to have nodejs installed. (If nodejs doesn't bundle npm on your OS/distro, then npm is required as well).\n"
fi
if ! [[ $count -gt 0 ]] ; then
read -p "Press any key to continue"
fi
printf "\n"
if [[ "$CLONEFOLDER" == "" ]]; then
printf "Enter the directory in /home/user/ to clone to.\n"
printf "The clone directory would be: ${HOME}/git \n"
read -p "Enter your clone directory: " CLONEFOLDER
if ! [[ "$CLONEFOLDER" =~ ^[[:alnum:]]+$ ]]; then
printf "Folder name not provided. Using default, '${DEFAULT}'.\n"
CLONEFOLDER="${DEFAULT}"
fi
fi
CLONEDIR="$HOME/$CLONEFOLDER"
## Create folder structure
printf "Cloning git repositories.\n"
mkdir -p ${CLONEDIR} &> '/dev/null'
### remove folders just in case
# rm -r ${CLONEDIR}/pluginloader
# rm -r ${CLONEDIR}/pluginlibrary
# rm -r ${CLONEDIR}/plugintemplate
clonefromto "https://github.com/SteamDeckHomebrew/PluginLoader" ${CLONEDIR}/pluginloader "$LOADERBRANCH"
clonefromto "https://github.com/SteamDeckHomebrew/decky-frontend-lib" ${CLONEDIR}/pluginlibrary "$LIBRARYBRANCH"
clonefromto "https://github.com/SteamDeckHomebrew/decky-plugin-template" ${CLONEDIR}/plugintemplate "$TEMPLATEBRANCH"
## install python dependencies (maybe use venv?)
python -m pip install -r ${CLONEDIR}/pluginloader/requirements.txt &> '/dev/null'
## Transpile and bundle typescript
[ "$UID" -eq 0 ] || printf "Input password to proceed with install.\n"
type npm &> '/dev/null'
NPMLIVES=$?
if ! [[ "$PNPMLIVES" -eq 0 ]]; then
printf "npm does not appear to be installed, exiting.\n"
exit 1
fi
sudo npm install -g pnpm &> '/dev/null'
type pnpm &> '/dev/null'
PNPMLIVES=$?
if ! [[ "$PNPMLIVES" -eq 0 ]]; then
printf "pnpm does not appear to be installed, exiting.\n"
exit 1
fi
printf "Transpiling and bundling typescript.\n"
pnpmtransbundle ${CLONEDIR}/pluginlibrary/ "library"
pnpmtransbundle ${CLONEDIR}/pluginloader/frontend "frontend"
pnpmtransbundle ${CLONEDIR}/plugintemplate "template"
printf "Plugin Loader is located at '${CLONEDIR}/pluginloader/'.\n"
printf "Run in console or in a script these commands to run your development version:\n'export PLUGIN_PATH=${CLONEDIR}/plugins; export CHOWN_PLUGIN_PATH=0; sudo -E python3 ${CLONEDIR}/pluginloader/backend/main.py'\n"
printf "All done!\n"
+2 -1
View File
@@ -9,7 +9,8 @@ Restart=always
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
WorkingDirectory=${HOMEBREW_FOLDER}/services
KillSignal=SIGKILL
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER}
Environment=PRIVILEGED_PATH=${HOMEBREW_FOLDER}
Environment=LOG_LEVEL=DEBUG
[Install]
WantedBy=multi-user.target
+2 -1
View File
@@ -9,7 +9,8 @@ Restart=always
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
WorkingDirectory=${HOMEBREW_FOLDER}/services
KillSignal=SIGKILL
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER}
Environment=PRIVILEGED_PATH=${HOMEBREW_FOLDER}
Environment=LOG_LEVEL=INFO
[Install]
WantedBy=multi-user.target
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

+162
View File
@@ -0,0 +1,162 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="81.700577mm"
height="24.334814mm"
viewBox="0 0 81.700577 24.334814"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="download.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="3.659624"
inkscape:cx="115.44902"
inkscape:cy="59.295709"
inkscape:window-width="1827"
inkscape:window-height="1233"
inkscape:window-x="69"
inkscape:window-y="38"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs2">
<linearGradient
inkscape:collect="always"
id="linearGradient4494">
<stop
style="stop-color:#009fff;stop-opacity:1;"
offset="0"
id="stop4490" />
<stop
style="stop-color:#ff1965;stop-opacity:1;"
offset="0.79417855"
id="stop4498" />
<stop
style="stop-color:#b9b500;stop-opacity:1;"
offset="1"
id="stop4492" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4494"
id="linearGradient4496"
x1="49.131042"
y1="118.6573"
x2="150.29259"
y2="138.74957"
gradientUnits="userSpaceOnUse"
spreadMethod="pad"
gradientTransform="matrix(1.0500324,0,0,1,-1.6155884,24.621921)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4494"
id="linearGradient13802"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0500324,0,0,1,-1.6155884,24.621921)"
x1="49.131042"
y1="118.6573"
x2="150.29259"
y2="138.74957"
spreadMethod="pad" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-64.149712,-136.3326)">
<rect
style="mix-blend-mode:normal;fill:url(#linearGradient13802);fill-opacity:1;stroke:none;stroke-width:0.271121"
id="rect111"
width="81.700577"
height="24.334814"
x="64.149712"
y="136.3326"
ry="8.1781616" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:#000000;stroke:none;stroke-width:0.264583"
x="66.364288"
y="124.84658"
id="text10382"><tspan
sodipodi:role="line"
id="tspan10380"
style="stroke-width:0.264583"
x="66.364288"
y="124.84658" /></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15.1694px;font-family:sans-serif;-inkscape-font-specification:sans-serif;white-space:pre;inline-size:82.6483;display:inline;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="67.732498"
y="126.05277"
id="text10440"
transform="translate(1.088576,28.135753)"><tspan
x="67.732498"
y="126.05277"
id="tspan13872">Download</tspan></text>
<rect
style="mix-blend-mode:normal;fill:url(#linearGradient4496);fill-opacity:1;stroke:none;stroke-width:0.271121"
id="rect13792"
width="81.700577"
height="24.334814"
x="64.149712"
y="136.3326"
ry="8.1781616" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:#000000;stroke:none;stroke-width:0.264583"
x="66.364288"
y="124.84658"
id="text13796"><tspan
sodipodi:role="line"
id="tspan13794"
style="stroke-width:0.264583"
x="66.364288"
y="124.84658" /></text>
<g
aria-label="Download"
transform="translate(1.088576,28.135753)"
id="text13800"
style="font-size:15.1694px;-inkscape-font-specification:sans-serif;white-space:pre;inline-size:82.6483;display:inline;fill:#ffffff;stroke-width:0.264583">
<path
d="m 77.880751,120.53111 q 0,2.74566 -1.501771,4.14125 -1.486601,1.38041 -4.156416,1.38041 h -3.01871 v -10.83095 h 3.337268 q 1.638295,0 2.836678,0.60678 1.198382,0.60677 1.850666,1.78999 0.652285,1.16804 0.652285,2.91252 z m -1.441093,0.0455 q 0,-2.16923 -1.077028,-3.17041 -1.061858,-1.01635 -3.01871,-1.01635 H 70.5691 v 8.49487 h 1.471432 q 4.399126,0 4.399126,-4.30811 z"
id="path13828" />
<path
d="m 87.164417,121.9722 q 0,2.01753 -1.03152,3.1249 -1.01635,1.10737 -2.760831,1.10737 -1.077027,0 -1.926513,-0.48542 -0.834317,-0.50059 -1.319738,-1.4411 -0.485421,-0.95567 -0.485421,-2.30575 0,-2.01753 1.01635,-3.10972 1.01635,-1.0922 2.760831,-1.0922 1.107366,0 1.941683,0.50059 0.849486,0.48542 1.319738,1.42592 0.485421,0.92534 0.485421,2.27541 z m -6.143608,0 q 0,1.4411 0.561268,2.29058 0.576437,0.83432 1.820328,0.83432 1.228722,0 1.805159,-0.83432 0.576437,-0.84948 0.576437,-2.29058 0,-1.44109 -0.576437,-2.26024 -0.576437,-0.81914 -1.820328,-0.81914 -1.243891,0 -1.805159,0.81914 -0.561268,0.81915 -0.561268,2.26024 z"
id="path13830" />
<path
d="m 94.218174,121.45644 q -0.197202,-0.62194 -0.348896,-1.21355 -0.136525,-0.60677 -0.212372,-0.9405 h -0.06068 q -0.06068,0.33373 -0.197203,0.9405 -0.136524,0.59161 -0.348896,1.22872 l -1.456262,4.56599 h -1.51694 l -2.229902,-8.1308 h 1.380415 l 1.122536,4.33845 q 0.166863,0.65229 0.318557,1.31974 0.151694,0.66745 0.212372,1.10737 h 0.06068 q 0.06068,-0.25788 0.136525,-0.63712 0.09102,-0.37923 0.197202,-0.78881 0.106186,-0.42474 0.212372,-0.75847 l 1.441093,-4.58116 h 1.456262 l 1.395585,4.58116 q 0.166864,0.51576 0.318558,1.12254 0.166863,0.60678 0.227541,1.04669 h 0.06068 q 0.04551,-0.37924 0.197202,-1.04669 0.166864,-0.66745 0.348897,-1.36525 l 1.137705,-4.33845 h 1.365246 l -2.260241,8.1308 h -1.562448 z"
id="path13832" />
<path
d="m 104.8064,117.77028 q 1.45627,0 2.19957,0.71296 0.7433,0.69779 0.7433,2.27541 v 5.29412 h -1.31974 v -5.2031 q 0,-1.95685 -1.82033,-1.95685 -1.35007,0 -1.86583,0.75847 -0.51576,0.75847 -0.51576,2.18439 v 4.21709 h -1.33491 v -8.1308 h 1.07703 l 0.1972,1.10737 h 0.0759 q 0.3944,-0.63711 1.09219,-0.9405 0.69779,-0.31856 1.47143,-0.31856 z"
id="path13834" />
<path
d="m 111.6023,126.05277 h -1.33491 v -11.52874 h 1.33491 z"
id="path13836" />
<path
d="m 121.25003,121.9722 q 0,2.01753 -1.03152,3.1249 -1.01635,1.10737 -2.76084,1.10737 -1.07702,0 -1.92651,-0.48542 -0.83432,-0.50059 -1.31974,-1.4411 -0.48542,-0.95567 -0.48542,-2.30575 0,-2.01753 1.01635,-3.10972 1.01635,-1.0922 2.76083,-1.0922 1.10737,0 1.94169,0.50059 0.84948,0.48542 1.31973,1.42592 0.48543,0.92534 0.48543,2.27541 z m -6.14361,0 q 0,1.4411 0.56127,2.29058 0.57643,0.83432 1.82032,0.83432 1.22873,0 1.80516,-0.83432 0.57644,-0.84948 0.57644,-2.29058 0,-1.44109 -0.57644,-2.26024 -0.57643,-0.81914 -1.82033,-0.81914 -1.24389,0 -1.80515,0.81914 -0.56127,0.81915 -0.56127,2.26024 z"
id="path13838" />
<path
d="m 126.43796,117.78545 q 1.4866,0 2.19956,0.65228 0.71296,0.65229 0.71296,2.07821 v 5.53683 h -0.97084 l -0.25788,-1.15287 h -0.0607 q -0.53093,0.66745 -1.12253,0.98601 -0.57644,0.31856 -1.60796,0.31856 -1.10737,0 -1.8355,-0.57644 -0.72813,-0.59161 -0.72813,-1.8355 0,-1.21355 0.95567,-1.86583 0.95567,-0.66746 2.94287,-0.72814 l 1.38041,-0.0455 v -0.48542 q 0,-1.01635 -0.43991,-1.41076 -0.43991,-0.3944 -1.24389,-0.3944 -0.63712,0 -1.21355,0.1972 -0.57644,0.18203 -1.07703,0.42474 l -0.40957,-1.00118 q 0.53092,-0.28822 1.25906,-0.48542 0.72813,-0.21237 1.51694,-0.21237 z m 0.3944,4.33845 q -1.51694,0.0607 -2.10855,0.48542 -0.57643,0.42474 -0.57643,1.19838 0,0.68262 0.40957,1.00118 0.42474,0.31856 1.07703,0.31856 1.03152,0 1.71414,-0.56127 0.68262,-0.57644 0.68262,-1.75965 v -0.72813 z"
id="path13840" />
<path
d="m 134.7508,126.20447 q -1.51694,0 -2.42711,-1.04669 -0.91016,-1.06186 -0.91016,-3.15524 0,-2.09337 0.91016,-3.15523 0.92534,-1.07703 2.44228,-1.07703 0.9405,0 1.53211,0.3489 0.60677,0.34889 0.98601,0.84948 h 0.091 q -0.0152,-0.1972 -0.0607,-0.57643 -0.0303,-0.39441 -0.0303,-0.62195 v -3.24625 h 1.3349 v 11.52874 h -1.07702 l -0.19721,-1.09219 h -0.0607 q -0.36407,0.51576 -0.97084,0.87982 -0.60678,0.36407 -1.56245,0.36407 z m 0.21237,-1.10737 q 1.2894,0 1.80516,-0.69779 0.53093,-0.71296 0.53093,-2.13889 v -0.24271 q 0,-1.51694 -0.50059,-2.32092 -0.50059,-0.81914 -1.85067,-0.81914 -1.07703,0 -1.62313,0.86465 -0.53093,0.84949 -0.53093,2.29058 0,1.45626 0.53093,2.26024 0.5461,0.80398 1.6383,0.80398 z"
id="path13842" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.9 KiB

+2
View File
@@ -2,3 +2,5 @@ node_modules/
.yalc
yalc.lock
stats.html
+100
View File
@@ -0,0 +1,100 @@
export default {
contextSeparator: '_',
// Key separator used in your translation keys
createOldCatalogs: false,
// Save the \_old files
defaultNamespace: 'translation',
// Default namespace used in your i18next config
defaultValue: '',
// Default value to give to keys with no value
// You may also specify a function accepting the locale, namespace, key, and value as arguments
indentation: 2,
// Indentation of the catalog files
keepRemoved: true,
// Keep keys from the catalog that are no longer in code
keySeparator: '.',
// Key separator used in your translation keys
// If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance.
// see below for more details
lexers: {
mjs: ['JavascriptLexer'],
js: ['JavascriptLexer'], // if you're writing jsx inside .js files, change this to JsxLexer
ts: ['JavascriptLexer'],
jsx: ['JsxLexer'],
tsx: ['JsxLexer'],
default: ['JavascriptLexer'],
},
lineEnding: 'auto',
// Control the line ending. See options at https://github.com/ryanve/eol
locales: ['en-US'],
// An array of the locales in your applications
namespaceSeparator: false,
// Namespace separator used in your translation keys
// If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance.
output: '../backend/locales/$LOCALE.json',
// Supports $LOCALE and $NAMESPACE injection
// Supports JSON (.json) and YAML (.yml) file formats
// Where to write the locale files relative to process.cwd()
pluralSeparator: '_',
// Plural separator used in your translation keys
// If you want to use plain english keys, separators such as `_` might conflict. You might want to set `pluralSeparator` to a different string that does not occur in your keys.
input: './src/**/*.{ts,tsx}',
// An array of globs that describe where to look for source files
// relative to the location of the configuration file
sort: true,
// Whether or not to sort the catalog. Can also be a [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#parameters)
verbose: false,
// Display info about the parsing including some stats
failOnWarnings: false,
// Exit with an exit code of 1 on warnings
failOnUpdate: false,
// Exit with an exit code of 1 when translations are updated (for CI purpose)
customValueTemplate: null,
// If you wish to customize the value output the value as an object, you can set your own format.
// ${defaultValue} is the default value you set in your translation function.
// Any other custom property will be automatically extracted.
//
// Example:
// {
// message: "${defaultValue}",
// description: "${maxLength}", // t('my-key', {maxLength: 150})
// }
resetDefaultValueLocale: null,
// The locale to compare with default values to determine whether a default value has been changed.
// If this is set and a default value differs from a translation in the specified locale, all entries
// for that key across locales are reset to the default value, and existing translations are moved to
// the `_old` file.
i18nextOptions: null,
// If you wish to customize options in internally used i18next instance, you can define an object with any
// configuration property supported by i18next (https://www.i18next.com/overview/configuration-options).
// { compatibilityJSON: 'v3' } can be used to generate v3 compatible plurals.
yamlOptions: null,
// If you wish to customize options for yaml output, you can define an object here.
// Configuration options are here (https://github.com/nodeca/js-yaml#dump-object---options-).
// Example:
// {
// lineWidth: -1,
// }
}
+19 -13
View File
@@ -12,28 +12,30 @@
},
"devDependencies": {
"@rollup/plugin-commonjs": "^21.1.0",
"@rollup/plugin-image": "^3.0.1",
"@rollup/plugin-image": "^3.0.2",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-replace": "^4.0.0",
"@rollup/plugin-typescript": "^8.3.3",
"@rollup/plugin-typescript": "^8.5.0",
"@types/react": "16.14.0",
"@types/react-file-icon": "^1.0.1",
"@types/react-router": "5.1.18",
"@types/webpack": "^5.28.0",
"husky": "^8.0.1",
"@types/webpack": "^5.28.1",
"husky": "^8.0.3",
"i18next-parser": "^8.0.0",
"import-sort-style-module": "^6.0.0",
"inquirer": "^8.2.4",
"prettier": "^2.7.1",
"inquirer": "^8.2.5",
"prettier": "^3.2.5",
"prettier-plugin-import-sort": "^0.0.7",
"react": "16.14.0",
"react-dom": "16.14.0",
"rollup": "^2.76.0",
"rollup": "^2.79.1",
"rollup-plugin-delete": "^2.0.0",
"rollup-plugin-external-globals": "^0.6.1",
"rollup-plugin-polyfill-node": "^0.10.2",
"tslib": "^2.4.0",
"typescript": "^4.7.4"
"rollup-plugin-visualizer": "^5.9.2",
"tslib": "^2.5.3",
"typescript": "^4.9.5"
},
"importSort": {
".js, .jsx, .ts, .tsx": {
@@ -42,10 +44,14 @@
}
},
"dependencies": {
"decky-frontend-lib": "^3.18.10",
"react-file-icon": "^1.2.0",
"react-icons": "^4.4.0",
"react-markdown": "^8.0.3",
"decky-frontend-lib": "3.25.0",
"filesize": "^10.0.7",
"i18next": "^23.2.1",
"i18next-http-backend": "^2.2.1",
"react-file-icon": "^1.3.0",
"react-i18next": "^12.3.1",
"react-icons": "^4.9.0",
"react-markdown": "^8.0.7",
"remark-gfm": "^3.0.1"
}
}
+2399 -980
View File
File diff suppressed because it is too large Load Diff
+6 -2
View File
@@ -7,15 +7,18 @@ import typescript from '@rollup/plugin-typescript';
import { defineConfig } from 'rollup';
import del from 'rollup-plugin-delete';
import externalGlobals from 'rollup-plugin-external-globals';
import { visualizer } from 'rollup-plugin-visualizer';
const hiddenWarnings = ['THIS_IS_UNDEFINED', 'EVAL'];
export default defineConfig({
input: 'src/index.tsx',
input: 'src/index.ts',
plugins: [
del({ targets: '../backend/static/*', force: true }),
commonjs(),
nodeResolve(),
nodeResolve({
browser: true,
}),
externalGlobals({
react: 'SP_REACT',
'react-dom': 'SP_REACTDOM',
@@ -31,6 +34,7 @@ export default defineConfig({
'process.env.NODE_ENV': JSON.stringify('production'),
}),
image(),
visualizer(),
],
preserveEntrySignatures: false,
output: {
+60 -5
View File
@@ -1,36 +1,57 @@
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';
interface PublicDeckyState {
plugins: Plugin[];
pluginOrder: string[];
frozenPlugins: string[];
hiddenPlugins: string[];
activePlugin: Plugin | null;
updates: PluginUpdateMapping | null;
hasLoaderUpdate?: boolean;
isLoaderUpdating: boolean;
versionInfo: VerInfo | null;
notificationSettings: NotificationSettings;
userInfo: UserInfo | null;
}
export interface UserInfo {
username: string;
path: string;
}
export class DeckyState {
private _plugins: Plugin[] = [];
private _pluginOrder: string[] = [];
private _frozenPlugins: string[] = [];
private _hiddenPlugins: string[] = [];
private _activePlugin: Plugin | null = null;
private _updates: PluginUpdateMapping | null = null;
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();
publicState(): PublicDeckyState {
return {
plugins: this._plugins,
pluginOrder: this._pluginOrder,
frozenPlugins: this._frozenPlugins,
hiddenPlugins: this._hiddenPlugins,
activePlugin: this._activePlugin,
updates: this._updates,
hasLoaderUpdate: this._hasLoaderUpdate,
isLoaderUpdating: this._isLoaderUpdating,
versionInfo: this._versionInfo,
notificationSettings: this._notificationSettings,
userInfo: this._userInfo,
};
}
@@ -44,6 +65,21 @@ export class DeckyState {
this.notifyUpdate();
}
setPluginOrder(pluginOrder: string[]) {
this._pluginOrder = pluginOrder;
this.notifyUpdate();
}
setFrozenPlugins(frozenPlugins: string[]) {
this._frozenPlugins = frozenPlugins;
this.notifyUpdate();
}
setHiddenPlugins(hiddenPlugins: string[]) {
this._hiddenPlugins = hiddenPlugins;
this.notifyUpdate();
}
setActivePlugin(name: string) {
this._activePlugin = this._plugins.find((plugin) => plugin.name === name) ?? null;
this.notifyUpdate();
@@ -69,6 +105,16 @@ export class DeckyState {
this.notifyUpdate();
}
setNotificationSettings(notificationSettings: NotificationSettings) {
this._notificationSettings = notificationSettings;
this.notifyUpdate();
}
setUserInfo(userInfo: UserInfo) {
this._userInfo = userInfo;
this.notifyUpdate();
}
private notifyUpdate() {
this.eventBus.dispatchEvent(new Event('update'));
}
@@ -78,6 +124,7 @@ interface DeckyStateContext extends PublicDeckyState {
setVersionInfo(versionInfo: VerInfo): void;
setIsLoaderUpdating(hasUpdate: boolean): void;
setActivePlugin(name: string): void;
setPluginOrder(pluginOrder: string[]): void;
closeActivePlugin(): void;
}
@@ -102,14 +149,22 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) =
return () => deckyState.eventBus.removeEventListener('update', onUpdate);
}, []);
const setIsLoaderUpdating = (hasUpdate: boolean) => deckyState.setIsLoaderUpdating(hasUpdate);
const setVersionInfo = (versionInfo: VerInfo) => deckyState.setVersionInfo(versionInfo);
const setActivePlugin = (name: string) => deckyState.setActivePlugin(name);
const closeActivePlugin = () => deckyState.closeActivePlugin();
const setIsLoaderUpdating = deckyState.setIsLoaderUpdating.bind(deckyState);
const setVersionInfo = deckyState.setVersionInfo.bind(deckyState);
const setActivePlugin = deckyState.setActivePlugin.bind(deckyState);
const closeActivePlugin = deckyState.closeActivePlugin.bind(deckyState);
const setPluginOrder = deckyState.setPluginOrder.bind(deckyState);
return (
<DeckyStateContext.Provider
value={{ ...publicDeckyState, setIsLoaderUpdating, setVersionInfo, setActivePlugin, closeActivePlugin }}
value={{
...publicDeckyState,
setIsLoaderUpdating,
setVersionInfo,
setActivePlugin,
closeActivePlugin,
setPluginOrder,
}}
>
{children}
</DeckyStateContext.Provider>
+8 -5
View File
@@ -30,11 +30,14 @@ const DeckyToaster: FC<DeckyToasterProps> = () => {
// not actually node but TS is shit
let interval: NodeJS.Timer | null;
if (renderedToast) {
interval = setTimeout(() => {
interval = null;
console.log('clear toast', renderedToast.data);
removeToast(renderedToast.data);
}, (renderedToast.data.duration || 5e3) + 1000);
interval = setTimeout(
() => {
interval = null;
console.log('clear toast', renderedToast.data);
removeToast(renderedToast.data);
},
(renderedToast.data.duration || 5e3) + 1000,
);
console.log('set int', interval);
}
return () => {
+2 -2
View File
@@ -1,4 +1,4 @@
import { Focusable, Router } from 'decky-frontend-lib';
import { Focusable, Navigation } from 'decky-frontend-lib';
import { FunctionComponent, useRef } from 'react';
import ReactMarkdown, { Options as ReactMarkdownOptions } from 'react-markdown';
import remarkGfm from 'remark-gfm';
@@ -22,7 +22,7 @@ const Markdown: FunctionComponent<MarkdownProps> = (props) => {
onActivate={() => {}}
onOKButton={() => {
props.onDismiss?.();
Router.NavigateToExternalWeb(aRef.current!.href);
Navigation.NavigateToExternalWeb(aRef.current!.href);
}}
style={{ display: 'inline' }}
>
+31 -17
View File
@@ -1,31 +1,34 @@
import {
ButtonItem,
Focusable,
PanelSection,
PanelSectionRow,
joinClassNames,
scrollClasses,
staticClasses,
} from 'decky-frontend-lib';
import { VFC } from 'react';
import { ButtonItem, Focusable, PanelSection, PanelSectionRow } from 'decky-frontend-lib';
import { VFC, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaEyeSlash } from 'react-icons/fa';
import { Plugin } from '../plugin';
import { useDeckyState } from './DeckyState';
import NotificationBadge from './NotificationBadge';
import { useQuickAccessVisible } from './QuickAccessVisibleState';
import TitleView from './TitleView';
const PluginView: VFC = () => {
const { plugins, updates, activePlugin, setActivePlugin, closeActivePlugin } = useDeckyState();
const { hiddenPlugins } = useDeckyState();
const { plugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = useDeckyState();
const visible = useQuickAccessVisible();
const { t } = useTranslation();
const [pluginList, setPluginList] = useState<Plugin[]>(
plugins.sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name)),
);
useEffect(() => {
setPluginList(plugins.sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name)));
console.log('updating PluginView after changes');
}, [plugins, pluginOrder]);
if (activePlugin) {
return (
<Focusable onCancelButton={closeActivePlugin}>
<TitleView />
<div
className={joinClassNames(staticClasses.TabGroupPanel, scrollClasses.ScrollPanel, scrollClasses.ScrollY)}
style={{ height: '100%' }}
>
<div style={{ height: '100%', paddingTop: '16px' }}>
{(visible || activePlugin.alwaysRender) && activePlugin.content}
</div>
</Focusable>
@@ -34,10 +37,15 @@ const PluginView: VFC = () => {
return (
<>
<TitleView />
<div className={joinClassNames(staticClasses.TabGroupPanel, scrollClasses.ScrollPanel, scrollClasses.ScrollY)}>
<div
style={{
paddingTop: '16px',
}}
>
<PanelSection>
{plugins
{pluginList
.filter((p) => p.content)
.filter(({ name }) => !hiddenPlugins.includes(name))
.map(({ name, icon }) => (
<PanelSectionRow key={name}>
<ButtonItem layout="below" onClick={() => setActivePlugin(name)}>
@@ -49,6 +57,12 @@ const PluginView: VFC = () => {
</ButtonItem>
</PanelSectionRow>
))}
{hiddenPlugins.length > 0 && (
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', fontSize: '0.8rem', marginTop: '10px' }}>
<FaEyeSlash />
<div>{t('PluginView.hidden', { count: hiddenPlugins.length })}</div>
</div>
)}
</PanelSection>
</div>
</>
@@ -1,21 +1,16 @@
import { FC, createContext, useContext, useState } from 'react';
const QuickAccessVisibleState = createContext<boolean>(true);
const QuickAccessVisibleState = createContext<boolean>(false);
export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState);
export const QuickAccessVisibleStateProvider: FC<{ initial: boolean; setter: ((val: boolean) => {}[]) | never[] }> = ({
children,
initial,
setter,
}) => {
export const QuickAccessVisibleStateProvider: FC<{ tab: any }> = ({ children, tab }) => {
const initial = tab.initialVisibility;
const [visible, setVisible] = useState<boolean>(initial);
const [prev, setPrev] = useState<boolean>(initial);
// hack to use an array as a "pointer" to pass the setter up the tree
setter[0] = setVisible;
if (initial != prev) {
setPrev(initial);
setVisible(initial);
}
// HACK but i can't think of a better way to do this
tab.qAMVisibilitySetter = (val: boolean) => {
if (val != visible) setVisible(val);
};
return <QuickAccessVisibleState.Provider value={visible}>{children}</QuickAccessVisibleState.Provider>;
};
+9 -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';
@@ -9,10 +10,13 @@ const titleStyles: CSSProperties = {
display: 'flex',
paddingTop: '3px',
paddingRight: '16px',
position: 'sticky',
top: '0px',
};
const TitleView: VFC = () => {
const { activePlugin, closeActivePlugin } = useDeckyState();
const { t } = useTranslation();
const onSettingsClick = () => {
Router.CloseSideMenus();
@@ -31,12 +35,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 +51,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>
);
};
@@ -0,0 +1,121 @@
import {
DialogButton,
DialogCheckbox,
DialogCheckboxProps,
Marquee,
Menu,
MenuItem,
findModuleChild,
showContextMenu,
} from 'decky-frontend-lib';
import { FC, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaChevronDown } from 'react-icons/fa';
const dropDownControlButtonClass = findModuleChild((m) => {
if (typeof m !== 'object') return undefined;
for (const prop in m) {
if (m[prop]?.toString()?.includes('gamepaddropdown_DropDownControlButton')) {
return m[prop];
}
}
});
const DropdownMultiselectItem: FC<
{
value: any;
onSelect: (checked: boolean, value: any) => void;
checked: boolean;
} & DialogCheckboxProps
> = ({ value, onSelect, checked: defaultChecked, ...rest }) => {
const [checked, setChecked] = useState(defaultChecked);
useEffect(() => {
onSelect?.(checked, value);
}, [checked, onSelect, value]);
return (
<MenuItem bInteractableItem onClick={() => setChecked((x) => !x)}>
<DialogCheckbox
style={{ marginBottom: 0, padding: 0 }}
className="decky_DropdownMultiselectItem_DialogCheckbox"
bottomSeparator="none"
{...rest}
onClick={() => setChecked((x) => !x)}
onChange={(checked) => setChecked(checked)}
controlled
checked={checked}
/>
</MenuItem>
);
};
const DropdownMultiselect: FC<{
items: {
label: string;
value: string;
}[];
selected: string[];
onSelect: (selected: any[]) => void;
label: string;
}> = ({ label, items, selected, onSelect }) => {
const [itemsSelected, setItemsSelected] = useState<any>(selected);
const { t } = useTranslation();
const handleItemSelect = useCallback((checked, value) => {
setItemsSelected((x: any) =>
checked ? [...x.filter((y: any) => y !== value), value] : x.filter((y: any) => y !== value),
);
}, []);
useEffect(() => {
onSelect(itemsSelected);
}, [itemsSelected, onSelect]);
return (
<DialogButton
style={{
display: 'flex',
alignItems: 'center',
maxWidth: '100%',
}}
className={dropDownControlButtonClass}
onClick={(evt) => {
evt.preventDefault();
showContextMenu(
<Menu label={label} cancelText={t('DropdownMultiselect.button.back') as string}>
<style>
{`
/* Inherit color from ".basiccontextmenu" */
.decky_DropdownMultiselectItem_DialogCheckbox > .DialogToggle_Label {
color: inherit;
}
`}
</style>
<div style={{ marginTop: '10px' }}>{/*FIXME: Hack for missing padding under label menu*/}</div>
{items.map((x) => (
<DropdownMultiselectItem
key={x.value}
label={x.label}
value={x.value}
checked={itemsSelected.includes(x.value)}
onSelect={handleItemSelect}
/>
))}
</Menu>,
evt.currentTarget ?? window,
);
}}
>
<Marquee>
{selected.length > 0
? selected.map((x: any) => items[items.findIndex((v) => v.value === x)].label).join(', ')
: '…'}
</Marquee>
<div style={{ flexGrow: 1, minWidth: '1ch' }} />
<FaChevronDown style={{ height: '1em', flex: '0 0 1em' }} />
</DialogButton>
);
};
export default DropdownMultiselect;
@@ -0,0 +1,82 @@
import { ConfirmModal, Navigation, QuickAccessTab } from 'decky-frontend-lib';
import { FC, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { InstallType } from '../../plugin';
interface MultiplePluginsInstallModalProps {
requests: { name: string; version: string; hash: string; install_type: InstallType }[];
onOK(): void | Promise<void>;
onCancel(): void | Promise<void>;
closeModal?(): void;
}
// values are the JSON keys used in the translation file
const InstallTypeTranslationMapping = {
[InstallType.INSTALL]: 'install',
[InstallType.REINSTALL]: 'reinstall',
[InstallType.UPDATE]: 'update',
} as const satisfies Record<InstallType, string>;
type TitleTranslationMapping = 'mixed' | (typeof InstallTypeTranslationMapping)[InstallType];
const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
requests,
onOK,
onCancel,
closeModal,
}) => {
const [loading, setLoading] = useState<boolean>(false);
const { t } = useTranslation();
// used as part of the title translation
// if we know all operations are of a specific type, we can show so in the title to make decision easier
const installTypeGrouped = useMemo((): TitleTranslationMapping => {
if (requests.every(({ install_type }) => install_type === InstallType.INSTALL)) return 'install';
if (requests.every(({ install_type }) => install_type === InstallType.REINSTALL)) return 'reinstall';
if (requests.every(({ install_type }) => install_type === InstallType.UPDATE)) return 'update';
return 'mixed';
}, [requests]);
return (
<ConfirmModal
bOKDisabled={loading}
closeModal={closeModal}
onOK={async () => {
setLoading(true);
await onOK();
setTimeout(() => Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky), 250);
setTimeout(() => window.DeckyPluginLoader.checkPluginUpdates(), 1000);
}}
onCancel={async () => {
await onCancel();
}}
strTitle={<div>{t(`MultiplePluginsInstallModal.title.${installTypeGrouped}`, { count: requests.length })}</div>}
strOKButtonText={t(`MultiplePluginsInstallModal.ok_button.${loading ? 'loading' : 'idle'}`)}
>
<div>
{t('MultiplePluginsInstallModal.confirm')}
<ul style={{ listStyle: 'none', display: 'flex', flexDirection: 'column', gap: '4px' }}>
{requests.map(({ name, version, install_type, hash }, i) => {
const installTypeStr = InstallTypeTranslationMapping[install_type];
const description = t(`MultiplePluginsInstallModal.description.${installTypeStr}`, {
name,
version,
});
return (
<li key={i} style={{ display: 'flex', flexDirection: 'column' }}>
<div>{description}</div>
{hash === 'False' && (
<div style={{ color: 'red', paddingLeft: '10px' }}>{t('PluginInstallModal.no_hash')}</div>
)}
</li>
);
})}
</ul>
</div>
</ConfirmModal>
);
};
export default MultiplePluginsInstallModal;
@@ -1,18 +1,31 @@
import { ConfirmModal, Navigation, QuickAccessTab } from 'decky-frontend-lib';
import { FC, useState } from 'react';
import { useTranslation } from 'react-i18next';
import TranslationHelper, { TranslationClass } from '../../utils/TranslationHelper';
interface PluginInstallModalProps {
artifact: string;
version: string;
hash: string;
// reinstall: boolean;
installType: number;
onOK(): void;
onCancel(): void;
closeModal?(): void;
}
const PluginInstallModal: FC<PluginInstallModalProps> = ({ artifact, version, hash, onOK, onCancel, closeModal }) => {
const PluginInstallModal: FC<PluginInstallModalProps> = ({
artifact,
version,
hash,
installType,
onOK,
onCancel,
closeModal,
}) => {
const [loading, setLoading] = useState<boolean>(false);
const { t } = useTranslation();
return (
<ConfirmModal
bOKDisabled={loading}
@@ -26,14 +39,48 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({ artifact, version, ha
onCancel={async () => {
await onCancel();
}}
strTitle={`Install ${artifact}`}
strOKButtonText={loading ? 'Installing' : 'Install'}
strTitle={
<div>
<TranslationHelper
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
trans_text="title"
i18n_args={{ artifact: artifact }}
install_type={installType}
/>
</div>
}
strOKButtonText={
loading ? (
<div>
<TranslationHelper
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
trans_text="button_processing"
install_type={installType}
/>
</div>
) : (
<div>
<TranslationHelper
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
trans_text="button_idle"
install_type={installType}
/>
</div>
)
}
>
{hash == 'False' ? (
<h3 style={{ color: 'red' }}>!!!!NO HASH PROVIDED!!!!</h3>
) : (
`Are you sure you want to install ${artifact} ${version}?`
)}
<div>
<TranslationHelper
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
trans_text="desc"
i18n_args={{
artifact: artifact,
version: version,
}}
install_type={installType}
/>
</div>
{hash == 'False' && <span style={{ color: 'red' }}>{t('PluginInstallModal.no_hash')}</span>}
</ConfirmModal>
);
};
@@ -0,0 +1,31 @@
import { ConfirmModal } from 'decky-frontend-lib';
import { FC } from 'react';
interface PluginUninstallModalProps {
name: string;
title: string;
buttonText: string;
description: string;
closeModal?(): void;
}
const PluginUninstallModal: FC<PluginUninstallModalProps> = ({ name, title, buttonText, description, closeModal }) => {
return (
<ConfirmModal
closeModal={closeModal}
onOK={async () => {
await window.DeckyPluginLoader.callServerMethod('uninstall_plugin', { name });
// uninstalling a plugin resets the frozen and hidden setting for it server-side
// we invalidate here so if you re-install it, you won't have an out-of-date hidden filter
await window.DeckyPluginLoader.frozenPluginsService.invalidate();
await window.DeckyPluginLoader.hiddenPluginsService.invalidate();
}}
strTitle={title}
strOKButtonText={buttonText}
>
{description}
</ConfirmModal>
);
};
export default PluginUninstallModal;
@@ -0,0 +1,56 @@
import { FC, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { IconContext } from 'react-icons';
import { FaExclamationTriangle, FaQuestionCircle, FaUserSlash } from 'react-icons/fa';
export enum FileErrorTypes {
FileNotFound,
PermissionDenied,
Unknown,
None,
}
interface FilePickerErrorProps {
error: FileErrorTypes;
rawError?: string;
}
const FilePickerError: FC<FilePickerErrorProps> = ({ error, rawError = null }) => {
const [icon, setIcon] = useState<JSX.Element>(<FaQuestionCircle />);
const [text, setText] = useState<string | null>(null);
const { t } = useTranslation();
useEffect(() => {
switch (error) {
case FileErrorTypes.FileNotFound:
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 />);
break;
case FileErrorTypes.None:
setText(null);
setIcon(<div></div>);
break;
}
}, [error]);
return (
<>
<div style={{ paddingTop: '50px', textAlign: 'center', height: '100%' }}>
<IconContext.Provider value={{ className: 'fileError', size: '128px' }}>
<div style={{ alignSelf: 'center', alignContent: 'center' }}>{icon}</div>
</IconContext.Provider>
<p style={{ height: '32px', paddingTop: '25px', alignSelf: 'flex-start', textAlign: 'center' }}>{text}</p>
</div>
</>
);
};
export default FilePickerError;
@@ -0,0 +1,46 @@
import { FC } from 'react';
import { Translation } from 'react-i18next';
export enum SortOptions {
name_desc = 'name_desc',
name_asc = 'name_asc',
modified_desc = 'modified_desc',
modified_asc = 'modified_asc',
created_desc = 'created_desc',
created_asc = 'created_asc',
size_desc = 'size_desc',
size_asc = 'size_asc',
}
interface TSortOptionsProps {
trans_part: SortOptions;
}
const TSortOptions: FC<TSortOptionsProps> = ({ trans_part }) => {
return (
<Translation>
{(t, {}) => {
switch (trans_part) {
case SortOptions.name_desc:
return t('FilePickerIndex.filter.name_desc');
case SortOptions.name_asc:
return t('FilePickerIndex.filter.name_asce');
case SortOptions.modified_desc:
return t('FilePickerIndex.filter.modified_desc');
case SortOptions.modified_asc:
return t('FilePickerIndex.filter.modified_asce');
case SortOptions.created_desc:
return t('FilePickerIndex.filter.created_desc');
case SortOptions.created_asc:
return t('FilePickerIndex.filter.created_asce');
case SortOptions.size_desc:
return t('FilePickerIndex.filter.size_desc');
case SortOptions.size_asc:
return t('FilePickerIndex.filter.size_asce');
}
}}
</Translation>
);
};
export default TSortOptions;
@@ -38,7 +38,7 @@ const imageStyle = {
color: '#d18f00',
};
const imageExtList = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tif', 'tiff'];
const imageExtList = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tif', 'tiff', 'apng', 'tga'];
styleDef.push([imageStyle, imageExtList]);
@@ -1,10 +1,26 @@
import { DialogButton, Focusable, SteamSpinner, TextField } from 'decky-frontend-lib';
import { useEffect } from 'react';
import { FunctionComponent, useState } from 'react';
import { FileIcon, defaultStyles } from 'react-file-icon';
import {
ControlsList,
DialogBody,
DialogButton,
DialogControlsSection,
DialogFooter,
Dropdown,
Focusable,
Marquee,
SteamSpinner,
TextField,
ToggleField,
} from 'decky-frontend-lib';
import { filesize } from 'filesize';
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react';
import { DefaultExtensionType, FileIcon, defaultStyles } from 'react-file-icon';
import { useTranslation } from 'react-i18next';
import { FaArrowUp, FaFolder } from 'react-icons/fa';
import Logger from '../../../logger';
import DropdownMultiselect from '../DropdownMultiselect';
import FilePickerError, { FileErrorTypes } from './FilePickerError';
import TSortOption, { SortOptions } from './i18n/TSortOptions';
import { styleDefObj } from './iconCustomizations';
const logger = new Logger('FilePicker');
@@ -12,27 +28,95 @@ const logger = new Logger('FilePicker');
export interface FilePickerProps {
startPath: string;
includeFiles?: boolean;
regex?: RegExp;
includeFolders?: boolean;
filter?: RegExp | ((file: File) => boolean);
validFileExtensions?: string[];
allowAllFiles?: boolean;
defaultHidden?: boolean;
max?: number;
fileSelType?: FileSelectionType;
onSubmit: (val: { path: string; realpath: string }) => void;
closeModal?: () => void;
}
interface File {
export interface File {
isdir: boolean;
ishidden: boolean;
name: string;
realpath: string;
size: number;
modified: number;
created: number;
}
export enum FileSelectionType {
FILE,
FOLDER,
}
interface FileListing {
realpath: string;
files: File[];
total: number;
}
const sortOptions = [
{
data: SortOptions.name_desc,
label: <TSortOption trans_part={SortOptions.name_desc} />,
},
{
data: SortOptions.name_asc,
label: <TSortOption trans_part={SortOptions.name_asc} />,
},
{
data: SortOptions.modified_desc,
label: <TSortOption trans_part={SortOptions.modified_desc} />,
},
{
data: SortOptions.modified_asc,
label: <TSortOption trans_part={SortOptions.modified_asc} />,
},
{
data: SortOptions.created_desc,
label: <TSortOption trans_part={SortOptions.created_desc} />,
},
{
data: SortOptions.created_asc,
label: <TSortOption trans_part={SortOptions.created_asc} />,
},
{
data: SortOptions.size_desc,
label: <TSortOption trans_part={SortOptions.size_desc} />,
},
{
data: SortOptions.size_asc,
label: <TSortOption trans_part={SortOptions.size_asc} />,
},
];
function getList(
path: string,
includeFiles: boolean = true,
includeFiles: boolean,
includeFolders: boolean = true,
includeExt: string[] | null = null,
includeHidden: boolean = false,
orderBy: SortOptions = SortOptions.name_desc,
filterFor: RegExp | ((file: File) => boolean) | null = null,
pageNumber: number = 1,
max: number = 1000,
): Promise<{ result: FileListing | string; success: boolean }> {
return window.DeckyPluginLoader.callServerMethod('filepicker_ls', { path, include_files: includeFiles });
return window.DeckyPluginLoader.callServerMethod('filepicker_ls', {
path,
include_files: includeFiles,
include_folders: includeFolders,
include_ext: includeExt ? includeExt : [],
include_hidden: includeHidden,
order_by: orderBy,
filter_for: filterFor,
page: pageNumber,
max: max,
});
}
const iconStyles = {
@@ -42,118 +126,259 @@ const iconStyles = {
const FilePicker: FunctionComponent<FilePickerProps> = ({
startPath,
//What are we allowing to show in the file picker
includeFiles = true,
regex,
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,
}) => {
if (startPath.endsWith('/')) startPath = startPath.substring(0, startPath.length - 1); // remove trailing path
const { t } = useTranslation();
if (startPath !== '/' && startPath.endsWith('/')) startPath = startPath.substring(0, startPath.length - 1); // remove trailing path
const [path, setPath] = useState<string>(startPath);
const [listing, setListing] = useState<FileListing>({ files: [], realpath: path });
const [error, setError] = useState<string | null>(null);
const [listing, setListing] = useState<FileListing>({ files: [], realpath: path, total: 0 });
const [files, setFiles] = useState<File[]>([]);
const [error, setError] = useState<FileErrorTypes>(FileErrorTypes.None);
const [rawError, setRawError] = useState<string | null>(null);
const [page, setPage] = useState<number>(1);
const [loading, setLoading] = useState<boolean>(true);
const [showHidden, setShowHidden] = useState<boolean>(defaultHidden);
const [sort, setSort] = useState<SortOptions>(SortOptions.name_desc);
const [selectedExts, setSelectedExts] = useState<string[] | undefined>(validFileExtensions);
const validExtsOptions = useMemo(() => {
let validExt: { label: string; value: string }[] = [];
if (validFileExtensions) {
if (allowAllFiles) {
validExt.push({ label: t('FilePickerIndex.files.all_files'), value: 'all_files' });
}
validExt.push(...validFileExtensions.map((x) => ({ label: x, value: x })));
}
return validExt;
}, [validFileExtensions, allowAllFiles]);
function isSelectionValid(validExts: string[], selection: string[]) {
if (validExts.some((el) => selection.includes(el))) return true;
return false;
}
const handleExtsSelect = useCallback((val: any) => {
// unselect other options if "All Files" is checked
if (allowAllFiles && val.includes('all_files')) {
setSelectedExts(['all_files']);
} else if (validFileExtensions && isSelectionValid(validFileExtensions, val)) {
// If at least one extension is still selected, then assign this selection to the selected values
setSelectedExts(val);
} else {
// Else do nothing
setSelectedExts(selectedExts);
}
}, []);
useEffect(() => {
(async () => {
if (error) setError(null);
setLoading(true);
const listing = await getList(path, includeFiles);
const listing = await getList(
path,
includeFiles,
includeFolders,
selectedExts,
showHidden,
sort,
filter,
page,
max,
);
if (!listing.success) {
setListing({ files: [], realpath: path });
setListing({ files: [], realpath: path, total: 0 });
setLoading(false);
setError(listing.result as string);
logger.error(listing.result);
const theError = listing.result as string;
switch (theError) {
case theError.match(/\[Errno\s2.*/i)?.input:
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);
break;
}
logger.debug(theError);
return;
} else {
setRawError(null);
setError(FileErrorTypes.None);
setFiles((listing.result as FileListing).files);
}
setLoading(false);
setListing(listing.result as FileListing);
logger.log('reloaded', path, listing);
})();
}, [path]);
}, [error, path, includeFiles, includeFolders, showHidden, sort, selectedExts, page]);
return (
<div className="deckyFilePicker">
<Focusable style={{ display: 'flex', flexDirection: 'row', paddingBottom: '10px' }}>
<DialogButton
style={{
minWidth: 'unset',
width: '40px',
flexGrow: '0',
borderRadius: 'unset',
margin: '0',
padding: '10px',
}}
onClick={() => {
const newPathArr = path.split('/');
newPathArr.pop();
let newPath = newPathArr.join('/');
if (newPath == '') newPath = '/';
setPath(newPath);
}}
>
<FaArrowUp />
</DialogButton>
<div style={{ flexGrow: '1', width: '100%' }}>
<TextField
value={path}
onChange={(e) => {
e.target.value && setPath(e.target.value);
<>
<DialogBody className="deckyFilePicker">
<DialogControlsSection>
<Focusable flow-children="right" style={{ display: 'flex', marginBottom: '1em' }}>
<DialogButton
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minWidth: 'unset',
width: '40px',
borderRadius: 'unset',
margin: '0',
padding: '10px',
}}
onClick={() => {
const newPathArr = path.split('/');
const lastPath = newPathArr.pop();
//If I have a single / with spaces, pop the array twice
if (lastPath?.match(/^\/\s*$/) != null) newPathArr.pop();
let newPath = newPathArr.join('/');
if (newPath == '') newPath = '/';
setPath(newPath);
}}
>
<FaArrowUp />
</DialogButton>
<div style={{ width: '100%' }}>
<TextField
value={path}
onChange={(e) => {
e.target.value && setPath(e.target.value);
}}
style={{ height: '100%' }}
/>
</div>
</Focusable>
<ControlsList alignItems="center" spacing="standard">
<ToggleField
highlightOnFocus={false}
label={t('FilePickerIndex.files.show_hidden')}
bottomSeparator="none"
checked={showHidden}
onChange={() => setShowHidden((x) => !x)}
/>
<Dropdown rgOptions={sortOptions} selectedOption={sort} onChange={(x) => setSort(x.data)} />
{validFileExtensions && (
<DropdownMultiselect
label={t('FilePickerIndex.files.file_type')}
items={validExtsOptions}
selected={selectedExts ? selectedExts : []}
onSelect={handleExtsSelect}
/>
)}
</ControlsList>
</DialogControlsSection>
<DialogControlsSection style={{ marginTop: '1em' }}>
<Focusable
style={{ display: 'flex', gap: '.25em', flexDirection: 'column', height: '60vh', overflow: 'scroll' }}
>
{loading && error === FileErrorTypes.None && <SteamSpinner style={{ height: '100%' }} />}
{!loading &&
error === FileErrorTypes.None &&
files.map((file) => {
const extension = file.realpath.split('.').pop() as string;
return (
<DialogButton
key={`${file.realpath}${file.name}`}
style={{ borderRadius: 'unset', margin: '0', padding: '10px' }}
onClick={() => {
const fullPath = `${path}${path.endsWith('/') ? '' : '/'}${file.name}`;
if (file.isdir) setPath(fullPath);
else {
onSubmit({ path: fullPath, realpath: file.realpath });
closeModal?.();
}
}}
>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'flex-start' }}>
{file.isdir ? (
<FaFolder style={iconStyles} />
) : (
<div style={iconStyles}>
{file.realpath.includes('.') ? (
<FileIcon
{...defaultStyles[extension as DefaultExtensionType]}
// @ts-expect-error
{...styleDefObj[extension]}
extension={''}
/>
) : (
<FileIcon />
)}
</div>
)}
<Marquee>{file.name}</Marquee>
</div>
<div
style={{
display: 'flex',
opacity: 0.5,
fontSize: '.6em',
textAlign: 'left',
lineHeight: 1,
marginTop: '.5em',
}}
>
{file.isdir ? t('FilePickerIndex.folder.label') : filesize(file.size, { standard: 'iec' })}
<span style={{ marginLeft: 'auto' }}>{new Date(file.modified * 1000).toLocaleString()}</span>
</div>
</DialogButton>
);
})}
{error !== FileErrorTypes.None && <FilePickerError error={error} rawError={rawError ? rawError : ''} />}
</Focusable>
</DialogControlsSection>
</DialogBody>
{!loading && error === FileErrorTypes.None && (
<DialogFooter>
<DialogButton
className="Primary"
style={{ marginTop: '10px', alignSelf: 'flex-end' }}
onClick={() => {
onSubmit({ path, realpath: listing.realpath });
closeModal?.();
}}
style={{ height: '100%' }}
/>
</div>
</Focusable>
<Focusable style={{ display: 'flex', flexDirection: 'column', height: '60vh', overflow: 'scroll' }}>
{loading && <SteamSpinner style={{ height: '100%' }} />}
{!loading &&
listing.files
.filter((file) => (includeFiles || file.isdir) && (!regex || regex.test(file.name)))
.map((file) => {
let extension = file.realpath.split('.').pop() as string;
return (
<DialogButton
style={{ borderRadius: 'unset', margin: '0', padding: '10px' }}
onClick={() => {
const fullPath = `${path}${path.endsWith('/') ? '' : '/'}${file.name}`;
if (file.isdir) setPath(fullPath);
else {
onSubmit({ path: fullPath, realpath: file.realpath });
closeModal?.();
}
}}
>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'flex-start' }}>
{file.isdir ? (
<FaFolder style={iconStyles} />
) : (
<div style={iconStyles}>
{file.realpath.includes('.') ? (
<FileIcon {...defaultStyles[extension]} {...styleDefObj[extension]} extension={''} />
) : (
<FileIcon />
)}
</div>
)}
{file.name}
</div>
</DialogButton>
);
})}
{error}
</Focusable>
{!loading && !error && !includeFiles && (
<DialogButton
className="Primary"
style={{ marginTop: '10px', alignSelf: 'flex-end' }}
onClick={() => {
onSubmit({ path, realpath: listing.realpath });
closeModal?.();
}}
>
Use this folder
</DialogButton>
>
{fileSelType === FileSelectionType.FILE
? t('FilePickerIndex.file.select')
: t('FilePickerIndex.folder.select')}
</DialogButton>
</DialogFooter>
)}
</div>
{page * max < listing.total && (
<DialogFooter>
<DialogButton
className="Primary"
style={{ marginTop: '10px', alignSelf: 'flex-end' }}
onClick={() => {
setPage(page + 1);
}}
>
{t('FilePickerIndex.folder.show_more')}
</DialogButton>
</DialogFooter>
)}
</>
);
};
@@ -4,13 +4,6 @@ import Logger from '../../../../logger';
const logger = new Logger('LibraryPatch');
declare global {
interface Window {
SteamClient: any;
appDetailsStore: any;
}
}
let patch: Patch;
function rePatch() {
@@ -20,7 +13,9 @@ function rePatch() {
const details = window.appDetailsStore.GetAppDetails(appid);
logger.debug('game details', details);
// strShortcutStartDir
const file = await window.DeckyPluginLoader.openFilePicker(details.strShortcutStartDir.replaceAll('"', ''));
const file = await window.DeckyPluginLoader.openFilePicker(
details?.strShortcutStartDir.replaceAll('"', '') || '/',
);
logger.debug('user selected', file);
window.SteamClient.Apps.SetShortcutExe(appid, JSON.stringify(file.path));
const pathArr = file.path.split('/');
+19 -5
View File
@@ -1,6 +1,7 @@
import { SidebarNavigation } from 'decky-frontend-lib';
import { lazy } from 'react';
import { FaCode, FaPlug } from 'react-icons/fa';
import { useTranslation } from 'react-i18next';
import { FaCode, FaFlask, FaPlug } from 'react-icons/fa';
import { useSetting } from '../../utils/hooks/useSetting';
import DeckyIcon from '../DeckyIcon';
@@ -9,25 +10,27 @@ import GeneralSettings from './pages/general';
import PluginList from './pages/plugin_list';
const DeveloperSettings = lazy(() => import('./pages/developer'));
const TestingMenu = lazy(() => import('./pages/testing'));
export default function SettingsPage() {
const [isDeveloper, setIsDeveloper] = useSetting<boolean>('developer.enabled', false);
const { t } = useTranslation();
const pages = [
{
title: 'Decky',
title: t('SettingsIndex.general_title'),
content: <GeneralSettings isDeveloper={isDeveloper} setIsDeveloper={setIsDeveloper} />,
route: '/decky/settings/general',
icon: <DeckyIcon />,
},
{
title: 'Plugins',
content: <PluginList />,
title: t('SettingsIndex.plugins_title'),
content: <PluginList isDeveloper={isDeveloper} />,
route: '/decky/settings/plugins',
icon: <FaPlug />,
},
{
title: 'Developer',
title: t('SettingsIndex.developer_title'),
content: (
<WithSuspense>
<DeveloperSettings />
@@ -37,6 +40,17 @@ export default function SettingsPage() {
icon: <FaCode />,
visible: isDeveloper,
},
{
title: t('SettingsIndex.testing_title'),
content: (
<WithSuspense>
<TestingMenu />
</WithSuspense>
),
route: '/decky/settings/testing',
icon: <FaFlask />,
visible: isDeveloper,
},
];
return <SidebarNavigation pages={pages} />;
@@ -1,64 +1,157 @@
import { DialogBody, Field, TextField, Toggle } from 'decky-frontend-lib';
import { useRef } from 'react';
import { FaReact, FaSteamSymbol } from 'react-icons/fa';
import {
DialogBody,
DialogButton,
DialogControlsSection,
DialogControlsSectionHeader,
Field,
Navigation,
TextField,
Toggle,
} from 'decky-frontend-lib';
import { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaFileArchive, FaLink, FaReact, FaSteamSymbol, FaTerminal } from 'react-icons/fa';
import { setShouldConnectToReactDevTools, setShowValveInternal } from '../../../../developer';
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');
const installFromZip = async () => {
const path = await getSetting<string>('user_info.user_home', '');
if (path === '') {
logger.error('The default path has not been found!');
return;
}
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);
});
};
export default function DeveloperSettings() {
const [enableValveInternal, setEnableValveInternal] = useSetting<boolean>('developer.valve_internal', false);
const [reactDevtoolsEnabled, setReactDevtoolsEnabled] = useSetting<boolean>('developer.rdt.enabled', false);
const [reactDevtoolsIP, setReactDevtoolsIP] = useSetting<string>('developer.rdt.ip', '');
const [pluginURL, setPluginURL] = useState('');
const textRef = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
return (
<DialogBody>
<RemoteDebuggingSettings />
<Field
label="Enable Valve Internal"
description={
<span style={{ whiteSpace: 'pre-line' }}>
Enables the Valve internal developer menu.{' '}
<span style={{ color: 'red' }}>Do not touch anything in this menu unless you know what it does.</span>
</span>
}
icon={<FaSteamSymbol style={{ display: 'block' }} />}
>
<Toggle
value={enableValveInternal}
onChange={(toggleValue) => {
setEnableValveInternal(toggleValue);
setShowValveInternal(toggleValue);
}}
/>
</Field>
<Field
label="Enable React DevTools"
description={
<>
<DialogControlsSection>
<DialogControlsSectionHeader>
{t('SettingsDeveloperIndex.third_party_plugins.header')}
</DialogControlsSectionHeader>
<Field
label={t('SettingsDeveloperIndex.third_party_plugins.label_zip')}
icon={<FaFileArchive style={{ display: 'block' }} />}
>
<DialogButton onClick={installFromZip}>
{t('SettingsDeveloperIndex.third_party_plugins.button_zip')}
</DialogButton>
</Field>
<Field
label={t('SettingsDeveloperIndex.third_party_plugins.label_url')}
description={
<TextField
label={t('SettingsDeveloperIndex.third_party_plugins.label_desc')}
value={pluginURL}
onChange={(e) => setPluginURL(e?.target.value)}
/>
}
icon={<FaLink style={{ display: 'block' }} />}
>
<DialogButton disabled={pluginURL.length == 0} onClick={() => installFromURL(pluginURL)}>
{t('SettingsDeveloperIndex.third_party_plugins.button_install')}
</DialogButton>
</Field>
</DialogControlsSection>
<DialogControlsSection>
<DialogControlsSectionHeader>{t('SettingsDeveloperIndex.header')}</DialogControlsSectionHeader>
<Field
label={t('SettingsDeveloperIndex.cef_console.label')}
description={<span style={{ whiteSpace: 'pre-line' }}>{t('SettingsDeveloperIndex.cef_console.desc')}</span>}
icon={<FaTerminal style={{ display: 'block' }} />}
>
<DialogButton
onClick={async () => {
let res = await window.DeckyPluginLoader.callServerMethod('get_tab_id', { name: 'SharedJSContext' });
if (res.success) {
Navigation.NavigateToExternalWeb(
'localhost:8080/devtools/inspector.html?ws=localhost:8080/devtools/page/' + res.result,
);
} else {
console.error('Unable to find ID for SharedJSContext tab ', res.result);
Navigation.NavigateToExternalWeb('localhost:8080');
}
}}
>
{t('SettingsDeveloperIndex.cef_console.button')}
</DialogButton>
</Field>
<RemoteDebuggingSettings />
<Field
label={t('SettingsDeveloperIndex.valve_internal.label')}
description={
<span style={{ whiteSpace: 'pre-line' }}>
Enables connection to a computer running React DevTools. Changing this setting will reload Steam. Set the
IP address before enabling.
{t('SettingsDeveloperIndex.valve_internal.desc1')}{' '}
<span style={{ color: 'red' }}>{t('SettingsDeveloperIndex.valve_internal.desc2')}</span>
</span>
<br />
<br />
<div ref={textRef}>
<TextField label={'IP'} value={reactDevtoolsIP} onChange={(e) => setReactDevtoolsIP(e?.target.value)} />
</div>
</>
}
icon={<FaReact style={{ display: 'block' }} />}
>
<Toggle
value={reactDevtoolsEnabled}
disabled={reactDevtoolsIP == ''}
onChange={(toggleValue) => {
setReactDevtoolsEnabled(toggleValue);
setShouldConnectToReactDevTools(toggleValue);
}}
/>
</Field>
}
icon={<FaSteamSymbol style={{ display: 'block' }} />}
>
<Toggle
value={enableValveInternal}
onChange={(toggleValue) => {
setEnableValveInternal(toggleValue);
setShowValveInternal(toggleValue);
}}
/>
</Field>
<Field
label={t('SettingsDeveloperIndex.react_devtools.label')}
description={
<>
<span style={{ whiteSpace: 'pre-line' }}>{t('SettingsDeveloperIndex.react_devtools.desc')}</span>
<br />
<br />
<div ref={textRef}>
<TextField
label={t('SettingsDeveloperIndex.react_devtools.ip_label')}
value={reactDevtoolsIP}
onChange={(e) => setReactDevtoolsIP(e?.target.value)}
/>
</div>
</>
}
icon={<FaReact style={{ display: 'block' }} />}
>
<Toggle
value={reactDevtoolsEnabled}
// disabled={reactDevtoolsIP == ''}
onChange={(toggleValue) => {
setReactDevtoolsEnabled(toggleValue);
setShouldConnectToReactDevTools(toggleValue);
}}
/>
</Field>
</DialogControlsSection>
</DialogBody>
);
}
@@ -1,5 +1,6 @@
import { Dropdown, Field } from 'decky-frontend-lib';
import { FunctionComponent } from 'react';
import { useTranslation } from 'react-i18next';
import Logger from '../../../../logger';
import { callUpdaterMethod } from '../../../../updater';
@@ -7,25 +8,36 @@ import { useSetting } from '../../../../utils/hooks/useSetting';
const logger = new Logger('BranchSelect');
enum UpdateBranch {
export enum UpdateBranch {
Stable,
Prerelease,
Testing,
}
enum LessUpdateBranch {
Stable,
Prerelease,
// Testing,
}
const BranchSelect: FunctionComponent<{}> = () => {
const [selectedBranch, setSelectedBranch] = useSetting<UpdateBranch>('branch', UpdateBranch.Prerelease);
const { t } = useTranslation();
const tBranches = [
t('BranchSelect.update_channel.stable'),
t('BranchSelect.update_channel.prerelease'),
t('BranchSelect.update_channel.testing'),
];
const [selectedBranch, setSelectedBranch] = useSetting<UpdateBranch>('branch', UpdateBranch.Stable);
return (
// Returns numerical values from 0 to 2 (with current branch setup as of 8/28/22)
// 0 being stable, 1 being pre-release and 2 being nightly
<Field label="Decky Update Channel" childrenContainerWidth={'fixed'}>
// Returns numerical values from 0 to 2 (with current branch setup as of 6/16/23)
// 0 being stable, 1 being pre-release and 2 being testing (not a branch!)
<Field label={t('BranchSelect.update_channel.label')} childrenContainerWidth={'fixed'}>
<Dropdown
rgOptions={Object.values(UpdateBranch)
.filter((branch) => typeof branch == 'string')
rgOptions={Object.values(selectedBranch == UpdateBranch.Testing ? UpdateBranch : LessUpdateBranch)
.filter((branch) => typeof branch == 'number')
.map((branch) => ({
label: 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;
@@ -1,19 +1,17 @@
import { Field, Toggle } from 'decky-frontend-lib';
import { useTranslation } from 'react-i18next';
import { FaChrome } from 'react-icons/fa';
import { useSetting } from '../../../../utils/hooks/useSetting';
export default function RemoteDebuggingSettings() {
const [allowRemoteDebugging, setAllowRemoteDebugging] = useSetting<boolean>('cef_forward', false);
const { t } = useTranslation();
return (
<Field
label="Allow Remote CEF Debugging"
description={
<span style={{ whiteSpace: 'pre-line' }}>
Allows unauthenticated access to the CEF debugger to anyone in your network.
</span>
}
label={t('RemoteDebugging.remote_cef.label')}
description={<span style={{ whiteSpace: 'pre-line' }}>{t('RemoteDebugging.remote_cef.desc')}</span>}
icon={<FaChrome style={{ display: 'block' }} />}
>
<Toggle
@@ -1,5 +1,6 @@
import { Dropdown, Field, TextField } from 'decky-frontend-lib';
import { FunctionComponent } from 'react';
import { useTranslation } from 'react-i18next';
import { FaShapes } from 'react-icons/fa';
import Logger from '../../../../logger';
@@ -11,18 +12,24 @@ const logger = new Logger('StoreSelect');
const StoreSelect: FunctionComponent<{}> = () => {
const [selectedStore, setSelectedStore] = useSetting<Store>('store', Store.Default);
const [selectedStoreURL, setSelectedStoreURL] = useSetting<string | null>('store-url', null);
const { t } = useTranslation();
const tStores = [
t('StoreSelect.store_channel.default'),
t('StoreSelect.store_channel.testing'),
t('StoreSelect.store_channel.custom'),
];
// Returns numerical values from 0 to 2 (with current branch setup as of 8/28/22)
// 0 being Default, 1 being Testing and 2 being Custom
return (
<>
<Field label="Plugin Store Channel" childrenContainerWidth={'fixed'}>
<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: store,
data: Store[store],
label: tStores[store as number],
data: store,
}))}
selectedOption={selectedStore}
onChange={async (newVal) => {
@@ -33,11 +40,11 @@ const StoreSelect: FunctionComponent<{}> = () => {
</Field>
{selectedStore == Store.Custom && (
<Field
label="Custom Store"
label={t('StoreSelect.custom_store.label')}
indentLevel={1}
description={
<TextField
label={'URL'}
label={t('StoreSelect.custom_store.url_label')}
value={selectedStoreURL || undefined}
onChange={(e) => setSelectedStoreURL(e?.target.value || null)}
/>
@@ -6,15 +6,16 @@ import {
Focusable,
ProgressBarWithInfo,
Spinner,
findSP,
showModal,
} from 'decky-frontend-lib';
import { useCallback } from 'react';
import { Suspense, lazy } from 'react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaExclamation } from 'react-icons/fa';
import { VerInfo, callUpdaterMethod, finishUpdate } from '../../../../updater';
import { findSP } from '../../../../utils/windows';
import { useDeckyState } from '../../../DeckyState';
import InlinePatchNotes from '../../../patchnotes/InlinePatchNotes';
import WithSuspense from '../../../WithSuspense';
@@ -23,6 +24,7 @@ const MarkdownRenderer = lazy(() => import('../../../Markdown'));
function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | null; closeModal?: () => {} }) {
const SP = findSP();
const { t } = useTranslation();
return (
<Focusable onCancelButton={closeModal}>
<FocusRing>
@@ -39,13 +41,13 @@ function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | n
}}
>
<div>
<h1>{versionInfo?.all?.[id]?.name}</h1>
<h1>{versionInfo?.all?.[id]?.name || 'Invalid Update Name'}</h1>
{versionInfo?.all?.[id]?.body ? (
<WithSuspense>
<MarkdownRenderer onDismiss={closeModal}>{versionInfo.all[id].body}</MarkdownRenderer>
</WithSuspense>
) : (
'no patch notes for this version'
t('Updater.no_patch_notes_desc')
)}
</div>
</Focusable>
@@ -58,7 +60,7 @@ function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | n
initialColumn={0}
autoFocus={true}
fnGetColumnWidth={() => SP.innerWidth}
name="Decky Updates"
name={t('Updater.decky_updates') as string}
/>
</FocusRing>
</Focusable>
@@ -72,6 +74,8 @@ export default function UpdaterSettings() {
const [updateProgress, setUpdateProgress] = useState<number>(-1);
const [reloading, setReloading] = useState<boolean>(false);
const { t } = useTranslation();
useEffect(() => {
window.DeckyUpdater = {
updateProgress: (i) => {
@@ -93,14 +97,14 @@ export default function UpdaterSettings() {
return (
<>
<Field
onOptionsActionDescription={versionInfo?.all ? 'Patch Notes' : undefined}
onOptionsActionDescription={versionInfo?.all ? t('Updater.patch_notes_desc') : undefined}
onOptionsButton={versionInfo?.all ? showPatchNotes : undefined}
label="Decky Updates"
label={t('Updater.updates.label')}
description={
checkingForUpdates || versionInfo?.remote?.tag_name != versionInfo?.current || !versionInfo?.remote ? (
''
) : (
<span>Up to date: running {versionInfo?.current}</span>
<span>{t('Updater.updates.lat_version', { ver: versionInfo?.current })} </span>
)
}
icon={
@@ -129,10 +133,10 @@ export default function UpdaterSettings() {
}
>
{checkingForUpdates
? 'Checking'
? t('Updater.updates.checking')
: !versionInfo?.remote || versionInfo?.remote?.tag_name == versionInfo?.current
? 'Check For Updates'
: 'Install Update'}
? t('Updater.updates.check_button')
: t('Updater.updates.install_button')}
</DialogButton>
) : (
<ProgressBarWithInfo
@@ -140,7 +144,7 @@ export default function UpdaterSettings() {
bottomSeparator="none"
nProgress={updateProgress}
indeterminate={reloading}
sOperationText={reloading ? 'Reloading' : 'Updating'}
sOperationText={reloading ? t('Updater.updates.reloading') : t('Updater.updates.updating')}
/>
)}
</Field>
@@ -1,17 +1,9 @@
import {
DialogBody,
DialogButton,
DialogControlsSection,
DialogControlsSectionHeader,
Field,
TextField,
Toggle,
} from 'decky-frontend-lib';
import { useState } from 'react';
import { DialogBody, DialogControlsSection, DialogControlsSectionHeader, Field, Toggle } from 'decky-frontend-lib';
import { useTranslation } from 'react-i18next';
import { installFromURL } from '../../../../store';
import { useDeckyState } from '../../../DeckyState';
import BranchSelect from './BranchSelect';
import NotificationSettings from './NotificationSettings';
import StoreSelect from './StoreSelect';
import UpdaterSettings from './Updater';
@@ -22,23 +14,27 @@ export default function GeneralSettings({
isDeveloper: boolean;
setIsDeveloper: (val: boolean) => void;
}) {
const [pluginURL, setPluginURL] = useState('');
const { versionInfo } = useDeckyState();
const { t } = useTranslation();
return (
<DialogBody>
<DialogControlsSection>
<DialogControlsSectionHeader>Updates</DialogControlsSectionHeader>
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.updates.header')}</DialogControlsSectionHeader>
<UpdaterSettings />
</DialogControlsSection>
<DialogControlsSection>
<DialogControlsSectionHeader>Beta Participation</DialogControlsSectionHeader>
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.beta.header')}</DialogControlsSectionHeader>
<BranchSelect />
<StoreSelect />
</DialogControlsSection>
<DialogControlsSection>
<DialogControlsSectionHeader>Other</DialogControlsSectionHeader>
<Field label="Enable Developer Mode">
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.notifications.header')}</DialogControlsSectionHeader>
<NotificationSettings />
</DialogControlsSection>
<DialogControlsSection>
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.other.header')}</DialogControlsSectionHeader>
<Field label={t('SettingsGeneralIndex.developer_mode.label')}>
<Toggle
value={isDeveloper}
onChange={(toggleValue) => {
@@ -46,18 +42,10 @@ export default function GeneralSettings({
}}
/>
</Field>
<Field
label="Install plugin from URL"
description={<TextField label={'URL'} value={pluginURL} onChange={(e) => setPluginURL(e?.target.value)} />}
>
<DialogButton disabled={pluginURL.length == 0} onClick={() => installFromURL(pluginURL)}>
Install
</DialogButton>
</Field>
</DialogControlsSection>
<DialogControlsSection>
<DialogControlsSectionHeader>About</DialogControlsSectionHeader>
<Field label="Decky Version" focusable={true}>
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.about.header')}</DialogControlsSectionHeader>
<Field label={t('SettingsGeneralIndex.about.decky_version')} focusable={true}>
<div style={{ color: 'var(--gpSystemLighterGrey)' }}>{versionInfo?.current}</div>
</Field>
</DialogControlsSection>
@@ -0,0 +1,50 @@
import { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { FaEyeSlash, FaLock } from 'react-icons/fa';
interface PluginListLabelProps {
frozen: boolean;
hidden: boolean;
name: string;
version?: string;
}
const PluginListLabel: FC<PluginListLabelProps> = ({ name, frozen, hidden, version }) => {
const { t } = useTranslation();
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
<div>
{name}
{version && (
<>
{' - '}
<span style={{ color: frozen ? '#67707b' : 'inherit' }}>
{frozen && (
<>
<FaLock />{' '}
</>
)}
{version}
</span>
</>
)}
</div>
{hidden && (
<div
style={{
fontSize: '0.8rem',
color: '#dcdedf',
display: 'flex',
alignItems: 'center',
gap: '10px',
}}
>
<FaEyeSlash />
{t('PluginListLabel.hidden')}
</div>
)}
</div>
);
};
export default PluginListLabel;
@@ -2,78 +2,241 @@ import {
DialogBody,
DialogButton,
DialogControlsSection,
Focusable,
GamepadEvent,
Menu,
MenuItem,
ReorderableEntry,
ReorderableList,
showContextMenu,
} from 'decky-frontend-lib';
import { useEffect } from 'react';
import { FaDownload, FaEllipsisH } from 'react-icons/fa';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaDownload, FaEllipsisH, FaRecycle } from 'react-icons/fa';
import { requestPluginInstall } from '../../../../store';
import { InstallType } from '../../../../plugin';
import {
StorePluginVersion,
getPluginList,
requestMultiplePluginInstalls,
requestPluginInstall,
} from '../../../../store';
import { useSetting } from '../../../../utils/hooks/useSetting';
import { useDeckyState } from '../../../DeckyState';
import PluginListLabel from './PluginListLabel';
export default function PluginList() {
const { plugins, updates } = useDeckyState();
async function reinstallPlugin(pluginName: string, currentVersion?: string) {
const serverData = await getPluginList();
const remotePlugin = serverData?.find((x) => x.name == pluginName);
if (remotePlugin && remotePlugin.versions?.length > 0) {
const currentVersionData = remotePlugin.versions.find((version) => version.name == currentVersion);
if (currentVersionData) requestPluginInstall(pluginName, currentVersionData, InstallType.REINSTALL);
}
}
type PluginTableData = PluginData & {
name: string;
frozen: boolean;
onFreeze(): void;
onUnfreeze(): void;
hidden: boolean;
onHide(): void;
onShow(): void;
isDeveloper: boolean;
};
function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }) {
const { t } = useTranslation();
// nothing to display without this data...
if (!props.entry.data) {
return null;
}
const { name, update, version, onHide, onShow, hidden, onFreeze, onUnfreeze, frozen, isDeveloper } = props.entry.data;
const showCtxMenu = (e: MouseEvent | GamepadEvent) => {
showContextMenu(
<Menu label={t('PluginListIndex.plugin_actions')}>
<MenuItem
onSelected={() => {
try {
fetch(`http://127.0.0.1:1337/plugins/${name}/reload`, {
method: 'POST',
credentials: 'include',
headers: {
Authentication: window.deckyAuthToken,
},
});
} catch (err) {
console.error('Error Reloading Plugin Backend', err);
}
window.DeckyPluginLoader.importPlugin(name, version);
}}
>
{t('PluginListIndex.reload')}
</MenuItem>
<MenuItem
onSelected={() =>
window.DeckyPluginLoader.uninstallPlugin(
name,
t('PluginLoader.plugin_uninstall.title', { name }),
t('PluginLoader.plugin_uninstall.button'),
t('PluginLoader.plugin_uninstall.desc', { name }),
)
}
>
{t('PluginListIndex.uninstall')}
</MenuItem>
{hidden ? (
<MenuItem onSelected={onShow}>{t('PluginListIndex.show')}</MenuItem>
) : (
<MenuItem onSelected={onHide}>{t('PluginListIndex.hide')}</MenuItem>
)}
{frozen ? (
<MenuItem onSelected={onUnfreeze}>{t('PluginListIndex.unfreeze')}</MenuItem>
) : (
isDeveloper && <MenuItem onSelected={onFreeze}>{t('PluginListIndex.freeze')}</MenuItem>
)}
</Menu>,
e.currentTarget ?? window,
);
};
return (
<>
{update ? (
<DialogButton
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
onClick={() => requestPluginInstall(name, update, InstallType.UPDATE)}
onOKButton={() => requestPluginInstall(name, update, InstallType.UPDATE)}
>
<div style={{ display: 'flex', minWidth: '180px', justifyContent: 'space-between', alignItems: 'center' }}>
{t('PluginListIndex.update_to', { name: update.name })}
<FaDownload style={{ paddingLeft: '1rem' }} />
</div>
</DialogButton>
) : (
<DialogButton
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
onClick={() => reinstallPlugin(name, version)}
onOKButton={() => reinstallPlugin(name, version)}
>
<div style={{ display: 'flex', minWidth: '180px', justifyContent: 'space-between', alignItems: 'center' }}>
{t('PluginListIndex.reinstall')}
<FaRecycle style={{ paddingLeft: '1rem' }} />
</div>
</DialogButton>
)}
<DialogButton
style={{
height: '40px',
width: '40px',
padding: '10px 12px',
minWidth: '40px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
}}
onClick={showCtxMenu}
onOKButton={showCtxMenu}
>
<FaEllipsisH />
</DialogButton>
</>
);
}
type PluginData = {
update?: StorePluginVersion;
version?: string;
};
export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) {
const { plugins, updates, pluginOrder, setPluginOrder, frozenPlugins, hiddenPlugins } = useDeckyState();
const [_, setPluginOrderSetting] = useSetting<string[]>(
'pluginOrder',
plugins.map((plugin) => plugin.name),
);
const { t } = useTranslation();
useEffect(() => {
window.DeckyPluginLoader.checkPluginUpdates();
}, []);
const [pluginEntries, setPluginEntries] = useState<ReorderableEntry<PluginTableData>[]>([]);
const frozenPluginsService = window.DeckyPluginLoader.frozenPluginsService;
const hiddenPluginsService = window.DeckyPluginLoader.hiddenPluginsService;
useEffect(() => {
setPluginEntries(
plugins.map(({ name, version }) => {
const frozen = frozenPlugins.includes(name);
const hidden = hiddenPlugins.includes(name);
return {
label: <PluginListLabel name={name} frozen={frozen} hidden={hidden} version={version} />,
position: pluginOrder.indexOf(name),
data: {
name,
frozen,
hidden,
isDeveloper,
version,
update: updates?.get(name),
onFreeze: () => frozenPluginsService.update([...frozenPlugins, name]),
onUnfreeze: () => frozenPluginsService.update(frozenPlugins.filter((pluginName) => name !== pluginName)),
onHide: () => hiddenPluginsService.update([...hiddenPlugins, name]),
onShow: () => hiddenPluginsService.update(hiddenPlugins.filter((pluginName) => name !== pluginName)),
},
};
}),
);
}, [plugins, updates, hiddenPlugins]);
if (plugins.length === 0) {
return (
<div>
<p>No plugins installed</p>
<p>{t('PluginListIndex.no_plugin')}</p>
</div>
);
}
function onSave(entries: ReorderableEntry<PluginTableData>[]) {
const newOrder = entries.map((entry) => entry.data!.name);
console.log(newOrder);
setPluginOrder(newOrder);
setPluginOrderSetting(newOrder);
}
return (
<DialogBody>
<DialogControlsSection>
<ul style={{ listStyleType: 'none', padding: '0' }}>
{plugins.map(({ name, version }) => {
const update = updates?.get(name);
return (
<li style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', paddingBottom: '10px' }}>
<span>
{name} <span style={{ opacity: '50%' }}>{'(' + version + ')'}</span>
</span>
<Focusable style={{ marginLeft: 'auto', boxShadow: 'none', display: 'flex', justifyContent: 'right' }}>
{update && (
<DialogButton
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
onClick={() => requestPluginInstall(name, update)}
>
<div style={{ display: 'flex', flexDirection: 'row' }}>
Update to {update.name}
<FaDownload style={{ paddingLeft: '2rem' }} />
</div>
</DialogButton>
)}
<DialogButton
style={{ height: '40px', width: '40px', padding: '10px 12px', minWidth: '40px' }}
onClick={(e: MouseEvent) =>
showContextMenu(
<Menu label="Plugin Actions">
<MenuItem onSelected={() => window.DeckyPluginLoader.importPlugin(name, version)}>
Reload
</MenuItem>
<MenuItem onSelected={() => window.DeckyPluginLoader.uninstallPlugin(name)}>
Uninstall
</MenuItem>
</Menu>,
e.currentTarget ?? window,
)
}
>
<FaEllipsisH />
</DialogButton>
</Focusable>
</li>
);
})}
</ul>
{updates && updates.size > 0 && (
<DialogButton
onClick={() =>
requestMultiplePluginInstalls(
[...updates.entries()].map(([plugin, selectedVer]) => ({
installType: InstallType.UPDATE,
plugin,
selectedVer,
})),
)
}
style={{
position: 'absolute',
top: '57px',
right: '2.8vw',
width: 'auto',
display: 'flex',
alignItems: 'center',
}}
>
{t('PluginListIndex.update_all', { count: updates.size })}
<FaDownload style={{ paddingLeft: '1rem' }} />
</DialogButton>
)}
<DialogControlsSection style={{ marginTop: 0 }}>
<ReorderableList<PluginTableData> entries={pluginEntries} onSave={onSave} interactables={PluginInteractables} />
</DialogControlsSection>
</DialogBody>
);
@@ -0,0 +1,87 @@
import { DialogBody, DialogButton, DialogControlsSection, Focusable, Navigation } from 'decky-frontend-lib';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaDownload, FaInfo } from 'react-icons/fa';
import { callUpdaterMethod } from '../../../../updater';
import { setSetting } from '../../../../utils/settings';
import { UpdateBranch } from '../general/BranchSelect';
interface TestingVersion {
id: number;
name: string;
link: string;
head_sha: string;
}
export default function TestingVersionList() {
const { t } = useTranslation();
const [testingVersions, setTestingVersions] = useState<TestingVersion[]>([]);
useEffect(() => {
(async () => {
setTestingVersions((await callUpdaterMethod('get_testing_versions')).result);
})();
}, []);
if (testingVersions.length === 0) {
return (
<div>
<p>No open PRs found</p>
</div>
);
}
return (
<DialogBody>
<DialogControlsSection>
<ul style={{ listStyleType: 'none', padding: '0' }}>
{testingVersions.map((version) => {
return (
<li style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', paddingBottom: '10px' }}>
<span>
{version.name} <span style={{ opacity: '50%' }}>{'#' + version.id}</span>
</span>
<Focusable style={{ height: '40px', marginLeft: 'auto', display: 'flex' }}>
<DialogButton
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
onClick={() => {
callUpdaterMethod('download_testing_version', { pr_id: version.id, sha_id: version.head_sha });
setSetting('branch', UpdateBranch.Testing);
}}
>
<div
style={{
display: 'flex',
minWidth: '150px',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
{t('Testing.download')}
<FaDownload style={{ paddingLeft: '1rem' }} />
</div>
</DialogButton>
<DialogButton
style={{
height: '40px',
width: '40px',
padding: '10px 12px',
minWidth: '40px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
}}
onClick={() => Navigation.NavigateToExternalWeb(version.link)}
>
<FaInfo />
</DialogButton>
</Focusable>
</li>
);
})}
</ul>
</DialogControlsSection>
</DialogBody>
);
}
+82 -84
View File
@@ -6,8 +6,10 @@ import {
SingleDropdownOption,
SuspensefulImage,
} from 'decky-frontend-lib';
import { FC, useState } from 'react';
import { CSSProperties, FC, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { InstallType } from '../../plugin';
import { StorePlugin, StorePluginVersion, requestPluginInstall } from '../../store';
interface PluginCardProps {
@@ -16,7 +18,9 @@ interface PluginCardProps {
const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
const [selectedOption, setSelectedOption] = useState<number>(0);
const root: boolean = plugin.tags.some((tag) => tag === 'root');
const root = plugin.tags.some((tag) => tag === 'root');
const { t } = useTranslation();
return (
<div
@@ -26,7 +30,6 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
marginRight: '20px',
marginBottom: '20px',
display: 'flex',
alignItems: 'center',
}}
>
<div
@@ -55,107 +58,102 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
width: 'calc(100% - 320px)', // The calc is here so that the info section doesn't expand into the image
display: 'flex',
flexDirection: 'column',
height: '100%',
justifyContent: 'space-between',
marginLeft: '1em',
justifyContent: 'center',
gap: '10px',
}}
>
<span
className="deckyStoreCardTitle"
style={{
fontSize: '1.25em',
fontWeight: 'bold',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
width: '90%',
}}
>
{plugin.name}
</span>
<span
className="deckyStoreCardAuthor"
style={{
marginRight: 'auto',
fontSize: '1em',
}}
>
{plugin.author}
</span>
<span
className="deckyStoreCardDescription"
style={{
fontSize: '13px',
color: '#969696',
WebkitLineClamp: root ? '2' : '3',
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
display: '-webkit-box',
}}
>
{plugin.description ? (
plugin.description
) : (
<span>
<i style={{ color: '#666' }}>No description provided.</i>
</span>
)}
</span>
{root && (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<span
className="deckyStoreCardDescription deckyStoreCardDescriptionRoot"
className="deckyStoreCardTitle"
style={{
fontSize: '13px',
color: '#fee75c',
fontSize: '1.25em',
fontWeight: 'bold',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
width: '90%',
}}
>
<i>This plugin has full access to your Steam Deck.</i>{' '}
<a
className="deckyStoreCardDescriptionRootLink"
href="https://deckbrew.xyz/root"
target="_blank"
{plugin.name}
</span>
<span
className="deckyStoreCardAuthor"
style={{
marginRight: 'auto',
fontSize: '1em',
}}
>
{plugin.author}
</span>
<span
className="deckyStoreCardDescription"
style={{
fontSize: '13px',
color: '#969696',
WebkitLineClamp: root ? '2' : '3',
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
display: '-webkit-box',
}}
>
{plugin.description ? (
plugin.description
) : (
<span>
<i style={{ color: '#666' }}>{t('PluginCard.plugin_no_desc')}</i>
</span>
)}
</span>
{root && (
<div
className="deckyStoreCardDescription deckyStoreCardDescriptionRoot"
style={{
fontSize: '13px',
color: '#fee75c',
textDecoration: 'none',
marginTop: 'auto',
}}
>
deckbrew.xyz/root
</a>
</span>
)}
<div
className="deckyStoreCardButtonRow"
style={{
marginTop: '1em',
width: '100%',
overflow: 'hidden',
}}
>
<i>{t('PluginCard.plugin_full_access')}</i>{' '}
<a
className="deckyStoreCardDescriptionRootLink"
href="https://deckbrew.xyz/root"
target="_blank"
style={{
color: '#fee75c',
textDecoration: 'none',
}}
>
deckbrew.xyz/root
</a>
</div>
)}
</div>
<div className="deckyStoreCardButtonRow">
<PanelSectionRow>
<Focusable style={{ display: 'flex', maxWidth: '100%' }}>
<Focusable style={{ display: 'flex', gap: '5px', padding: 0 }}>
<div
className="deckyStoreCardInstallContainer"
style={{
paddingTop: '0px',
paddingBottom: '0px',
width: '40%',
}}
style={
{
paddingTop: '0px',
paddingBottom: '0px',
flexGrow: 1,
'--field-negative-horizontal-margin': 0,
} as CSSProperties
}
>
<ButtonItem
bottomSeparator="none"
layout="below"
onClick={() => requestPluginInstall(plugin.name, plugin.versions[selectedOption])}
onClick={() =>
requestPluginInstall(plugin.name, plugin.versions[selectedOption], InstallType.INSTALL)
}
>
<span className="deckyStoreCardInstallText">Install</span>
<span className="deckyStoreCardInstallText">{t('PluginCard.plugin_install')}</span>
</ButtonItem>
</div>
<div
className="deckyStoreCardVersionContainer"
style={{
marginLeft: '5%',
width: '30%',
}}
>
<div className="deckyStoreCardVersionContainer" style={{ minWidth: '130px' }}>
<Dropdown
rgOptions={
plugin.versions.map((version: StorePluginVersion, index) => ({
@@ -163,7 +161,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
label: version.name,
})) as SingleDropdownOption[]
}
menuLabel="Plugin Version"
menuLabel={t('PluginCard.plugin_version_label') as string}
selectedOption={selectedOption}
onChange={({ data }) => setSelectedOption(data)}
/>
+130 -81
View File
@@ -8,30 +8,25 @@ import {
TextField,
findModule,
} from 'decky-frontend-lib';
import { FC, useEffect, useMemo, useState } from 'react';
import { Dispatch, FC, SetStateAction, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import logo from '../../../assets/plugin_store.png';
import Logger from '../../logger';
import { StorePlugin, getPluginList } from '../../store';
import { SortDirections, SortOptions, Store, StorePlugin, getPluginList, getStore } from '../../store';
import PluginCard from './PluginCard';
const logger = new Logger('FilePicker');
const logger = new Logger('Store');
const StorePage: FC<{}> = () => {
const [currentTabRoute, setCurrentTabRoute] = useState<string>('browse');
const [data, setData] = useState<StorePlugin[] | null>(null);
const [pluginCount, setPluginCount] = useState<number | null>(null);
const { TabCount } = findModule((m) => {
if (m?.TabCount && m?.TabTitle) return true;
return false;
});
useEffect(() => {
(async () => {
const res = await getPluginList();
logger.log('got data!', res);
setData(res);
})();
}, []);
const { t } = useTranslation();
return (
<>
@@ -42,50 +37,71 @@ const StorePage: FC<{}> = () => {
background: '#0005',
}}
>
{!data ? (
<div style={{ height: '100%' }}>
<SteamSpinner />
</div>
) : (
<Tabs
activeTab={currentTabRoute}
onShowTab={(tabId: string) => {
setCurrentTabRoute(tabId);
}}
tabs={[
{
title: 'Browse',
content: <BrowseTab children={{ data: data }} />,
id: 'browse',
renderTabAddon: () => <span className={TabCount}>{data.length}</span>,
},
{
title: 'About',
content: <AboutTab />,
id: 'about',
},
]}
/>
)}
<Tabs
activeTab={currentTabRoute}
onShowTab={(tabId: string) => {
setCurrentTabRoute(tabId);
}}
tabs={[
{
title: t('Store.store_tabs.title'),
content: <BrowseTab setPluginCount={setPluginCount} />,
id: 'browse',
renderTabAddon: () => <span className={TabCount}>{pluginCount}</span>,
},
{
title: t('Store.store_tabs.about'),
content: <AboutTab />,
id: 'about',
},
]}
/>
</div>
</>
);
};
const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
const sortOptions = useMemo(
const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }> = ({ setPluginCount }) => {
const { t } = useTranslation();
const dropdownSortOptions = useMemo(
(): DropdownOption[] => [
{ data: 1, label: 'Alphabetical (A to Z)' },
{ data: 2, label: 'Alphabetical (Z to A)' },
// ascending and descending order are the wrong way around for the alphabetical sort
// this is because it was initially done incorrectly for i18n and 'fixing' it would
// make all the translations incorrect
{ data: [SortOptions.name, SortDirections.ascending], label: t('Store.store_tabs.alph_desc') },
{ data: [SortOptions.name, SortDirections.descending], label: t('Store.store_tabs.alph_asce') },
{ data: [SortOptions.date, SortDirections.ascending], label: t('Store.store_tabs.date_asce') },
{ data: [SortOptions.date, SortDirections.descending], label: t('Store.store_tabs.date_desc') },
{ data: [SortOptions.downloads, SortDirections.descending], label: t('Store.store_tabs.downloads_desc') },
{ data: [SortOptions.downloads, SortDirections.ascending], label: t('Store.store_tabs.downloads_asce') },
],
[],
);
// const filterOptions = useMemo((): DropdownOption[] => [{ data: 1, label: 'All' }], []);
const [selectedSort, setSort] = useState<number>(sortOptions[0].data);
const [selectedSort, setSort] = useState<[SortOptions, SortDirections]>(dropdownSortOptions[0].data);
// const [selectedFilter, setFilter] = useState<number>(filterOptions[0].data);
const [searchFieldValue, setSearchValue] = useState<string>('');
const [pluginList, setPluginList] = useState<StorePlugin[] | null>(null);
const [isTesting, setIsTesting] = useState<boolean>(false);
useEffect(() => {
(async () => {
const res = await getPluginList(selectedSort[0], selectedSort[1]);
logger.log('got data!', res);
setPluginList(res);
setPluginCount(res.length);
})();
}, [selectedSort]);
useEffect(() => {
(async () => {
const storeRes = await getStore();
logger.log(`store is ${storeRes}, isTesting is ${storeRes === Store.Testing}`);
setIsTesting(storeRes === Store.Testing);
})();
}, []);
return (
<>
@@ -105,11 +121,11 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
width: '47.5%',
}}
>
<span className="DialogLabel">Sort</span>
<span className="DialogLabel">{t("Store.store_sort.label")}</span>
<Dropdown
menuLabel="Sort"
rgOptions={sortOptions}
strDefaultLabel="Last Updated (Newest)"
menuLabel={t("Store.store_sort.label") as string}
rgOptions={dropdownSortOptions}
strDefaultLabel={t("Store.store_sort.label_def") as string}
selectedOption={selectedSort}
onChange={(e) => setSort(e.data)}
/>
@@ -122,11 +138,11 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
marginLeft: 'auto',
}}
>
<span className="DialogLabel">Filter</span>
<span className="DialogLabel">{t("Store.store_filter.label")}</span>
<Dropdown
menuLabel="Filter"
menuLabel={t("Store.store_filter.label")}
rgOptions={filterOptions}
strDefaultLabel="All"
strDefaultLabel={t("Store.store_filter.label_def")}
selectedOption={selectedFilter}
onChange={(e) => setFilter(e.data)}
/>
@@ -136,7 +152,7 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
<div style={{ justifyContent: 'center', display: 'flex' }}>
<Focusable style={{ display: 'flex', alignItems: 'center', width: '96%' }}>
<div style={{ width: '100%' }}>
<TextField label="Search" value={searchFieldValue} onChange={(e) => setSearchValue(e.target.value)} />
<TextField label={t("Store.store_search.label")} value={searchFieldValue} onChange={(e) => setSearchValue(e.target.value)} />
</div>
</Focusable>
</div>
@@ -151,11 +167,11 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
maxWidth: '100%',
}}
>
<span className="DialogLabel">Sort</span>
<span className="DialogLabel">{t('Store.store_sort.label')}</span>
<Dropdown
menuLabel="Sort"
rgOptions={sortOptions}
strDefaultLabel="Last Updated (Newest)"
menuLabel={t('Store.store_sort.label') as string}
rgOptions={dropdownSortOptions}
strDefaultLabel={t('Store.store_sort.label_def') as string}
selectedOption={selectedSort}
onChange={(e) => setSort(e.data)}
/>
@@ -165,33 +181,69 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
<div style={{ justifyContent: 'center', display: 'flex' }}>
<Focusable style={{ display: 'flex', alignItems: 'center', width: '96%' }}>
<div style={{ width: '100%' }}>
<TextField label="Search" value={searchFieldValue} onChange={(e) => setSearchValue(e.target.value)} />
<TextField
label={t('Store.store_search.label')}
value={searchFieldValue}
onChange={(e) => setSearchValue(e.target.value)}
/>
</div>
</Focusable>
</div>
{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) => {
return (
plugin.name.toLowerCase().includes(searchFieldValue.toLowerCase()) ||
plugin.description.toLowerCase().includes(searchFieldValue.toLowerCase()) ||
plugin.author.toLowerCase().includes(searchFieldValue.toLowerCase()) ||
plugin.tags.some((tag: string) => tag.toLowerCase().includes(searchFieldValue.toLowerCase()))
);
})
.sort((a, b) => {
if (selectedSort % 2 === 1) return a.name.localeCompare(b.name);
else return b.name.localeCompare(a.name);
})
.map((plugin: StorePlugin) => (
<PluginCard plugin={plugin} />
))}
{!pluginList ? (
<div style={{ height: '100%' }}>
<SteamSpinner />
</div>
) : (
pluginList
.filter((plugin: StorePlugin) => {
return (
plugin.name.toLowerCase().includes(searchFieldValue.toLowerCase()) ||
plugin.description.toLowerCase().includes(searchFieldValue.toLowerCase()) ||
plugin.author.toLowerCase().includes(searchFieldValue.toLowerCase()) ||
plugin.tags.some((tag: string) => tag.toLowerCase().includes(searchFieldValue.toLowerCase()))
);
})
.map((plugin: StorePlugin) => <PluginCard plugin={plugin} />)
)}
</div>
</>
);
};
const AboutTab: FC<{}> = () => {
const { t } = useTranslation();
return (
<div
style={{
@@ -216,24 +268,21 @@ const AboutTab: FC<{}> = () => {
/>
<span className="deckyStoreAboutHeader">Testing</span>
<span>
Please consider testing new plugins to help the Decky Loader team!{' '}
{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">Contributing</span>
<span>
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.
</span>
<span className="deckyStoreAboutHeader">Source Code</span>
<span>All plugin source code is available on SteamDeckHomebrew/decky-plugin-database repository on GitHub.</span>
<span className="deckyStoreAboutHeader">{t('Store.store_contrib.label')}</span>
<span>{t('Store.store_contrib.desc')}</span>
<span className="deckyStoreAboutHeader">{t('Store.store_source.label')}</span>
<span>{t('Store.store_source.desc')}</span>
</div>
);
};
+24 -65
View File
@@ -1,66 +1,50 @@
import {
Navigation,
ReactRouter,
Router,
fakeRenderComponent,
findInReactTree,
findInTree,
findModule,
findModuleChild,
gamepadDialogClasses,
gamepadSliderClasses,
playSectionClasses,
quickAccessControlsClasses,
quickAccessMenuClasses,
scrollClasses,
scrollPanelClasses,
sleep,
staticClasses,
updaterFieldClasses,
} from 'decky-frontend-lib';
import { sleep } from 'decky-frontend-lib';
import { FaReact } from 'react-icons/fa';
import Logger from './logger';
import { getSetting } from './utils/settings';
import TranslationHelper, { TranslationClass } from './utils/TranslationHelper';
const logger = new Logger('DeveloperMode');
let removeSettingsObserver: () => void = () => {};
export async function setShowValveInternal(show: boolean) {
let settingsMod: any;
while (!settingsMod) {
settingsMod = findModuleChild((m) => {
if (typeof m !== 'object') return undefined;
for (let prop in m) {
if (typeof m[prop]?.settings?.bIsValveEmail !== 'undefined') return m[prop];
}
});
if (!settingsMod) {
logger.debug('[ValveInternal] waiting for settingsMod');
await sleep(1000);
}
declare global {
interface Window {
settingsStore: any;
}
}
export async function setShowValveInternal(show: boolean) {
if (show) {
removeSettingsObserver = settingsMod[
Object.getOwnPropertySymbols(settingsMod).find((x) => x.toString() == 'Symbol(mobx administration)') as any
].observe((e: any) => {
const mobx =
window.settingsStore[
Object.getOwnPropertySymbols(window.settingsStore).find(
(x) => x.toString() == 'Symbol(mobx administration)',
) as any
];
removeSettingsObserver = (mobx.observe_ || mobx.observe).call(mobx, (e: any) => {
e.newValue.bIsValveEmail = true;
});
settingsMod.m_Settings.bIsValveEmail = true;
window.settingsStore.m_Settings.bIsValveEmail = true;
logger.log('Enabled Valve Internal menu');
} else {
removeSettingsObserver();
settingsMod.m_Settings.bIsValveEmail = false;
window.settingsStore.m_Settings.bIsValveEmail = false;
logger.log('Disabled Valve Internal menu');
}
}
export async function setShouldConnectToReactDevTools(enable: boolean) {
window.DeckyPluginLoader.toaster.toast({
title: (enable ? 'Enabling' : 'Disabling') + ' React DevTools',
body: 'Reloading in 5 seconds',
title: enable ? (
<TranslationHelper trans_class={TranslationClass.DEVELOPER} trans_text={'enabling'} />
) : (
<TranslationHelper trans_class={TranslationClass.DEVELOPER} trans_text={'disabling'} />
),
body: <TranslationHelper trans_class={TranslationClass.DEVELOPER} trans_text={'5secreload'} />,
icon: <FaReact />,
});
await sleep(5000);
@@ -77,29 +61,4 @@ export async function startup() {
if ((isRDTEnabled && !window.deckyHasConnectedRDT) || (!isRDTEnabled && window.deckyHasConnectedRDT))
setShouldConnectToReactDevTools(isRDTEnabled);
logger.log('Exposing decky-frontend-lib APIs as DFL');
window.DFL = {
findModuleChild,
findModule,
Navigation,
Router,
ReactRouter,
ReactUtils: {
fakeRenderComponent,
findInReactTree,
findInTree,
},
classes: {
scrollClasses,
staticClasses,
playSectionClasses,
scrollPanelClasses,
updaterFieldClasses,
gamepadDialogClasses,
gamepadSliderClasses,
quickAccessMenuClasses,
quickAccessControlsClasses,
},
};
}

Some files were not shown because too many files have changed in this diff Show More