Compare commits

...

210 Commits

Author SHA1 Message Date
AAGaming d71fb7935b fix plugin uninstall dialog issues 2024-06-27 01:43:01 -04:00
AAGaming 107b9abb3e fix testing our own PRs 2024-06-27 00:37:35 -04:00
AAGaming 0cfb41755a lint 2024-06-27 00:21:32 -04:00
AAGaming 2f8b5df007 bump @decky/ui 2024-06-27 00:20:18 -04:00
AAGaming 69e9f998e9 bump @decky/ui 2024-06-27 00:13:54 -04:00
AAGaming a8d55785cf fix cef debug toggle 2024-06-27 00:09:05 -04:00
AAGaming d067fe6361 add some errorboundaries 2024-06-27 00:09:00 -04:00
AAGaming c02a78ed6e fix plugins not loading on install 2024-06-27 00:08:45 -04:00
AAGaming c2f8cba4af trigger temporarilyDisableReporting in errorboundary 2024-06-27 00:08:32 -04:00
AAGaming c36f1985bd improve error checking logic 2024-06-27 00:08:13 -04:00
AAGaming fc52cf53ee don't report errors to valve if we've caused one in the last 30 seconds 2024-06-27 00:07:55 -04:00
AAGaming dcff7d146b move webpack check into index 2024-06-27 00:07:38 -04:00
AAGaming 13a38d82fd accidental lack of indent? 2024-06-27 00:07:25 -04:00
AAGaming b537968feb clean up legacy utilities for security 2024-06-27 00:07:06 -04:00
AAGaming 983fcf3014 RUN LINTER AGAINNNNNNN 2024-06-14 20:34:08 -04:00
AAGaming 61ad88db77 add doNotReportErrors 2024-06-14 20:20:15 -04:00
AAGaming 84577c8708 typescript please 2024-06-14 19:32:10 -04:00
Party Wumpus 6bd3951d31 add docstring for emit 2024-06-14 22:45:02 +00:00
AAGaming 48e79f803a update @decky/ui and @decky/api 2024-06-13 18:27:59 -04:00
AAGaming 7f421f5bd4 remove unused dep 2024-06-13 17:52:19 -04:00
AAGaming d6e71b23ef fixup prettier 2024-06-13 17:49:40 -04:00
AAGaming 54aecee64e hook up plugin events properly 2024-06-13 17:47:53 -04:00
AAGaming 822b6bcaaa fix lockfile 2024-06-13 17:08:38 -04:00
AAGaming 259aabf82f update dependencies 2024-06-13 17:00:50 -04:00
AAGaming 1de8c5915b the rest of f11e34a (from 3a83fa8) 2024-06-13 15:26:23 -04:00
WerWolvTranslationBot 4f92276147 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-13 15:14:24 -04:00
Party Wumpus f11e34ab25 Typing fix
linters are the light of my life
2024-06-13 15:13:58 -04:00
Sims 23944f7cbf Add new user agent (#610) 2024-06-13 15:12:45 -04:00
AAGaming e6b1950bcb fix for latest beta (backport) 2024-06-13 15:12:24 -04:00
AAGaming 0c6c7b1b06 missed a log 2024-05-27 17:22:08 -04:00
AAGaming 9c8db576f5 error boundary now properly reports steam errors 2024-05-27 17:21:27 -04:00
AAGaming a84a13c76d Custom error handler and some misc fixes 2024-05-25 19:14:54 -04:00
AAGaming 96cc72f2ca chore: fetch -> fetchNoCors 2024-05-24 23:26:39 -04:00
Party Wumpus 372771a228 plugin install progress (#614)
* Frontend progress bars

* Backend bit

* closure is stale i think so no closure for you

* Fix formatting of the progress svgs

* Reset progress bar when new plugin starts downloading
2024-05-13 14:42:55 +01:00
PartyWumpus 675b6d5ef8 fix locale mistake
from 28aca03f0d
2024-05-12 23:56:11 +01:00
Party Wumpus 97b62ac72b [needs websockets] Add development flake file (#620)
* Add development flake file

* use regular nixpkgs import

* Make vscode work and add .envrc

* add .direnv to the .gitignore file
2024-05-12 21:02:47 +01:00
AAGaming 0b1c069448 port to @decky/ui
TODO: update package.json to match once @decky/ui is on NPM
2024-05-12 15:47:08 -04:00
PartyWumpus 43b940e216 remove some type: ignore and make some specific 2024-05-05 10:17:03 +01:00
PartyWumpus 10e13571e5 version does have types, false alarm 2024-05-05 08:52:23 +01:00
AAGaming 14ea7b964f implement fetch and external resource request apis 2024-05-04 22:39:30 -04:00
AAGaming 2a22f000c1 add deckdebug.sh utility 2024-05-04 22:38:40 -04:00
WerWolvTranslationBot 63f90d884e 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-04-22 13:29:14 -04:00
Wayne Heaney a1a29616e5 Add Plugin.uninstall callback support (#555)
* Add Plugin.uninstall callback support

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

* Remove empty deck.sh
2024-04-22 13:29:13 -04:00
AAGaming 6b06bae250 fix: support new minified class names
bumps decky-frontend-lib to 3.25.0
can't stop us THAT easily :P
2024-04-22 13:18:33 -04:00
TrainDoctor 9a0a52f9e3 Update bug_report.yml 2024-04-22 13:17:30 -04:00
TrainDoctor 6f7dd26d56 Update bug_report.yml 2024-04-22 13:17:30 -04:00
WerWolvTranslationBot 28aca03f0d 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-04-22 13:17:28 -04:00
Party Wumpus f9ff518e6d attempt to add plugin events to the plugin frontend api.
unable to test right now though
2024-04-09 15:54:48 +01:00
Party Wumpus de9d2144a6 name RouteNotFoundError 2024-03-01 23:08:32 +00:00
Party Wumpus 11b743a792 Add loading bar to testing page and make downloading more robust 2024-03-01 23:07:51 +00:00
Party Wumpus 637e3c566e what if the error message was better 2024-02-22 17:37:20 +00:00
Party Wumpus 89a4a69f6d make frontend -> backend errors actually work 2024-02-22 16:38:50 +00:00
AAGaming a449181802 hook up the backend api 2024-02-21 17:39:13 -05:00
AAGaming 4696583680 fix unused imports 2024-02-21 01:42:39 -05:00
AAGaming 6d2e9365c0 more major websocket progress 2024-02-21 01:08:25 -05:00
AAGaming 61cf80f8a2 update service to reduce startup time 2024-02-20 18:51:22 -05:00
Party Wumpus 39e752e4e2 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-20 21:42:37 +00:00
Party Wumpus 992e2e2ad3 fix finding qam root node for feb 14th beta 2024-02-20 21:40:01 +00:00
Andrew Moore c2ebc78836 [Feature] Freeze updates for devs (#582) 2024-02-20 21:39:43 +00:00
eXhumer dc1697d049 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-20 21:34:16 +00:00
Party Wumpus 35f6f041c1 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-20 21:33:41 +00:00
AAGaming 7e3f9edacf clean up updater logic 2024-02-19 23:42:24 -05:00
AAGaming 22b732bab4 clean up deranged react triple-import in Updater.tsx 2024-02-19 23:34:01 -05:00
Party Wumpus 61b984bfa1 what if the error handling worked for async event listeners
the anonymous async function is made so the event handlers can each be started in parallel, just in case there's a long running function
2024-02-16 13:35:08 +00:00
Party Wumpus 867ce63f7b Add event handler 2024-02-15 22:28:36 +00:00
AAGaming ee6122b97d restart ui by killing webhelper
this cleanly reloads the ui, prevents memory leaks, and won't break the toasts
2024-02-14 17:49:52 -05:00
AAGaming 091428f683 small loader refactoring 2024-02-14 17:49:27 -05:00
AAGaming 9db3f3f20e bump dfl 2024-02-14 17:49:10 -05:00
AAGaming 37d70c31ff stop using homebrew/dev/plugins
it keeps causing me confusion between plugin versions in development
2024-02-14 17:49:00 -05:00
AAGaming ee1627a3a1 fix some broken types 2024-02-14 17:06:41 -05:00
Jozen Blue Martinez ecd8ef5998 fix(filepicker_ls): use case insensitive matching for file exts (#585) 2024-02-14 16:44:49 -05:00
Party Wumpus 8987076c5f 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-14 16:43:34 -05:00
Party Wumpus ec41c61219 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-14 16:43:30 -05:00
Party Wumpus 21c7742f9a 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-14 16:42:14 -05:00
AAGaming e8add28797 fix: use findInReactTree to find correct errorboundary for toaster
fixes toaster error on latest beta
2024-02-14 16:42:14 -05:00
AAGaming f5e902f741 fix vscode import memes 2024-02-14 16:40:48 -05:00
Beebles 063961d36a 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-21 16:49:52 -05:00
dependabot[bot] 96ce599e34 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-21 16:49:52 -05:00
AAGaming c5ea95a787 finalize api for plugin events in backend
just need frontend impl now
2023-12-31 20:29:19 -05:00
AAGaming db96121304 -post -> -dev 2023-12-31 19:13:19 -05:00
AAGaming 40c7c1b515 port updater to ws, also small refactoring 2023-12-30 21:32:23 -05:00
AAGaming 70104065e2 chore: get rid of useless old tabs hook 2023-12-30 18:06:07 -05:00
AAGaming 11a88186ba fix ci issue 2023-12-30 00:50:12 -05:00
AAGaming 6522ebf0ca Implement legacy & modern plugin method calls over WS
This version builds fine and runs all of the 14 plugins I have installed perfectly, so we're really close to having this done.
2023-12-30 00:46:59 -05:00
WerWolvTranslationBot 6042ca56b8 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-29 18:40:53 -05:00
Jan 5190765ce1 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-29 18:40:53 -05:00
AAGaming 3a38cf8074 fix(developer): add back valve internal on beta
look i was tired when writing yesterday's fix okay
2023-12-29 18:40:53 -05:00
AAGaming 4f40b97f53 fix: idiotic formatting error i should have noticed 2023-12-29 18:40:52 -05:00
AAGaming 5fd5b2f08c fix: Adjust tabs and toaster hooks to work on react 18, also half-fix Valve Internal 2023-12-29 18:40:52 -05:00
Party Wumpus 87d7e15951 bump dfl 2023-12-29 18:40:52 -05:00
Jan 98e2d1232c replace chmod implementation with os.chmod (#541) 2023-12-29 18:40:52 -05:00
Jan 6cb545c78d Check if Linux service is running before trying to start or stop it (#540)
this prevents needless prompts opening up
2023-12-29 18:40:52 -05:00
K900 41c62c3a34 refactor(backend): get version from package metadata 2023-12-29 18:34:19 -05:00
K900 31a6202da9 refactor(ci): reorganize CI to build tagged artifacts 2023-12-29 18:34:19 -05:00
K900 3565c3c9b4 feat(backend): log our version on startup 2023-12-29 18:34:19 -05:00
K900 e2ade0d731 feat(backend): enable poetry-dynamic-versioning 2023-12-29 18:34:19 -05:00
AAGaming 06690890fb fix tasks 2023-12-29 18:33:36 -05:00
marios8543 8b0d1753ef fix a couple types 2023-11-14 00:34:48 +02:00
AAGaming 70532c8d0b change default event DataType 2023-11-14 00:04:56 +02:00
AAGaming 5e1e035bc2 more progress on websockets 2023-11-14 00:04:56 +02:00
AAGaming 34d1a34b10 Migrate most of frontend callServerMethod usage over to websocket 2023-11-14 00:04:56 +02:00
AAGaming cfb6fe69e3 remove test method 2023-11-14 00:04:56 +02:00
AAGaming 1921e7ec56 JS -> Python WS now functional 2023-11-14 00:04:56 +02:00
AAGaming 05b41b3410 more progress on WS router 2023-11-14 00:03:47 +02:00
AAGaming 18d89e76fd more work on websockets 2023-11-14 00:03:47 +02:00
AAGaming 4a9b45b98e initial WSRouter implementation 2023-11-14 00:03:47 +02:00
AAGaming 8f299a90dc init 2023-11-14 00:02:14 +02:00
K900 5a633fdd82 Packaging rework (#531)
* fix: get rid of title view jank on latest beta

* Count the number of installs for each plugin (#557)

* Bump aiohttp from 3.8.4 to 3.8.5 in /backend (#558)

* fix: include Decky version in request for index.js

This avoids the If-Modified-Since logic in aiohttp and ensures Steam doesn't cache old JS,
even if the timestamps are normalized.

* fix: clean up shellcheck warnings in act runner script

* fix: gitignore settings/

* fix: ensure state directories exist when running without the installer

* feat: determine root directory correctly when running from in-tree

* fix: fix typo in CI script

* refactor: build a proper Python package with poetry

* refactor: move decky_plugin under the poetry structure

There's no need to special case it anymore, just treat it like any other Python module.

* sandboxed_plugin: better fix, attempt 2

---------

Co-authored-by: AAGaming <aagaming@riseup.net>
Co-authored-by: Party Wumpus <48649272+PartyWumpus@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-13 23:40:37 +02:00
marios8543 8ce4a7679e Remove legacy data from pyinstaller 2023-11-01 00:04:14 +02:00
marios8543 a0920cf0d0 fix sys.modules import 2023-10-31 23:38:00 +02:00
marios8543 7565a66d90 fix emit_message mechanism 2023-10-31 23:18:31 +02:00
marios8543 e4b1efc44d run method calls asynchronously 2023-10-31 23:18:31 +02:00
marios8543 85f4604bfd fix static/lang file fetch and method call 2023-10-31 23:18:31 +02:00
marios8543 f30309d153 fix start/stop methods 2023-10-31 23:18:31 +02:00
marios8543 f48d774554 fix customtypes import 2023-10-31 23:18:31 +02:00
marios8543 268311c482 Add message emit mechanism 2023-10-31 23:18:31 +02:00
marios8543 ed0f851d4d Add act to .gitignore 2023-10-31 23:18:31 +02:00
Party Wumpus 2f4e79a40e fix typo
this is what i get for commiting to main 😔
2023-10-31 23:18:31 +02:00
Party Wumpus f508d1dfce fix logical error when no store was set 2023-10-31 23:18:30 +02:00
marios8543 8dc6f19d2b Run response_listener task 2023-10-31 23:18:23 +02:00
marios8543 321242b0d9 Experimental support for async method calls 2023-10-31 23:18:23 +02:00
marios8543 949c5e73c4 Add localplatform stuff to its own package 2023-10-31 23:17:57 +02:00
marios8543 da9217ac4a Fix type error on store.tsx 2023-10-31 23:17:57 +02:00
marios8543 39f64ca666 Drop support for legacy plugins 2023-10-31 23:17:49 +02:00
AAGaming 2391af09eb fix paths 2023-10-31 23:17:23 +02:00
AAGaming 0b01df7339 oops 2023-10-31 23:17:03 +02:00
AAGaming c69ca5e821 fix broken import 2023-10-31 23:17:03 +02:00
AAGaming b155734dcf fix ci (hopefully, because act wont work) 2023-10-31 23:17:03 +02:00
WerWolv 96ae502202 Moved locales folder and requirements.txt 2023-10-31 23:16:24 +02:00
WerWolv b373c3114b Moved main.py 2023-10-31 23:15:46 +02:00
WerWolv af6784272c Moved backend entirely into the backend folder 2023-10-31 23:15:25 +02:00
AAGaming df08f611b9 run lint and typecheck on PRs 2023-10-31 23:13:19 +02:00
AAGaming de1b24b8bc remove quotes on some types 2023-10-31 23:13:19 +02:00
AAGaming fae09596a7 make ci title consistent 2023-10-31 23:13:19 +02:00
AAGaming e8f5ce8d5a move type checking to other workflow, fix TS errors, add TSC checking 2023-10-31 23:13:19 +02:00
AAGaming 81726acd51 add pyright ci 2023-10-31 23:13:00 +02:00
AAGaming 5582457c58 move to module imports 2023-10-31 23:12:43 +02:00
marios8543 df755063c2 type hints on main,plugin,updater,utilites.localsocket 2023-10-31 23:11:37 +02:00
AAGaming 1949e9fcf1 begin adding static types to backend code 2023-10-31 23:11:08 +02: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
marios8543 28ca7b5c90 fix emit_message mechanism 2023-10-18 21:04:51 +03:00
marios8543 feabb582b2 run method calls asynchronously 2023-10-18 19:38:07 +03:00
marios8543 47e9708a20 fix static/lang file fetch and method call 2023-10-18 17:21:57 +03:00
marios8543 934b1b35ad fix start/stop methods 2023-10-18 15:34:25 +03:00
marios8543 88250b3e20 fix customtypes import 2023-10-18 14:59:05 +03:00
marios8543 d9ba637cd9 Add message emit mechanism 2023-10-18 14:45:36 +03:00
marios8543 dcee5ca4e4 Add act to .gitignore 2023-10-18 02:39:52 +03:00
marios8543 dffa82a555 fix uninstall bug 2023-10-18 02:39:15 +03:00
Party Wumpus 63f8cff341 fix typo
this is what i get for commiting to main 😔
2023-10-18 02:38:58 +03:00
Party Wumpus 836bcfbc03 fix logical error when no store was set 2023-10-18 02:37:48 +03:00
marios8543 64867369f9 Fix decky_plugin on windows CI 2023-10-18 02:36:55 +03:00
marios8543 315b2f9cda Run response_listener task 2023-10-18 00:04:14 +03:00
marios8543 07c8ddc0b2 Experimental support for async method calls 2023-10-17 23:52:18 +03:00
marios8543 36c145bb3a Add localplatform stuff to its own package 2023-10-17 23:51:57 +03:00
marios8543 19793d71e6 Fix type error on store.tsx 2023-10-17 17:11:50 +03:00
marios8543 796b8b49f4 Drop support for legacy plugins 2023-10-17 17:08:23 +03:00
marios8543 1b9d674a81 fix decky_plugin path in pyinstaller 2023-10-17 17:08:23 +03:00
AAGaming 949244e8e6 fix paths 2023-10-17 17:08:23 +03:00
AAGaming b7d4d57bc2 oops 2023-10-17 17:08:23 +03:00
AAGaming 458fa6a66c fix broken import 2023-10-17 17:08:23 +03:00
AAGaming 06fccb792f fix ci (hopefully, because act wont work) 2023-10-17 17:08:23 +03:00
AAGaming 6867feba85 speed up stupid make 2023-10-17 17:08:23 +03:00
WerWolv 45353c87c2 Moved locales folder and requirements.txt 2023-10-17 17:08:23 +03:00
WerWolv 37b8c5264f Moved main.py 2023-10-17 17:08:23 +03:00
WerWolv 5937971014 Moved backend entirely into the backend folder 2023-10-17 17:08:23 +03:00
AAGaming a351c02ac1 with, not env 2023-10-17 17:08:23 +03:00
AAGaming fc086db5e6 run lint and typecheck on PRs 2023-10-17 17:08:23 +03:00
AAGaming ca1332334d remove quotes on some types 2023-10-17 17:08:23 +03:00
AAGaming aebca54eac make ci title consistent 2023-10-17 17:08:23 +03:00
AAGaming 8fe8062950 move type checking to other workflow, fix TS errors, add TSC checking 2023-10-17 17:08:23 +03:00
AAGaming 11d731cf35 add pyright ci 2023-10-17 17:08:23 +03:00
AAGaming bf83eabe6b move to module imports 2023-10-17 17:08:23 +03:00
marios8543 a7c358844c type hints on main,plugin,updater,utilites.localsocket 2023-10-17 17:08:23 +03:00
AAGaming e2d708a6af begin adding static types to backend code 2023-10-17 17:08:23 +03:00
AAGaming 1e1e82ed71 remove useless main.py imports 2023-10-17 17:08:23 +03:00
Party Wumpus f53a3f383d fix typo
this is what i get for commiting to main 😔
2023-10-17 13:52:11 +01:00
Party Wumpus 407e647993 fix logical error when no store was set 2023-10-17 13:44:44 +01:00
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
131 changed files with 8942 additions and 5132 deletions
+1
View File
@@ -0,0 +1 @@
use flake
+13 -3
View File
@@ -12,7 +12,7 @@ body:
- label: I have searched existing issues
- label: This issue is not a duplicate of an existing one
- label: I have checked the [common issues section in the readme file](https://github.com/SteamDeckHomebrew/decky-loader#-common-issues)
- label: I have attached logs to this bug report (failure to include logs will mean your issue will not be responded too).
- label: I have attached logs to this bug report (failure to include logs will mean your issue may not be responded to).
- type: textarea
attributes:
@@ -42,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
@@ -67,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: 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.
+67
View File
@@ -0,0 +1,67 @@
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 Poetry
uses: snok/install-poetry@v1
with:
virtualenvs-create: false
- name: Install Python dependencies ⬇️
working-directory: ./backend
run: |
C:\Users\runneradmin\.local\bin\poetry self add "poetry-dynamic-versioning[plugin]"
C:\Users\runneradmin\.local\bin\poetry install --no-interaction
- 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 🛠️
working-directory: ./backend
run: |
C:\Users\runneradmin\.local\bin\poetry dynamic-versioning
C:\Users\runneradmin\.local\bin\poetry run pyinstaller pyinstaller.spec
- name: Build Python Backend (noconsole) 🛠️
working-directory: ./backend
run: $env:DECKY_NOCONSOLE = 1; C:\Users\runneradmin\.local\bin\poetry run pyinstaller pyinstaller.spec
- name: Upload package artifact ⬆️
uses: actions/upload-artifact@v3
with:
name: PluginLoader Win
path: |
./backend/dist/PluginLoader.exe
./backend/dist/PluginLoader_noconsole.exe
+39 -247
View File
@@ -3,60 +3,58 @@ name: Builder
on:
push:
pull_request:
workflow_call:
# schedule:
# - cron: '0 13 * * *' # run at 1 PM UTC
workflow_dispatch:
inputs:
release:
type: choice
description: Release the asset
default: 'none'
options:
- none
- prerelease
- release
bump:
type: choice
description: Semver to bump
default: 'none'
options:
- none
- patch
- minor
- major
permissions:
contents: write
jobs:
build:
name: Build PluginLoader
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- name: Print input
run : |
echo "release: ${{ github.event.inputs.release }}\n"
echo "bump: ${{ github.event.inputs.bump }}\n"
- name: Checkout 🧰
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up NodeJS 18 💎
uses: actions/setup-node@v3
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 Poetry
uses: snok/install-poetry@v1
with:
virtualenvs-create: false
- 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
poetry self add "poetry-dynamic-versioning[plugin]"
poetry install --no-interaction
- name: Install JS dependencies ⬇️
working-directory: ./frontend
@@ -69,226 +67,20 @@ jobs:
run: pnpm run build
- name: Build Python Backend 🛠️
run: pyinstaller --noconfirm --onefile --name "PluginLoader" --add-data ./backend/static:/static --add-data ./backend/locales:/locales --add-data ./backend/legacy:/legacy --add-data ./plugin:/plugin ./backend/*.py
working-directory: ./backend
run: |
poetry dynamic-versioning
pyinstaller pyinstaller.spec
- name: Upload package artifact ⬆️
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v3
with:
name: PluginLoader
path: ./dist/PluginLoader
path: ./backend/dist/PluginLoader
- name: Download package artifact locally
if: ${{ env.ACT }}
uses: actions/upload-artifact@v3
with:
path: ./dist/PluginLoader
build-win:
name: Build PluginLoader for Win
runs-on: windows-2022
steps:
- name: Checkout 🧰
uses: actions/checkout@v3
- name: Set up NodeJS 18 💎
uses: actions/setup-node@v3
with:
node-version: 18
- name: Set up Python 3.10.2 🐍
uses: actions/setup-python@v4
with:
python-version: "3.10.2"
- name: Install Python dependencies ⬇️
run: |
python -m pip install --upgrade pip
pip install pyinstaller==5.5
pip install -r requirements.txt
- name: Install JS dependencies ⬇️
working-directory: ./frontend
run: |
npm i -g pnpm
pnpm i --frozen-lockfile
- name: Build JS Frontend 🛠️
working-directory: ./frontend
run: pnpm run build
- name: Build Python Backend 🛠️
run: pyinstaller --noconfirm --onefile --name "PluginLoader" --add-data "./backend/static;/static" --add-data "./backend/locales;/locales" --add-data "./backend/legacy;/legacy" --add-data "./plugin;/plugin" ./backend/main.py
- name: Upload package artifact ⬆️
uses: actions/upload-artifact@v3
with:
name: PluginLoader Win
path: ./dist/PluginLoader.exe
release:
name: Release stable version of the package
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release == 'release' }}
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout 🧰
uses: actions/checkout@v3
- name: Install semver-tool asdf
uses: asdf-vm/actions/install@v1
with:
tool_versions: |
semver 3.3.0
- name: Fetch package artifact ⬇️
uses: actions/download-artifact@v3
if: ${{ !env.ACT }}
with:
name: PluginLoader
path: dist
- name: Get latest release
uses: rez0n/actions-github-release@main
id: latest_release
env:
token: ${{ secrets.GITHUB_TOKEN }}
repository: "SteamDeckHomebrew/decky-loader"
type: "nodraft"
- name: Prepare tag ⚙️
id: ready_tag
run: |
export VERSION=${{ steps.latest_release.outputs.release }}
echo "VERS: $VERSION"
OUT="notsemver"
if [[ "$VERSION" =~ "-pre" ]]; then
printf "is prerelease, bumping to release\n"
OUT=$(semver bump release "$VERSION")
printf "OUT: ${OUT}\n"\
printf "bumping by selected type.\n"
if [[ "${{github.event.inputs.bump}}" != "none" ]]; then
OUT=$(semver bump ${{github.event.inputs.bump}} "$OUT")
printf "OUT: ${OUT}\n"
else
printf "no type selected, not bumping for release.\n"
fi
elif [[ ! "$VERSION" =~ "-pre" ]]; then
printf "previous tag is a release, bumping by selected type.\n"
if [[ "${{github.event.inputs.bump}}" != "none" ]]; then
OUT=$(semver bump ${{github.event.inputs.bump}} "$VERSION")
printf "OUT: ${OUT}\n"
else
printf "previous tag is a release, but no bump selected. Defaulting to a patch bump.\n"
OUT=$(semver bump patch "$VERSION")
printf "OUT: ${OUT}\n"
fi
fi
echo "vOUT: v$OUT"
echo tag_name=v$OUT >> $GITHUB_OUTPUT
- name: Push tag 📤
uses: rickstaa/action-create-tag@v1.3.2
if: ${{ steps.ready_tag.outputs.tag_name && github.event_name == 'workflow_dispatch' && !env.ACT }}
with:
tag: ${{ steps.ready_tag.outputs.tag_name }}
message: Pre-release ${{ steps.ready_tag.outputs.tag_name }}
- name: Release 📦
uses: softprops/action-gh-release@v1
if: ${{ github.event_name == 'workflow_dispatch' && !env.ACT }}
with:
name: Release ${{ steps.ready_tag.outputs.tag_name }}
tag_name: ${{ steps.ready_tag.outputs.tag_name }}
files: ./dist/PluginLoader
prerelease: false
generate_release_notes: true
prerelease:
name: Release the pre-release version of the package
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release == 'prerelease' }}
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout 🧰
uses: actions/checkout@v3
- name: Install semver-tool asdf
uses: asdf-vm/actions/install@v1
with:
tool_versions: |
semver 3.3.0
- name: Fetch package artifact ⬇️
uses: actions/download-artifact@v3
if: ${{ !env.ACT }}
with:
name: PluginLoader
path: dist
- name: Get latest release
uses: rez0n/actions-github-release@main
id: latest_release
env:
token: ${{ secrets.GITHUB_TOKEN }}
repository: "SteamDeckHomebrew/decky-loader"
type: "nodraft"
- name: Prepare tag ⚙️
id: ready_tag
run: |
export VERSION=${{ steps.latest_release.outputs.release }}
echo "VERS: $VERSION"
OUT=""
if [[ ! "$VERSION" =~ "-pre" ]]; then
printf "pre-release from release, bumping by selected type and prerel\n"
if [[ ! ${{ github.event.inputs.bump }} == "none" ]]; then
OUT=$(semver bump ${{github.event.inputs.bump}} "$VERSION")
printf "OUT: ${OUT}\n"
else
printf "type not selected, defaulting to patch\n"
OUT=$(semver bump patch "$VERSION")
printf "OUT: ${OUT}\n"
fi
OUT="$OUT-pre"
OUT=$(semver bump prerel "$OUT")
printf "OUT: ${OUT}\n"
elif [[ "$VERSION" =~ "-pre" ]]; then
printf "pre-release to pre-release, bumping by selected type and or prerel version\n"
if [[ ! ${{ github.event.inputs.bump }} == "none" ]]; then
OUT=$(semver bump ${{github.event.inputs.bump}} "$VERSION")
printf "OUT: ${OUT}\n"
OUT="$OUT-pre"
printf "OUT: ${OUT}\n"
printf "bumping prerel\n"
OUT=$(semver bump prerel "$OUT")
printf "OUT: ${OUT}\n"
else
printf "type not selected, defaulting to new pre-release only\n"
printf "bumping prerel\n"
OUT=$(semver bump prerel "$VERSION")
printf "OUT: ${OUT}\n"
fi
fi
printf "vOUT: v${OUT}\n"
echo tag_name=v$OUT >> $GITHUB_OUTPUT
- name: Push tag 📤
uses: rickstaa/action-create-tag@v1.3.2
if: ${{ steps.ready_tag.outputs.tag_name && github.event_name == 'workflow_dispatch' && !env.ACT }}
with:
tag: ${{ steps.ready_tag.outputs.tag_name }}
message: Pre-release ${{ steps.ready_tag.outputs.tag_name }}
- name: Release 📦
uses: softprops/action-gh-release@v1
if: ${{ github.event_name == 'workflow_dispatch' && !env.ACT }}
with:
name: Prerelease ${{ steps.ready_tag.outputs.tag_name }}
tag_name: ${{ steps.ready_tag.outputs.tag_name }}
files: ./dist/PluginLoader
prerelease: true
generate_release_notes: true
path: ./backend/dist/PluginLoader
+8 -8
View File
@@ -14,26 +14,26 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v35.6.3
uses: tj-actions/changed-files@v41.0.0
with:
separator: ","
files: |
plugin/*
backend/decky_loader/plugin/imports/decky.pyi
- name: Is stub changed
id: changed-stub
run: |
STUB_CHANGED="false"
PATHS=(plugin plugin/decky_plugin.pyi)
PATHS=(backend backend/decky_loader/plugin/imports/decky.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"
if [[ "$FILES" == *"backend/decky_loader/plugin/imports/decky.pyi"* ]]; then
STUB_CHANGED="true"
echo "Stub has changed, pushing updated stub"
else
echo "Stub has not changed, exiting."
@@ -43,12 +43,12 @@ jobs:
echo "has_changed=$STUB_CHANGED" >> $GITHUB_OUTPUT
- name: Push updated stub
if: steps.changed-stub.outputs.has_changed == true
if: github.ref == 'refs/heads/main' && 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'
source_file: 'backend/decky_loader/plugin/imports/decky.pyi'
destination_repo: 'SteamDeckHomebrew/decky-plugin-template'
user_email: '11465594+TrainDoctor@users.noreply.github.com'
user_name: 'TrainDoctor'
+11 -5
View File
@@ -2,6 +2,7 @@ name: Lint
on:
push:
pull_request:
jobs:
lint:
@@ -9,9 +10,14 @@ jobs:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2 # Check out the repository first.
- name: Run prettier (JavaScript & TypeScript)
- uses: actions/checkout@v3 # Check out the repository first.
- name: Install TypeScript dependencies
working-directory: frontend
run: |
pushd frontend
npm install
npm run lint
npm i -g pnpm
pnpm i --frozen-lockfile
- name: Run prettier (TypeScript)
working-directory: frontend
run: pnpm run lint
+156
View File
@@ -0,0 +1,156 @@
name: Release
on:
workflow_dispatch:
inputs:
release:
type: choice
description: Release the asset
default: 'none'
options:
- none
- prerelease
- release
bump:
type: choice
description: Semver to bump
default: 'none'
options:
- none
- patch
- minor
- major
permissions:
contents: write
jobs:
create_tag:
name: Tag a new version of the package
runs-on: ubuntu-latest
outputs:
tag_name: ${{ steps.ready_tag.outputs.tag_name }}
steps:
- name: Checkout 🧰
uses: actions/checkout@v3
- name: Install semver-tool asdf
uses: asdf-vm/actions/install@v1
with:
tool_versions: |
semver 3.3.0
- name: Get latest release
uses: rez0n/actions-github-release@main
id: latest_release
with:
token: ${{ secrets.GITHUB_TOKEN }}
repository: "SteamDeckHomebrew/decky-loader"
type: "nodraft"
- name: Prepare tag ⚙️
id: ready_tag
run: |
export VERSION=${{ steps.latest_release.outputs.release }}
echo "VERS: $VERSION"
if [[ ${{github.event.inputs.release}} == "release" ]]; then
OUT="notsemver"
if [[ "$VERSION" =~ "-pre" ]]; then
printf "is prerelease, bumping to release\n"
OUT=$(semver bump release "$VERSION")
printf "OUT: ${OUT}\n"\
printf "bumping by selected type.\n"
if [[ "${{github.event.inputs.bump}}" != "none" ]]; then
OUT=$(semver bump ${{github.event.inputs.bump}} "$OUT")
printf "OUT: ${OUT}\n"
else
printf "no type selected, not bumping for release.\n"
fi
elif [[ ! "$VERSION" =~ "-pre" ]]; then
printf "previous tag is a release, bumping by selected type.\n"
if [[ "${{github.event.inputs.bump}}" != "none" ]]; then
OUT=$(semver bump ${{github.event.inputs.bump}} "$VERSION")
printf "OUT: ${OUT}\n"
else
printf "previous tag is a release, but no bump selected. Defaulting to a patch bump.\n"
OUT=$(semver bump patch "$VERSION")
printf "OUT: ${OUT}\n"
fi
fi
else
OUT=""
if [[ ! "$VERSION" =~ "-pre" ]]; then
printf "pre-release from release, bumping by selected type and prerel\n"
if [[ ! ${{ github.event.inputs.bump }} == "none" ]]; then
OUT=$(semver bump ${{github.event.inputs.bump}} "$VERSION")
printf "OUT: ${OUT}\n"
else
printf "type not selected, defaulting to patch\n"
OUT=$(semver bump patch "$VERSION")
printf "OUT: ${OUT}\n"
fi
OUT="$OUT-pre"
OUT=$(semver bump prerel "$OUT")
printf "OUT: ${OUT}\n"
elif [[ "$VERSION" =~ "-pre" ]]; then
printf "pre-release to pre-release, bumping by selected type and or prerel version\n"
if [[ ! ${{ github.event.inputs.bump }} == "none" ]]; then
OUT=$(semver bump ${{github.event.inputs.bump}} "$VERSION")
printf "OUT: ${OUT}\n"
OUT="$OUT-pre"
printf "OUT: ${OUT}\n"
printf "bumping prerel\n"
OUT=$(semver bump prerel "$OUT")
printf "OUT: ${OUT}\n"
else
printf "type not selected, defaulting to new pre-release only\n"
printf "bumping prerel\n"
OUT=$(semver bump prerel "$VERSION")
printf "OUT: ${OUT}\n"
fi
fi
fi
echo "vOUT: v${OUT}"
echo tag_name=v$OUT >> $GITHUB_OUTPUT
- name: Push tag 📤
uses: rickstaa/action-create-tag@v1.3.2
if: ${{ steps.ready_tag.outputs.tag_name && !env.ACT }}
with:
tag: ${{ steps.ready_tag.outputs.tag_name }}
message: Pre-release ${{ steps.ready_tag.outputs.tag_name }}
build:
name: Build tagged artifact
uses: ./.github/workflows/build.yml
needs: [create_tag]
release:
name: Release tagged artifact
runs-on: ubuntu-latest
needs: [create_tag, build]
steps:
- name: Fetch package artifact ⬇️
uses: actions/download-artifact@v3
with:
name: PluginLoader
- name: Pre-release 📦
if: github.event.inputs.release == 'prerelease'
uses: softprops/action-gh-release@v1
with:
name: Prerelease ${{ needs.create_tag.outputs.tag_name }}
tag_name: ${{ needs.create_tag.outputs.tag_name }}
files: ./PluginLoader
prerelease: true
generate_release_notes: true
- name: Release 📦
if: github.event.inputs.release == 'release'
uses: softprops/action-gh-release@v1
with:
name: Release ${{ needs.create_tag.outputs.tag_name }}
tag_name: ${{ needs.create_tag.outputs.tag_name }}
files: ./PluginLoader
prerelease: false
generate_release_notes: true
+44
View File
@@ -0,0 +1,44 @@
name: Type Check
on:
push:
pull_request:
jobs:
typecheck:
name: Run type checkers
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3 # Check out the repository first.
- name: Set up Python 3.10.6 🐍
uses: actions/setup-python@v4
with:
python-version: "3.10.6"
- name: Install Poetry
uses: snok/install-poetry@v1
with:
virtualenvs-create: false
- name: Install Python dependencies ⬇️
working-directory: backend
run: poetry install --no-interaction
- 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 run typecheck
+6 -3
View File
@@ -29,7 +29,7 @@ MANIFEST
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
backend/dist/
# Installer logs
pip-log.txt
@@ -126,6 +126,7 @@ venv/
ENV/
env.bak/
venv.bak/
.direnv/
# Spyder project settings
.spyderproject
@@ -159,6 +160,8 @@ backend/static
.vscode/settings.json
# plugins folder for local launches
plugins/*
/plugins/*
act/.directory
act/artifacts/*
act/artifacts/*
bin/act
/settings/
+1
View File
@@ -1,6 +1,7 @@
{
"deckip" : "0.0.0.0",
"deckport" : "22",
"deckuser" : "deck",
"deckpass" : "ssap",
"deckkey" : "-i ${env:HOME}/.ssh/id_rsa",
"deckdir" : "/home/deck"
+17 -5
View File
@@ -37,8 +37,11 @@
"label": "dependencies",
"type": "shell",
"group": "none",
"dependsOn": [
"deploy"
],
"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": "ssh ${config:deckuser}@${config:deckip} -p ${config:deckport} ${config:deckkey} 'python -m ensurepip --root / && python -m pip install --user --break-system-packages --upgrade poetry && cd ${config:deckdir}/homebrew/dev/pluginloader/backend && python -m poetry install'",
"problemMatcher": []
},
{
@@ -97,7 +100,7 @@
"dependsOn": [
"checkforsettings"
],
"command": "ssh deck@${config:deckip} -p ${config:deckport} ${config:deckkey} 'mkdir -p ${config:deckdir}/homebrew/dev/pluginloader && mkdir -p ${config:deckdir}/homebrew/dev/plugins'",
"command": "ssh ${config:deckuser}@${config:deckip} -p ${config:deckport} ${config:deckkey} 'mkdir -p ${config:deckdir}/homebrew/dev/pluginloader && mkdir -p ${config:deckdir}/homebrew/plugins'",
"problemMatcher": []
},
{
@@ -105,7 +108,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 --force --rsh='ssh -p ${config:deckport} ${config:deckkey}' --exclude='.git/' --exclude='.github/' --exclude='.vscode/' --exclude='frontend/' --exclude='dist/' --exclude='contrib/' --exclude='*.log' --exclude='**/__pycache__/' --exclude='.gitignore' . ${config:deckuser}@${config:deckip}:${config:deckdir}/homebrew/dev/pluginloader",
"problemMatcher": []
},
// RUN
@@ -117,7 +120,7 @@
"dependsOn": [
"checkforsettings"
],
"command": "ssh deck@${config:deckip} -p ${config:deckport} ${config:deckkey} 'export PLUGIN_PATH=${config:deckdir}/homebrew/dev/plugins; export CHOWN_PLUGIN_PATH=0; export LOG_LEVEL=DEBUG; cd ${config:deckdir}/homebrew/services; echo '${config:deckpass}' | sudo -SE python3 ${config:deckdir}/homebrew/dev/pluginloader/backend/main.py'",
"command": "ssh ${config:deckuser}@${config:deckip} -p ${config:deckport} ${config:deckkey} 'export PATH=${config:deckdir}/.local/bin:$PATH; export PLUGIN_PATH=${config:deckdir}/homebrew/plugins; export CHOWN_PLUGIN_PATH=0; export LOG_LEVEL=DEBUG; cd ${config:deckdir}/homebrew/dev/pluginloader/backend; echo '${config:deckpass}' | poetry run sh -c \"cd ${config:deckdir}/homebrew/services; sudo -SE env \"PATH=\\$PATH\" python3 ${config:deckdir}/homebrew/dev/pluginloader/backend/main.py\"'",
"problemMatcher": []
},
{
@@ -181,10 +184,19 @@
"buildall",
"createfolders",
"dependencies",
"deploy",
// dependencies runs deploy already
// "deploy",
"runpydeck"
],
"problemMatcher": []
},
{
"label": "act",
"type": "shell",
"group": "none",
"detail": "Build release artifact using local CI",
"command": "./act/run-act.sh release",
"problemMatcher": []
}
]
}
+1
View File
@@ -35,6 +35,7 @@ For more information about Decky Loader as well as documentation and development
### 🤔 Common Issues
- Syncthing may use port 8080 on Steam Deck, which Decky Loader needs to function. If you are using Syncthing as a service, please change its port to something else.
- 8384 is the recommended port for Syncthing.
- If you are using any software that uses port 1337 or 8080, please change its port to something else or uninstall it.
- Sometimes Decky will disappear on SteamOS updates. This can easily be fixed by just re-running the installer and installing the stable branch again. If this doesn't work, try installing the prerelease instead. If that doesn't work, then [check the existing issues](https://github.com/SteamDeckHomebrew/decky-loader/issues) and if there isn't one then you can [file a new issue](https://github.com/SteamDeckHomebrew/decky-loader/issues/new?assignees=&labels=bug&template=bug_report.yml&title=%5BBUG%5D+%3Ctitle%3E).
+23 -20
View File
@@ -1,44 +1,47 @@
#!/bin/bash
#!/usr/bin/env bash
set -eo pipefail
type=$1
# bump=$2
oldartifactsdir="old"
parent_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P )
cd "$parent_path"
parent_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" || exit ; pwd -P )
cd "$parent_path" || exit
artifactfolders=$(find artifacts/ -maxdepth 1 -mindepth 1 -type d)
if [[ ${#artifactfolders[@]} > 0 ]]; then
for i in ${artifactfolders[@]}; do
foldername=$(dirname $i)
subfoldername=$(basename $i)
out=$foldername/$oldartifactsdir/$subfoldername-$(date +'%s')
if [[ ! "$subfoldername" =~ "$oldartifactsdir" ]]; then
mkdir -p $out
mv $i $out
printf "Moved "${foldername}"/"${subfoldername}" to "${out}" \n"
fi
done
fi
for i in artifacts/*; do
if [[ ! -d "$i" ]]; then
continue;
fi
subfoldername=$(basename "$i")
if [[ "$subfoldername" == "$oldartifactsdir" ]]; then
continue;
fi
out=artifacts/$oldartifactsdir/$subfoldername-$(date +'%s')
mkdir -p "$out"
mv "$i" "$out"
echo "Moved artifacts/${subfoldername} to ${out}"
done
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"
fi
cd act/artifacts
cd act/artifacts || exit
if [[ -d "1" ]]; then
cd "1/artifact"
cd "1/artifact" || exit
cp "PluginLoader.gz__" "PluginLoader.gz"
gzip -d "PluginLoader.gz"
chmod +x PluginLoader
-6
View File
@@ -1,6 +0,0 @@
from enum import Enum
class UserType(Enum):
HOST_USER = 1
EFFECTIVE_USER = 2
ROOT = 3
@@ -4,53 +4,70 @@ import json
# from pprint import pformat
# Partial imports
from aiohttp import ClientSession, web
from asyncio import get_event_loop, sleep
from concurrent.futures import ProcessPoolExecutor
from aiohttp import ClientSession
from asyncio import sleep
from hashlib import sha256
from io import BytesIO
from logging import getLogger
from os import R_OK, W_OK, path, rename, listdir, access, mkdir
from os import R_OK, W_OK, path, listdir, access, mkdir
from re import sub
from shutil import rmtree
from time import time
from zipfile import ZipFile
from localplatform import chown, chmod
from enum import IntEnum
from typing import Dict, List, TypedDict
# Local modules
from helpers import get_ssl_context, download_remote_binary_to_path
from injector import get_gamepadui_tab
from .localplatform.localplatform import chown, chmod
from .loader import Loader, Plugins
from .helpers import get_ssl_context, download_remote_binary_to_path
from .settings import SettingsManager
logger = getLogger("Browser")
class PluginInstallType(IntEnum):
INSTALL = 0
REINSTALL = 1
UPDATE = 2
class PluginInstallRequest(TypedDict):
name: str
artifact: str
version: str
hash: str
install_type: PluginInstallType
class PluginInstallContext:
def __init__(self, artifact, name, version, hash) -> None:
def __init__(self, artifact: str, name: str, version: str, hash: str) -> None:
self.artifact = artifact
self.name = name
self.version = version
self.hash = hash
class PluginBrowser:
def __init__(self, plugin_path, plugins, loader, settings) -> None:
def __init__(self, plugin_path: str, plugins: Plugins, loader: Loader, settings: SettingsManager) -> None:
self.plugin_path = plugin_path
self.plugins = plugins
self.loader = loader
self.settings = settings
self.install_requests = {}
self.install_requests: Dict[str, PluginInstallContext | List[PluginInstallContext]] = {}
def _unzip_to_plugin_dir(self, zip, name, hash):
def _unzip_to_plugin_dir(self, zip: BytesIO, name: str, hash: str):
zip_hash = sha256(zip.getbuffer()).hexdigest()
if hash and (zip_hash != hash):
return False
zip_file = ZipFile(zip)
zip_file.extractall(self.plugin_path)
plugin_dir = path.join(self.plugin_path, self.find_plugin_folder(name))
plugin_folder = self.find_plugin_folder(name)
assert plugin_folder is not None
plugin_dir = path.join(self.plugin_path, plugin_folder)
if not chown(plugin_dir) or not chmod(plugin_dir, 555):
logger.error(f"chown/chmod exited with a non-zero exit code")
return False
return True
async def _download_remote_binaries_for_plugin_with_name(self, pluginBasePath):
async def _download_remote_binaries_for_plugin_with_name(self, pluginBasePath: str):
rv = False
try:
packageJsonPath = path.join(pluginBasePath, 'package.json')
@@ -91,7 +108,7 @@ class PluginBrowser:
return rv
"""Return the filename (only) for the specified plugin"""
def find_plugin_folder(self, name):
def find_plugin_folder(self, name: str) -> str | None:
for folder in listdir(self.plugin_path):
try:
with open(path.join(self.plugin_path, folder, 'plugin.json'), "r", encoding="utf-8") as f:
@@ -102,23 +119,23 @@ class PluginBrowser:
except:
logger.debug(f"skipping {folder}")
async def uninstall_plugin(self, name):
async def uninstall_plugin(self, name: str):
if self.loader.watcher:
self.loader.watcher.disabled = True
tab = await get_gamepadui_tab()
plugin_dir = path.join(self.plugin_path, self.find_plugin_folder(name))
plugin_folder = self.find_plugin_folder(name)
assert plugin_folder is not None
plugin_dir = path.join(self.plugin_path, plugin_folder)
try:
logger.info("uninstalling " + name)
logger.info(" at dir " + plugin_dir)
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)
await self.loader.ws.emit("loader/unload_plugin", name)
# 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()
await 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)
@@ -133,30 +150,33 @@ class PluginBrowser:
if self.loader.watcher:
self.loader.watcher.disabled = False
async def _install(self, artifact, name, version, hash):
async def _install(self, artifact: str, name: str, version: str, hash: str):
await self.loader.ws.emit("loader/plugin_download_start", name)
await self.loader.ws.emit("loader/plugin_download_info", 5, "Store.download_progress_info.start")
# Will be set later in code
res_zip = None
# Check if plugin is installed
isInstalled = False
# Preserve plugin order before removing plugin (uninstall alters the order and removes the plugin from the list)
current_plugin_order = self.settings.getSetting("pluginOrder")[:]
if self.loader.watcher:
self.loader.watcher.disabled = True
try:
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 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})")
await self.loader.ws.emit("loader/plugin_download_info", 10, "Store.download_progress_info.open_zip")
res_zip = BytesIO(open(artifact[7:], "rb").read())
else:
logger.info(f"Installing {name} from URL (Version: {version})")
await self.loader.ws.emit("loader/plugin_download_info", 10, "Store.download_progress_info.download_zip")
async with ClientSession() as client:
logger.debug(f"Fetching {artifact}")
res = await client.get(artifact, ssl=get_ssl_context())
#TODO track progress of this download in chunks like with decky updates
#TODO but squish with min 15 and max 75
if res.status == 200:
logger.debug("Got 200. Reading...")
data = await res.read()
@@ -165,6 +185,42 @@ class PluginBrowser:
else:
logger.fatal(f"Could not fetch from URL. {await res.text()}")
await self.loader.ws.emit("loader/plugin_download_info", 80, "Store.download_progress_info.increment_count")
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}")
await self.loader.ws.emit("loader/plugin_download_info", 85, "Store.download_progress_info.parse_zip")
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}")
@@ -172,78 +228,82 @@ class PluginBrowser:
# If plugin is installed, uninstall it
if isInstalled:
await self.loader.ws.emit("loader/plugin_download_info", 90, "Store.download_progress_info.uninstalling_previous")
try:
logger.debug("Uninstalling existing plugin...")
await self.uninstall_plugin(name)
except:
logger.error(f"Plugin {name} could not be uninstalled.")
await self.loader.ws.emit("loader/plugin_download_info", 95, "Store.download_progress_info.installing_plugin")
# 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)
#TODO count again from 0% to 100% quickly for this one if it does anything
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()
await self.loader.plugins[name].stop()
self.loader.plugins.pop(name, None)
await sleep(1)
current_plugin_order = self.settings.getSetting("pluginOrder")
current_plugin_order.append(name)
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)
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)
await self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder)
else:
logger.fatal(f"Failed Downloading Remote Binaries")
else:
self.log.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
logger.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
if self.loader.watcher:
self.loader.watcher.disabled = False
await self.loader.ws.emit("loader/plugin_download_finish", name)
async def request_plugin_install(self, artifact, name, version, hash, install_type):
async def request_plugin_install(self, artifact: str, name: str, version: str, hash: str, install_type: PluginInstallType):
request_id = str(time())
self.install_requests[request_id] = PluginInstallContext(artifact, name, version, hash)
tab = await get_gamepadui_tab()
await tab.open_websocket()
await tab.evaluate_js(f"DeckyPluginLoader.addPluginInstallPrompt('{name}', '{version}', '{request_id}', '{hash}', {install_type})")
async def request_multiple_plugin_installs(self, requests):
await self.loader.ws.emit("loader/add_plugin_install_prompt", 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}])")
await self.loader.ws.emit("loader/add_multiple_plugins_install_prompt", request_id, requests)
async def confirm_plugin_install(self, request_id):
async def confirm_plugin_install(self, request_id: str):
requestOrRequests = self.install_requests.pop(request_id)
if isinstance(requestOrRequests, list):
[await self._install(req.artifact, req.name, req.version, req.hash) for req in requestOrRequests]
else:
await self._install(requestOrRequests.artifact, requestOrRequests.name, requestOrRequests.version, requestOrRequests.hash)
def cancel_plugin_install(self, request_id):
def cancel_plugin_install(self, request_id: str):
self.install_requests.pop(request_id)
def cleanup_plugin_settings(self, name):
def cleanup_plugin_settings(self, name: str):
"""Removes any settings related to a plugin. Propably called when a plugin is uninstalled.
Args:
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:
+10
View File
@@ -0,0 +1,10 @@
from enum import IntEnum
class UserType(IntEnum):
HOST_USER = 1
EFFECTIVE_USER = 2
ROOT = 3
class PluginLoadType(IntEnum):
LEGACY_EVAL_IIFE = 0 # legacy, uses legacy serverAPI
ESMODULE_V1 = 1 # esmodule loading with modern @decky/backend apis
@@ -2,18 +2,21 @@ import re
import ssl
import uuid
import os
import sys
import subprocess
from hashlib import sha256
from io import BytesIO
import importlib.metadata
import certifi
from aiohttp.web import Response, middleware
from aiohttp.web import Request, Response, middleware
from aiohttp.typedefs import Handler
from aiohttp import ClientSession
import localplatform
from customtypes import UserType
from .localplatform import localplatform
from .enums import UserType
from logging import getLogger
from packaging.version import Version
SSHD_UNIT = "sshd.service"
REMOTE_DEBUGGER_UNIT = "steam-web-debug-portforward.service"
# global vars
@@ -21,6 +24,7 @@ csrf_token = str(uuid.uuid4())
ssl_ctx = ssl.create_default_context(cafile=certifi.where())
assets_regex = re.compile("^/plugins/.*/assets/.*")
dist_regex = re.compile("^/plugins/.*/dist/.*")
frontend_regex = re.compile("^/frontend/.*")
logger = getLogger("Main")
@@ -31,17 +35,29 @@ 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)):
async def csrf_middleware(request: Request, handler: Handler):
if str(request.method) == "OPTIONS" or \
request.headers.get('X-Decky-Auth') == 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("/steam_resource/") or \
str(request.rel_url).startswith("/frontend/") or \
str(request.rel_url.path) == "/fetch" or \
str(request.rel_url.path) == "/ws" or \
assets_regex.match(str(request.rel_url)) or \
dist_regex.match(str(request.rel_url)) or \
frontend_regex.match(str(request.rel_url)):
return await handler(request)
return Response(text='Forbidden', status='403')
return Response(text='Forbidden', status=403)
# Get the default homebrew path unless a home_path is specified. home_path argument is deprecated
def get_homebrew_path(home_path = None) -> str:
def get_homebrew_path() -> str:
return localplatform.get_unprivileged_path()
# Recursively create path and chown as user
def mkdir_as_user(path):
def mkdir_as_user(path: str):
path = os.path.realpath(path)
os.makedirs(path, exist_ok=True)
localplatform.chown(path)
@@ -49,31 +65,38 @@ def mkdir_as_user(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()
# Normalize Python-style version to conform to Decky style
v = Version(importlib.metadata.version("decky_loader"))
version_str = f'v{v.major}.{v.minor}.{v.micro}'
if v.pre:
version_str += f'-pre{v.pre[1]}'
if v.post:
version_str += f'-dev{v.post}'
return version_str
except Exception as e:
logger.warn(f"Failed to execute get_loader_version(): {str(e)}")
return "unknown"
user_agent = f"Decky/{get_loader_version()} (https://decky.xyz)"
# returns the appropriate system python paths
def get_system_pythonpaths() -> list[str]:
extra_args = {}
if localplatform.ON_LINUX:
# run as normal normal user to also include user python paths
extra_args["user"] = localplatform.localplatform._get_user_id()
extra_args["env"] = {}
try:
# run as normal normal user if on linux to also include user python paths
proc = subprocess.run(["python3" if localplatform.ON_LINUX else "python", "-c", "import sys; print('\\n'.join(x for x in sys.path if x))"],
capture_output=True, **extra_args)
# TODO make this less insane
capture_output=True, user=localplatform.localplatform._get_user_id() if localplatform.ON_LINUX else None, env={} if localplatform.ON_LINUX else None) # pyright: ignore [reportPrivateUsage]
return [x.strip() for x in proc.stdout.decode().strip().split("\n")]
except Exception as e:
logger.warn(f"Failed to execute get_system_pythonpaths(): {str(e)}")
return []
# Download Remote Binaries to local Plugin
async def download_remote_binary_to_path(url, binHash, path) -> bool:
async def download_remote_binary_to_path(url: str, binHash: str, path: str) -> bool:
rv = False
try:
if os.access(os.path.dirname(path), os.W_OK):
@@ -110,46 +133,42 @@ def set_user_group() -> str:
# Get the user id hosting the plugin loader
def get_user_id() -> int:
return localplatform.localplatform._get_user_id()
return localplatform.localplatform._get_user_id() # pyright: ignore [reportPrivateUsage]
# Get the user hosting the plugin loader
def get_user() -> str:
return localplatform.localplatform._get_user()
return localplatform.localplatform._get_user() # pyright: ignore [reportPrivateUsage]
# Get the effective user id of the running process
def get_effective_user_id() -> int:
return localplatform.localplatform._get_effective_user_id()
return localplatform.localplatform._get_effective_user_id() # pyright: ignore [reportPrivateUsage]
# Get the effective user of the running process
def get_effective_user() -> str:
return localplatform.localplatform._get_effective_user()
return localplatform.localplatform._get_effective_user() # pyright: ignore [reportPrivateUsage]
# Get the effective user group id of the running process
def get_effective_user_group_id() -> int:
return localplatform.localplatform._get_effective_user_group_id()
return localplatform.localplatform._get_effective_user_group_id() # pyright: ignore [reportPrivateUsage]
# Get the effective user group of the running process
def get_effective_user_group() -> str:
return localplatform.localplatform._get_effective_user_group()
return localplatform.localplatform._get_effective_user_group() # pyright: ignore [reportPrivateUsage]
# Get the user owner of the given file path.
def get_user_owner(file_path) -> str:
return localplatform.localplatform._get_user_owner(file_path)
def get_user_owner(file_path: str) -> str:
return localplatform.localplatform._get_user_owner(file_path) # pyright: ignore [reportPrivateUsage]
# Get the user group of the given file path.
def get_user_group(file_path) -> str:
return localplatform.localplatform._get_user_group(file_path)
# Get the user group of the given file path, or the user group hosting the plugin loader
def get_user_group(file_path: str | None = None) -> str:
return localplatform.localplatform._get_user_group(file_path) # pyright: ignore [reportPrivateUsage]
# Get the group id of the user hosting the plugin loader
def get_user_group_id() -> int:
return localplatform.localplatform._get_user_group_id()
# Get the group of the user hosting the plugin loader
def get_user_group() -> str:
return localplatform.localplatform._get_user_group()
return localplatform.localplatform._get_user_group_id() # pyright: ignore [reportPrivateUsage]
# Get the default home path unless a user is specified
def get_home_path(username = None) -> str:
def get_home_path(username: str | None = None) -> str:
return localplatform.get_home_path(UserType.ROOT if username == "root" else UserType.HOST_USER)
async def is_systemd_unit_active(unit_name: str) -> bool:
@@ -2,10 +2,9 @@
from asyncio import sleep
from logging import getLogger
from traceback import format_exc
from typing import List
from typing import Any, Callable, List, TypedDict, Dict
from aiohttp import ClientSession, WSMsgType
from aiohttp import ClientSession
from aiohttp.client_exceptions import ClientConnectorError, ClientOSError
from asyncio.exceptions import TimeoutError
import uuid
@@ -14,15 +13,20 @@ 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
@@ -32,17 +36,20 @@ class Tab:
self.websocket = await self.client.ws_connect(self.ws_url)
async def close_websocket(self):
await self.websocket.close()
await self.client.close()
if self.websocket:
await self.websocket.close()
if self.client:
await self.client.close()
async def listen_for_message(self):
async for message in self.websocket:
data = message.json()
yield data
logger.warn(f"The Tab {self.title} socket has been disconnected while listening for messages.")
await self.close_websocket()
if self.websocket:
async for message in self.websocket:
data = message.json()
yield data
logger.warn(f"The Tab {self.title} socket has been disconnected while listening for messages.")
await self.close_websocket()
async def _send_devtools_cmd(self, dc, receive=True):
async def _send_devtools_cmd(self, dc: Dict[str, Any], receive: bool = True):
if self.websocket:
self.cmd_id += 1
dc["id"] = self.cmd_id
@@ -54,7 +61,7 @@ class Tab:
return None
raise RuntimeError("Websocket not opened")
async def evaluate_js(self, js, run_async=False, manage_socket=True, get_result=True):
async def evaluate_js(self, js: str, run_async: bool | None = False, manage_socket: bool | None = True, get_result: bool = True):
try:
if manage_socket:
await self.open_websocket()
@@ -73,15 +80,16 @@ class Tab:
await self.close_websocket()
return res
async def has_global_var(self, var_name, manage_socket=True):
async def has_global_var(self, var_name: str, manage_socket: bool = True):
res = await self.evaluate_js(f"window['{var_name}'] !== null && window['{var_name}'] !== undefined", False, manage_socket)
assert res is not None
if not "result" in res or not "result" in res["result"] or not "value" in res["result"]["result"]:
return False
return res["result"]["result"]["value"]
async def close(self, manage_socket=True):
async def close(self, manage_socket: bool = True):
try:
if manage_socket:
await self.open_websocket()
@@ -111,7 +119,7 @@ class Tab:
"method": "Page.disable",
}, False)
async def refresh(self, manage_socket=True):
async def refresh(self, manage_socket: bool = True):
try:
if manage_socket:
await self.open_websocket()
@@ -125,7 +133,7 @@ class Tab:
await self.close_websocket()
return
async def reload_and_evaluate(self, js, manage_socket=True):
async def reload_and_evaluate(self, js: str, manage_socket: bool = True):
"""
Reloads the current tab, with JS to run on load via debugger
"""
@@ -153,11 +161,13 @@ class Tab:
}
}, True)
assert breakpoint_res is not None
logger.info(breakpoint_res)
# Page finishes loading when breakpoint hits
for x in range(20):
for _ in range(20):
# this works around 1/5 of the time, so just send it 8 times.
# the js accounts for being injected multiple times allowing only one instance to run at a time anyway
await self._send_devtools_cmd({
@@ -176,7 +186,7 @@ class Tab:
}
}, False)
for x in range(4):
for _ in range(4):
await self._send_devtools_cmd({
"method": "Debugger.resume"
}, False)
@@ -190,7 +200,7 @@ class Tab:
await self.close_websocket()
return
async def add_script_to_evaluate_on_new_document(self, js, add_dom_wrapper=True, manage_socket=True, get_result=True):
async def add_script_to_evaluate_on_new_document(self, js: str, add_dom_wrapper: bool = True, manage_socket: bool = True, get_result: bool = True):
"""
How the underlying call functions is not particularly clear from the devtools docs, so stealing puppeteer's description:
@@ -253,7 +263,7 @@ class Tab:
await self.close_websocket()
return res
async def remove_script_to_evaluate_on_new_document(self, script_id, manage_socket=True):
async def remove_script_to_evaluate_on_new_document(self, script_id: str, manage_socket: bool = True):
"""
Removes a script from a page that was added with `add_script_to_evaluate_on_new_document`
@@ -267,7 +277,7 @@ class Tab:
if manage_socket:
await self.open_websocket()
res = await self._send_devtools_cmd({
await self._send_devtools_cmd({
"method": "Page.removeScriptToEvaluateOnNewDocument",
"params": {
"identifier": script_id
@@ -278,15 +288,16 @@ class Tab:
if manage_socket:
await self.close_websocket()
async def has_element(self, element_name, manage_socket=True):
async def has_element(self, element_name: str, manage_socket: bool = True):
res = await self.evaluate_js(f"document.getElementById('{element_name}') != null", False, manage_socket)
assert res is not None
if not "result" in res or not "result" in res["result"] or not "value" in res["result"]["result"]:
return False
return res["result"]["result"]["value"]
async def inject_css(self, style, manage_socket=True):
async def inject_css(self, style: str, manage_socket: bool = True):
try:
css_id = str(uuid.uuid4())
@@ -300,6 +311,8 @@ class Tab:
}})()
""", False, manage_socket)
assert result is not None
if "exceptionDetails" in result["result"]:
return {
"success": False,
@@ -316,7 +329,7 @@ class Tab:
"result": e
}
async def remove_css(self, css_id, manage_socket=True):
async def remove_css(self, css_id: str, manage_socket: bool = True):
try:
result = await self.evaluate_js(
f"""
@@ -328,6 +341,8 @@ class Tab:
}})()
""", False, manage_socket)
assert result is not None
if "exceptionDetails" in result["result"]:
return {
"success": False,
@@ -343,8 +358,9 @@ class Tab:
"result": e
}
async def get_steam_resource(self, url):
async def get_steam_resource(self, url: str):
res = await self.evaluate_js(f'(async function test() {{ return await (await fetch("{url}")).text() }})()', True)
assert res is not None
return res["result"]["result"]["value"]
def __repr__(self):
@@ -380,14 +396,14 @@ async def get_tabs() -> List[Tab]:
raise Exception(f"/json did not return 200. {await res.text()}")
async def get_tab(tab_name) -> Tab:
async def get_tab(tab_name: str) -> Tab:
tabs = await get_tabs()
tab = next((i for i in tabs if i.title == tab_name), None)
if not tab:
raise ValueError(f"Tab {tab_name} not found")
return tab
async def get_tab_lambda(test) -> Tab:
async def get_tab_lambda(test: Callable[[Tab], bool]) -> Tab:
tabs = await get_tabs()
tab = next((i for i in tabs if test(i)), None)
if not tab:
@@ -396,7 +412,7 @@ async def get_tab_lambda(test) -> 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_URL = "Valve Steam Gamepad/default" # Steam Big Picture Mode tab
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 in SHARED_CTX_NAMES
@@ -408,7 +424,7 @@ async def get_gamepadui_tab() -> Tab:
raise ValueError(f"GamepadUI Tab not found")
return tab
async def inject_to_tab(tab_name, js, run_async=False):
async def inject_to_tab(tab_name: str, js: str, run_async: bool = False):
tab = await get_tab(tab_name)
return await tab.evaluate_js(js, run_async)
@@ -416,7 +432,7 @@ async def inject_to_tab(tab_name, js, run_async=False):
async def close_old_tabs():
tabs = await get_tabs()
for t in tabs:
if not t.title or (t.title not in SHARED_CTX_NAMES and any(url in t.url for url in CLOSEABLE_URLS) and DO_NOT_CLOSE_URL not in t.url):
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)
@@ -1,35 +1,44 @@
from asyncio import Queue, sleep
from json.decoder import JSONDecodeError
from __future__ import annotations
from asyncio import AbstractEventLoop, Queue, sleep
from logging import getLogger
from os import listdir, path
from pathlib import Path
from traceback import print_exc
from traceback import print_exc, format_exc
from typing import Any, Tuple, Dict, cast
from aiohttp import web
from os.path import exists
from watchdog.events import RegexMatchingEventHandler
from watchdog.events import RegexMatchingEventHandler, DirCreatedEvent, DirModifiedEvent, FileCreatedEvent, FileModifiedEvent
from watchdog.observers import Observer
from injector import get_tab, get_gamepadui_tab
from plugin import PluginWrapper
from typing import TYPE_CHECKING, List
if TYPE_CHECKING:
from .main import PluginManager
from .plugin.plugin import PluginWrapper
from .wsrouter import WSRouter
from .enums import PluginLoadType
Plugins = dict[str, PluginWrapper]
ReloadQueue = Queue[Tuple[str, str, bool | None] | Tuple[str, str]]
class FileChangeHandler(RegexMatchingEventHandler):
def __init__(self, queue, plugin_path) -> None:
super().__init__(regexes=[r'^.*?dist\/index\.js$', r'^.*?main\.py$'])
def __init__(self, queue: ReloadQueue, plugin_path: str) -> None:
super().__init__(regexes=[r'^.*?dist\/index\.js$', r'^.*?main\.py$']) # pyright: ignore [reportUnknownMemberType]
self.logger = getLogger("file-watcher")
self.plugin_path = plugin_path
self.queue = queue
self.disabled = True
def maybe_reload(self, src_path):
def maybe_reload(self, src_path: str):
if self.disabled:
return
plugin_dir = Path(path.relpath(src_path, self.plugin_path)).parts[0]
if exists(path.join(self.plugin_path, plugin_dir, "plugin.json")):
self.queue.put_nowait((path.join(self.plugin_path, plugin_dir, "main.py"), plugin_dir, True))
def on_created(self, event):
src_path = event.src_path
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
@@ -42,8 +51,8 @@ class FileChangeHandler(RegexMatchingEventHandler):
self.logger.debug(f"file created: {src_path}")
self.maybe_reload(src_path)
def on_modified(self, event):
src_path = event.src_path
def on_modified(self, event: DirModifiedEvent | FileModifiedEvent):
src_path = cast(str, event.src_path) # type: ignore
if "__pycache__" in src_path:
return
@@ -57,170 +66,146 @@ class FileChangeHandler(RegexMatchingEventHandler):
self.maybe_reload(src_path)
class Loader:
def __init__(self, server_instance, plugin_path, loop, live_reload=False) -> None:
def __init__(self, server_instance: PluginManager, ws: WSRouter, plugin_path: str, loop: AbstractEventLoop, live_reload: bool = False) -> None:
self.loop = loop
self.logger = getLogger("Loader")
self.ws = ws
self.plugin_path = plugin_path
self.logger.info(f"plugin_path: {self.plugin_path}")
self.plugins : dict[str, PluginWrapper] = {}
self.plugins: Plugins = {}
self.watcher = None
self.live_reload = live_reload
self.reload_queue = Queue()
self.reload_queue: ReloadQueue = Queue()
self.loop.create_task(self.handle_reloads())
if live_reload:
self.observer = Observer()
self.watcher = FileChangeHandler(self.reload_queue, plugin_path)
self.observer.schedule(self.watcher, self.plugin_path, recursive=True)
self.observer.schedule(self.watcher, self.plugin_path, recursive=True) # pyright: ignore [reportUnknownMemberType]
self.observer.start()
self.loop.create_task(self.enable_reload_wait())
server_instance.add_routes([
server_instance.web_app.add_routes([
web.get("/frontend/{path:.*}", self.handle_frontend_assets),
web.get("/locales/{path:.*}", self.handle_frontend_locales),
web.get("/plugins", self.get_plugins),
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}/dist/{path:.*}", self.handle_plugin_dist),
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)
])
server_instance.ws.add_route("loader/get_plugins", self.get_plugins)
server_instance.ws.add_route("loader/reload_plugin", self.handle_plugin_backend_reload)
server_instance.ws.add_route("loader/call_plugin_method", self.handle_plugin_method_call)
server_instance.ws.add_route("loader/call_legacy_plugin_method", self.handle_plugin_method_call_legacy)
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"])
if self.watcher:
self.logger.info("Hot reload enabled")
self.watcher.disabled = False
async def handle_frontend_assets(self, request: web.Request):
file = Path(__file__).parents[1].joinpath("static").joinpath(request.match_info["path"])
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
async def handle_frontend_locales(self, request):
async def handle_frontend_locales(self, request: web.Request):
req_lang = request.match_info["path"]
file = path.join(path.dirname(__file__), "locales", req_lang)
file = Path(__file__).parents[1].joinpath("locales").joinpath(req_lang)
if exists(file):
return web.FileResponse(file, headers={"Cache-Control": "no-cache", "Content-Type": "application/json"})
else:
self.logger.info(f"Language {req_lang} not available, returning an empty dictionary")
return web.json_response(data={}, headers={"Cache-Control": "no-cache"})
async def get_plugins(self, request):
async def get_plugins(self):
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])
return [{"name": str(i), "version": i.version, "load_type": i.load_type} for i in plugins]
def handle_plugin_frontend_assets(self, request):
async def handle_plugin_dist(self, request: web.Request):
plugin = self.plugins[request.match_info["plugin_name"]]
file = path.join(self.plugin_path, plugin.plugin_directory, "dist", request.match_info["path"])
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
async def handle_plugin_frontend_assets(self, request: web.Request):
plugin = self.plugins[request.match_info["plugin_name"]]
file = path.join(self.plugin_path, plugin.plugin_directory, "dist/assets", request.match_info["path"])
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
def handle_frontend_bundle(self, request):
async def handle_frontend_bundle(self, request: web.Request):
plugin = self.plugins[request.match_info["plugin_name"]]
with open(path.join(self.plugin_path, plugin.plugin_directory, "dist/index.js"), "r", encoding="utf-8") as bundle:
return web.Response(text=bundle.read(), content_type="application/javascript")
def import_plugin(self, file, plugin_directory, refresh=False, batch=False):
async 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)
async def plugin_emitted_event(event: str, args: Any):
self.logger.debug(f"PLUGIN EMITTED EVENT: {event} with args {args}")
await self.ws.emit(f"loader/plugin_event", {"plugin": plugin.name, "event": event, "args": args})
plugin = PluginWrapper(file, plugin_directory, self.plugin_path, plugin_emitted_event)
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()
await 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))
self.loop.create_task(self.dispatch_plugin(plugin.name, plugin.version, plugin.load_type))
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}')")
async def dispatch_plugin(self, name: str, version: str | None, load_type: int = PluginLoadType.ESMODULE_V1.value):
await self.ws.emit("loader/import_plugin", name, version, load_type)
def import_plugins(self):
async 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)
await 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)
await self.import_plugin(*args) # pyright: ignore [reportArgumentType]
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 = {}
async def handle_plugin_method_call_legacy(self, plugin_name: str, method_name: str, kwargs: Dict[Any, Any]):
res: Dict[Any, Any] = {}
plugin = self.plugins[plugin_name]
try:
if method_name.startswith("_"):
raise RuntimeError("Tried to call private method")
res["result"] = await plugin.execute_method(method_name, args)
raise RuntimeError(f"Plugin {plugin.name} tried to call private method {method_name}")
res["result"] = await plugin.execute_legacy_method(method_name, kwargs)
res["success"] = True
except Exception as e:
res["result"] = str(e)
res["success"] = False
return web.json_response(res)
return 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")
async def handle_plugin_method_call(self, plugin_name: str, method_name: str, *args: List[Any]):
plugin = self.plugins[plugin_name]
try:
return web.Response(text=await tab.get_steam_resource(f"https://steamloopback.host/{request.match_info['path']}"), content_type="text/html")
if method_name.startswith("_"):
raise RuntimeError(f"Plugin {plugin.name} tried to call private method {method_name}")
result = await plugin.execute_method(method_name, *args)
except Exception as e:
return web.Response(text=str(e), status=400)
self.logger.error(f"Method {method_name} of plugin {plugin.name} failed with the following exception:\n{format_exc()}")
raise e # throw again to pass the error to the frontend
return result
async def handle_backend_reload_request(self, request):
plugin_name : str = request.match_info["plugin_name"]
async def handle_plugin_backend_reload(self, plugin_name: str):
plugin = self.plugins[plugin_name]
await self.reload_queue.put((plugin.file, plugin.plugin_directory))
@@ -4,11 +4,11 @@ ON_WINDOWS = platform.system() == "Windows"
ON_LINUX = not ON_WINDOWS
if ON_WINDOWS:
from localplatformwin import *
import localplatformwin as localplatform
from .localplatformwin import *
from . import localplatformwin as localplatform
else:
from localplatformlinux import *
import localplatformlinux as localplatform
from .localplatformlinux import *
from . import localplatformlinux as localplatform
def get_privileged_path() -> str:
'''Get path accessible by elevated user. Holds plugins, decky loader and decky loader configs'''
@@ -40,4 +40,13 @@ def get_keep_systemd_service() -> bool:
def get_log_level() -> int:
return {"CRITICAL": 50, "ERROR": 40, "WARNING": 30, "INFO": 20, "DEBUG": 10}[
os.getenv("LOG_LEVEL", "INFO")
]
]
def get_selinux() -> bool:
if ON_LINUX:
from subprocess import check_output
try:
if (check_output("getenforce").decode("ascii").strip("\n") == "Enforcing"): return True
except FileNotFoundError:
pass
return False
@@ -1,6 +1,6 @@
import os, pwd, grp, sys, logging
from subprocess import call, run, DEVNULL, PIPE, STDOUT
from customtypes import UserType
from ..enums import UserType
logger = logging.getLogger("localplatform")
@@ -29,21 +29,17 @@ def _get_effective_user_group() -> str:
return grp.getgrgid(_get_effective_user_group_id()).gr_name
# Get the user owner of the given file path.
def _get_user_owner(file_path) -> str:
def _get_user_owner(file_path: str) -> str:
return pwd.getpwuid(os.stat(file_path).st_uid).pw_name
# Get the user group of the given file path.
def _get_user_group(file_path) -> str:
return grp.getgrgid(os.stat(file_path).st_gid).gr_name
# Get the user group of the given file path, or the user group hosting the plugin loader
def _get_user_group(file_path: str | None = None) -> str:
return grp.getgrgid(os.stat(file_path).st_gid if file_path is not None else _get_user_group_id()).gr_name
# Get the group id of the user hosting the plugin loader
def _get_user_group_id() -> int:
return pwd.getpwuid(_get_user_id()).pw_gid
# Get the group of the user hosting the plugin loader
def _get_user_group() -> str:
return grp.getgrgid(_get_user_group_id()).gr_name
def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool = True) -> bool:
user_str = ""
@@ -60,8 +56,24 @@ def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool =
return result == 0
def chmod(path : str, permissions : int, recursive : bool = True) -> bool:
result = call(["chmod", "-R", str(permissions), path] if recursive else ["chmod", str(permissions), path])
return result == 0
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)
@@ -127,24 +139,40 @@ async def service_restart(service_name : str) -> bool:
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()
os.makedirs(path, exist_ok=True)
return path
def _parent_dir(path : str) -> str:
def _parent_dir(path : str | None) -> str | None:
if path == None:
return None
@@ -161,16 +189,23 @@ def get_unprivileged_path() -> str:
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 not os.path.exists(path):
if hasattr(sys, 'frozen'):
# Expected path of loader binary is /home/deck/homebrew/service/PluginLoader
path = _parent_dir(_parent_dir(os.path.realpath(sys.argv[0])))
else:
# Expected path of this file is $src_root/backend/src/localplatformlinux.py
path = _parent_dir(_parent_dir(_parent_dir(__file__)))
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
os.makedirs(path, exist_ok=True)
return path
@@ -191,4 +226,4 @@ def get_unprivileged_user() -> str:
logger.warn("Unprivileged user is not properly configured. Defaulting to 'deck'")
user = 'deck'
return user
return user
@@ -1,4 +1,4 @@
from customtypes import UserType
from ..enums import UserType
import os, sys
def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool = True) -> bool:
@@ -47,6 +47,8 @@ def get_unprivileged_path() -> str:
if path == None:
path = os.getenv("PRIVILEGED_PATH", os.path.join(os.path.expanduser("~"), "homebrew"))
os.makedirs(path, exist_ok=True)
return path
def get_unprivileged_user() -> str:
@@ -1,10 +1,13 @@
import asyncio, time, random
from localplatform import ON_WINDOWS
import asyncio, time
from typing import Any, Callable, Coroutine
import random
from .localplatform import ON_WINDOWS
BUFFER_LIMIT = 2 ** 20 # 1 MiB
class UnixSocket:
def __init__(self, on_new_message):
def __init__(self, on_new_message: Callable[[str], Coroutine[Any, Any, Any]]):
'''
on_new_message takes 1 string argument.
It's return value gets used, if not None, to write data to the socket.
@@ -15,6 +18,7 @@ class UnixSocket:
self.socket = None
self.reader = None
self.writer = None
self.server_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)
@@ -46,28 +50,32 @@ class UnixSocket:
self.reader = None
async def read_single_line(self) -> str|None:
reader, writer = await self.get_socket_connection()
reader, _ = await self.get_socket_connection()
if self.reader == None:
return None
try:
assert reader
except AssertionError:
return
return await self._read_single_line(reader)
async def write_single_line(self, message : str):
reader, writer = await self.get_socket_connection()
_, writer = await self.get_socket_connection()
if self.writer == None:
return;
try:
assert writer
except AssertionError:
return
await self._write_single_line(writer, message)
async def _read_single_line(self, reader) -> str:
async def _read_single_line(self, reader: asyncio.StreamReader) -> str:
line = bytearray()
while True:
try:
line.extend(await reader.readuntil())
except asyncio.LimitOverrunError:
line.extend(await reader.read(reader._limit))
line.extend(await reader.read(reader._limit)) # pyright: ignore [reportUnknownMemberType, reportUnknownArgumentType, reportAttributeAccessIssue]
continue
except asyncio.IncompleteReadError as err:
line.extend(err.partial)
@@ -77,27 +85,32 @@ class UnixSocket:
return line.decode("utf-8")
async def _write_single_line(self, writer, message : str):
async def _write_single_line(self, writer: asyncio.StreamWriter, message : str):
if not message.endswith("\n"):
message += "\n"
writer.write(message.encode("utf-8"))
await writer.drain()
async def write_single_line_server(self, message: str):
if self.server_writer is None:
return
await self._write_single_line(self.server_writer, message)
async def _listen_for_method_call(self, reader, writer):
async def _listen_for_method_call(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
self.server_writer = writer
while True:
def _(task: asyncio.Task[str|None]):
res = task.result()
if res is not None:
asyncio.create_task(self._write_single_line(writer, res))
line = await self._read_single_line(reader)
try:
res = await self.on_new_message(line)
except Exception as e:
return
if res != None:
await self._write_single_line(writer, res)
asyncio.create_task(self.on_new_message(line)).add_done_callback(_)
class PortSocket (UnixSocket):
def __init__(self, on_new_message):
def __init__(self, on_new_message: Callable[[str], Coroutine[Any, Any, Any]]):
'''
on_new_message takes 1 string argument.
It's return value gets used, if not None, to write data to the socket.
@@ -125,8 +138,8 @@ class PortSocket (UnixSocket):
return True
if ON_WINDOWS:
class LocalSocket (PortSocket):
class LocalSocket (PortSocket): # type: ignore
pass
else:
class LocalSocket (UnixSocket):
pass
pass
+189
View File
@@ -0,0 +1,189 @@
# Change PyInstaller files permissions
import sys
from typing import Dict
from .localplatform.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 # pyright: ignore [reportMissingTypeStubs]
# Partial imports
from aiohttp import client_exceptions
from aiohttp.web import Application, Response, Request, get, run_app, static # pyright: ignore [reportUnknownVariableType]
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_loader_version,
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 .enums import UserType
from .wsrouter import WSRouter
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.ws = WSRouter(self.loop, self.web_app)
self.plugin_loader = Loader(self, self.ws, 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) # pyright: ignore [reportUnknownMemberType]
self.web_app.add_routes([static("/static", path.join(path.dirname(__file__), '..', 'static'))])
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")
await self.plugin_loader.import_plugins()
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():
if msg.get("method", None) == "Page.domContentEventFired":
if not await tab.has_global_var("deckyHasLoaded", False):
await self.inject_javascript(tab)
elif 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()
return # We'll catch the next tab in the main loop
await tab.evaluate_js("try{if (window.deckyHasLoaded){setTimeout(() => SteamClient.Browser.RestartJSContext(), 100)}else{window.deckyHasLoaded = true;(async()=>{try{await import('http://localhost:1337/frontend/index.js?v=%s')}catch(e){console.error(e)};})();}}catch(e){console.error(e)}" % (get_loader_version(), ), 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 system and user python paths
sys.path.extend(get_system_pythonpaths())
logger.info(f"Starting Decky version {get_loader_version()}")
loop = new_event_loop()
set_event_loop(loop)
PluginManager(loop).run()
@@ -12,11 +12,14 @@ Some basic migration helpers are available: `migrate_any`, `migrate_settings`, `
A logging facility `logger` is available which writes to the recommended location.
"""
__version__ = '0.1.0'
__version__ = '1.0.0'
import os
import subprocess
import logging
import time
from typing import Any
"""
Constants
@@ -117,7 +120,8 @@ Environment variable: `DECKY_PLUGIN_AUTHOR`.
e.g.: `John Doe`
"""
DECKY_PLUGIN_LOG: str = os.path.join(DECKY_PLUGIN_LOG_DIR, "plugin.log")
__cur_time = time.strftime("%Y-%m-%d %H.%M.%S")
DECKY_PLUGIN_LOG: str = os.path.join(DECKY_PLUGIN_LOG_DIR, f"{__cur_time}.log")
"""
The path to the plugin's main logfile.
Environment variable: `DECKY_PLUGIN_LOG`.
@@ -192,10 +196,30 @@ def migrate_logs(*files_or_directories: str) -> dict[str, str]:
Logging
"""
try:
for x in [entry.name for entry in sorted(os.scandir(DECKY_PLUGIN_LOG_DIR),key=lambda x: x.stat().st_mtime, reverse=True) if entry.name.endswith(".log")][4:]:
os.unlink(os.path.join(DECKY_PLUGIN_LOG_DIR, x))
except Exception as e:
print(f"Failed to delete old logs: {str(e)}")
logging.basicConfig(filename=DECKY_PLUGIN_LOG,
format='[%(asctime)s][%(levelname)s]: %(message)s',
force=True)
logger: logging.Logger = logging.getLogger()
# Also log to stdout
logger.addHandler(logging.StreamHandler())
"""The main plugin logger writing to `DECKY_PLUGIN_LOG`."""
logger.setLevel(logging.INFO)
"""
Event handling
"""
# This is overriden with an actual implementation before being passed to any plugins
# in ../sandboxed_plugin.py 's initialize function
async def emit(event: str, *args: Any) -> None:
"""
Triggers all event listeners in the frontend waiting for `event`, passing the remaining `*args` as the arguments to each listener function.
(Event listeners are set up in the frontend via the `addEventListener` function from `@decky/api`)
"""
pass
@@ -12,10 +12,12 @@ Some basic migration helpers are available: `migrate_any`, `migrate_settings`, `
A logging facility `logger` is available which writes to the recommended location.
"""
__version__ = '0.1.0'
__version__ = '1.0.0'
import logging
from typing import Any
"""
Constants
"""
@@ -171,3 +173,13 @@ Logging
logger: logging.Logger
"""The main plugin logger writing to `DECKY_PLUGIN_LOG`."""
"""
Event handling
"""
async def emit(event: str, *args: Any) -> None:
"""
Triggers all event listeners in the frontend waiting for `event`, passing the remaining `*args` as the arguments to each listener function.
(Event listeners are set up in the frontend via the `addEventListener` function from `@decky/api`)
"""
+36
View File
@@ -0,0 +1,36 @@
from typing import Any, TypedDict
from enum import IntEnum
from uuid import uuid4
from asyncio import Event
class SocketMessageType(IntEnum):
CALL = 0
RESPONSE = 1
EVENT = 2
class SocketResponseDict(TypedDict):
type: SocketMessageType
id: str
success: bool
res: Any
class MethodCallResponse:
def __init__(self, success: bool, result: Any) -> None:
self.success = success
self.result = result
class MethodCallRequest:
def __init__(self) -> None:
self.id = str(uuid4())
self.event = Event()
self.response: MethodCallResponse
def set_result(self, dc: SocketResponseDict):
self.response = MethodCallResponse(dc["success"], dc["res"])
self.event.set()
async def wait_for_result(self):
await self.event.wait()
if not self.response.success:
raise Exception(self.response.result)
return self.response.result
+116
View File
@@ -0,0 +1,116 @@
from asyncio import Task, create_task
from json import dumps, load, loads
from logging import getLogger
from os import path
from multiprocessing import Process
from .sandboxed_plugin import SandboxedPlugin
from .messages import MethodCallRequest, SocketMessageType
from ..enums import PluginLoadType
from ..localplatform.localsocket import LocalSocket
from ..helpers import get_homebrew_path, mkdir_as_user
from typing import Any, Callable, Coroutine, Dict, List
EmittedEventCallbackType = Callable[[str, Any], Coroutine[Any, Any, Any]]
class PluginWrapper:
def __init__(self, file: str, plugin_directory: str, plugin_path: str, emit_callback: EmittedEventCallbackType) -> None:
self.file = file
self.plugin_path = plugin_path
self.plugin_directory = plugin_directory
self.version = None
self.load_type = PluginLoadType.LEGACY_EVAL_IIFE.value
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"]
if ("type" in package_json and package_json["type"] == "module"):
self.load_type = PluginLoadType.ESMODULE_V1.value
self.name = json["name"]
self.author = json["author"]
self.flags = json["flags"]
self.api_version = json["api_version"] if "api_version" in json else 0
self.passive = not path.isfile(self.file)
self.log = getLogger("plugin")
self.sandboxed_plugin = SandboxedPlugin(self.name, self.passive, self.flags, self.file, self.plugin_directory, self.plugin_path, self.version, self.author, self.api_version)
# TODO: Maybe make LocalSocket not require on_new_message to make this cleaner
self._socket = LocalSocket(self.sandboxed_plugin.on_new_message)
self._listener_task: Task[Any]
self._method_call_requests: Dict[str, MethodCallRequest] = {}
self.emitted_event_callback: EmittedEventCallbackType = emit_callback
# TODO enable this after websocket release
self.legacy_method_warning = False
home = get_homebrew_path()
mkdir_as_user(path.join(home, "settings", self.plugin_directory))
# TODO maybe dont chown this?
mkdir_as_user(path.join(home, "data"))
mkdir_as_user(path.join(home, "data", self.plugin_directory))
# TODO maybe dont chown this?
mkdir_as_user(path.join(home, "logs"))
mkdir_as_user(path.join(home, "logs", self.plugin_directory))
def __str__(self) -> str:
return self.name
async def _response_listener(self):
while True:
try:
line = await self._socket.read_single_line()
if line != None:
res = loads(line)
if res["type"] == SocketMessageType.EVENT.value:
create_task(self.emitted_event_callback(res["event"], res["args"]))
elif res["type"] == SocketMessageType.RESPONSE.value:
self._method_call_requests.pop(res["id"]).set_result(res)
except:
pass
async def execute_legacy_method(self, method_name: str, kwargs: Dict[Any, Any]):
if not self.legacy_method_warning:
self.legacy_method_warning = True
self.log.warn(f"Plugin {self.name} is using legacy method calls. This will be removed in a future release.")
if self.passive:
raise RuntimeError("This plugin is passive (aka does not implement main.py)")
request = MethodCallRequest()
await self._socket.get_socket_connection()
await self._socket.write_single_line(dumps({ "type": SocketMessageType.CALL, "method": method_name, "args": kwargs, "id": request.id, "legacy": True }, ensure_ascii=False))
self._method_call_requests[request.id] = request
return await request.wait_for_result()
async def execute_method(self, method_name: str, *args: List[Any]):
if self.passive:
raise RuntimeError("This plugin is passive (aka does not implement main.py)")
request = MethodCallRequest()
await self._socket.get_socket_connection()
await self._socket.write_single_line(dumps({ "type": SocketMessageType.CALL, "method": method_name, "args": args, "id": request.id }, ensure_ascii=False))
self._method_call_requests[request.id] = request
return await request.wait_for_result()
def start(self):
if self.passive:
return self
Process(target=self.sandboxed_plugin.initialize, args=[self._socket]).start()
self._listener_task = create_task(self._response_listener())
return self
async def stop(self, uninstall: bool = False):
if hasattr(self, "_socket"):
await self._socket.write_single_line(dumps({ "stop": True, "uninstall": uninstall }, ensure_ascii=False))
await self._socket.close_socket_connection()
if hasattr(self, "_listener_task"):
self._listener_task.cancel()
@@ -0,0 +1,183 @@
from os import path, environ
from signal import SIGINT, signal
from importlib.util import module_from_spec, spec_from_file_location
from json import dumps, loads
from logging import getLogger
from sys import exit, path as syspath, modules as sysmodules
from traceback import format_exc
from asyncio import (get_event_loop, new_event_loop,
set_event_loop, sleep)
from .messages import SocketResponseDict, SocketMessageType
from ..localplatform.localsocket import LocalSocket
from ..localplatform.localplatform import setgid, setuid, get_username, get_home_path
from ..enums import UserType
from .. import helpers
from typing import List, TypeVar, Any
DataType = TypeVar("DataType")
class SandboxedPlugin:
def __init__(self,
name: str,
passive: bool,
flags: List[str],
file: str,
plugin_directory: str,
plugin_path: str,
version: str|None,
author: str,
api_version: int) -> None:
self.name = name
self.passive = passive
self.flags = flags
self.file = file
self.plugin_path = plugin_path
self.plugin_directory = plugin_directory
self.version = version
self.author = author
self.api_version = api_version
self.log = getLogger("plugin")
def initialize(self, socket: LocalSocket):
self._socket = socket
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)
environ["DECKY_PLUGIN_RUNTIME_DIR"] = path.join(environ["DECKY_HOME"], "data", self.plugin_directory)
environ["DECKY_PLUGIN_LOG_DIR"] = path.join(environ["DECKY_HOME"], "logs", self.plugin_directory)
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 for key in sysmodules if key.startswith("decky_loader.")]
for key in keys:
sysmodules[key.replace("decky_loader.", "")] = sysmodules[key]
from .imports import decky
async def emit(event: str, *args: Any) -> None:
await self._socket.write_single_line_server(dumps({
"type": SocketMessageType.EVENT,
"event": event,
"args": args
}))
# copy the docstring over so we don't have to duplicate it
emit.__doc__ = decky.emit.__doc__
decky.emit = emit
sysmodules["decky"] = decky
# provided for compatibility
sysmodules["decky_plugin"] = decky
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)
# TODO fix self weirdness once plugin.json versioning is done. need this before WS release!
if self.api_version > 0:
self.Plugin = module.Plugin()
else:
self.Plugin = module.Plugin
if hasattr(self.Plugin, "_migration"):
if self.api_version > 0:
get_event_loop().run_until_complete(self.Plugin._migration())
else:
get_event_loop().run_until_complete(self.Plugin._migration(self.Plugin))
if hasattr(self.Plugin, "_main"):
if self.api_version > 0:
get_event_loop().create_task(self.Plugin._main())
else:
get_event_loop().create_task(self.Plugin._main(self.Plugin))
get_event_loop().create_task(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"):
if self.api_version > 0:
await self.Plugin._unload()
else:
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"):
if self.api_version > 0:
await self.Plugin._uninstall()
else:
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")
d: SocketResponseDict = {"type": SocketMessageType.RESPONSE, "res": None, "success": True, "id": data["id"]}
try:
if data.get("legacy"):
if self.api_version > 0:
raise Exception("Legacy methods may not be used on api_version > 0")
# Legacy kwargs
d["res"] = await getattr(self.Plugin, data["method"])(self.Plugin, **data["args"])
else:
if self.api_version < 1 :
raise Exception("api_version 1 or newer is required to call methods with index-based arguments")
# New args
d["res"] = await getattr(self.Plugin, data["method"])(*data["args"])
except Exception as e:
d["res"] = str(e)
d["success"] = False
finally:
return dumps(d, ensure_ascii=False)
@@ -1,13 +1,14 @@
from json import dump, load
from os import mkdir, path, listdir, rename
from localplatform import chown, folder_owner, get_chown_plugin_path
from customtypes import UserType
from typing import Any, Dict
from .localplatform.localplatform import chown, folder_owner, get_chown_plugin_path
from .enums import UserType
from helpers import get_homebrew_path
from .helpers import get_homebrew_path
class SettingsManager:
def __init__(self, name, settings_directory = None) -> None:
def __init__(self, name: str, settings_directory: str | None = None) -> None:
wrong_dir = get_homebrew_path()
if settings_directory == None:
settings_directory = path.join(wrong_dir, "settings")
@@ -31,11 +32,11 @@ class SettingsManager:
if folder_owner(settings_directory) != expected_user:
chown(settings_directory, expected_user, False)
self.settings = {}
self.settings: Dict[str, Any] = {}
try:
open(self.path, "x", encoding="utf-8")
except FileExistsError as e:
except FileExistsError as _:
self.read()
pass
@@ -51,9 +52,9 @@ class SettingsManager:
with open(self.path, "w+", encoding="utf-8") as file:
dump(self.settings, file, indent=4, ensure_ascii=False)
def getSetting(self, key, default=None):
def getSetting(self, key: str, default: Any = None) -> Any:
return self.settings.get(key, default)
def setSetting(self, key, value):
def setSetting(self, key: str, value: Any) -> Any:
self.settings[key] = value
self.commit()
@@ -1,35 +1,46 @@
import os
import shutil
import uuid
from __future__ import annotations
from asyncio import sleep
from ensurepip import version
from json.decoder import JSONDecodeError
from logging import getLogger
import os
from os import getcwd, path, remove
from localplatform import chmod, service_restart, ON_LINUX, get_keep_systemd_service
from typing import TYPE_CHECKING, List, TypedDict
if TYPE_CHECKING:
from .main import PluginManager
from .localplatform.localplatform import chmod, service_restart, service_stop, ON_LINUX, ON_WINDOWS, get_keep_systemd_service, get_selinux
import shutil
from typing import List, TYPE_CHECKING, TypedDict
import zipfile
from aiohttp import ClientSession, web
from aiohttp import ClientSession
from . import helpers
from .injector import get_gamepadui_tab
from .settings import SettingsManager
if TYPE_CHECKING:
from .main import PluginManager
import helpers
from injector import get_gamepadui_tab, inject_to_tab
from settings import SettingsManager
logger = getLogger("Updater")
class RemoteVerAsset(TypedDict):
name: str
browser_download_url: str
class RemoteVer(TypedDict):
tag_name: str
prerelease: bool
assets: List[RemoteVerAsset]
class TestingVersion(TypedDict):
id: int
name: str
link: str
head_sha: str
class Updater:
def __init__(self, context) -> None:
def __init__(self, context: PluginManager) -> None:
self.context = context
self.settings = self.context.settings
# Exposes updater methods to frontend
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
self.remoteVer: RemoteVer | None = None
self.allRemoteVers: List[RemoteVer] = []
self.localVer = helpers.get_loader_version()
try:
@@ -39,27 +50,15 @@ class Updater:
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.ws.add_route("updater/get_version_info", self.get_version_info);
context.ws.add_route("updater/check_for_updates", self.check_for_updates);
context.ws.add_route("updater/do_restart", self.do_restart);
context.ws.add_route("updater/do_shutdown", self.do_shutdown);
context.ws.add_route("updater/do_update", self.do_update);
context.ws.add_route("updater/get_testing_versions", self.get_testing_versions);
context.ws.add_route("updater/download_testing_version", self.download_testing_version);
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)
@@ -92,7 +91,7 @@ class Updater:
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service"
return str(url)
async def get_version(self):
async def get_version_info(self):
return {
"current": self.localVer,
"remote": self.remoteVer,
@@ -104,8 +103,8 @@ class Updater:
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()
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases", headers={'X-GitHub-Api-Version': '2022-11-28'}, 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))
@@ -127,9 +126,8 @@ class Updater:
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()
await self.context.ws.emit("loader/notify_updates")
return await self.get_version_info()
async def version_reloader(self):
await sleep(30)
@@ -140,12 +138,67 @@ class Updater:
pass
await sleep(60 * 60 * 6) # 6 hours
async def download_decky_binary(self, download_url: str, version: str, is_zip: bool = False, size_in_bytes: int | None = None):
download_filename = "PluginLoader" if ON_LINUX else "PluginLoader.exe"
download_temp_filename = download_filename + ".new"
tab = await get_gamepadui_tab()
await tab.open_websocket()
if size_in_bytes == None:
size_in_bytes = 26214400 # 25MiB, a reasonable overestimate (19.6MiB as of 2024/02/25)
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', size_in_bytes))
if total == 0: total = 1
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)
raw += len(c)
new_progress = round((raw / total) * 100)
if progress != new_progress:
self.context.loop.create_task(self.context.ws.emit("updater/update_download_percentage", new_progress))
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 self.context.ws.emit("updater/finish_download")
await tab.close_websocket()
await self.do_restart()
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"
download_temp_filename = download_filename + ".new"
for x in self.remoteVer["assets"]:
if x["name"] == download_filename:
@@ -158,8 +211,6 @@ class Updater:
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:
if ON_LINUX and not get_keep_systemd_service():
logger.debug("Downloading systemd service")
@@ -187,32 +238,56 @@ class Updater:
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))
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)
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))
shutil.move(path.join(getcwd(), download_temp_filename), path.join(getcwd(), download_filename))
chmod(path.join(getcwd(), download_filename), 777, False)
logger.info("Updated loader installation.")
await tab.evaluate_js("window.DeckyUpdater.finish()", False, False)
await self.do_restart()
await tab.close_websocket()
await self.download_decky_binary(download_url, version)
async def do_restart(self):
await service_restart("plugin_loader")
async def do_shutdown(self):
await service_stop("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!
artifact = jresp['artifacts'][0]
down_link = f"https://nightly.link/SteamDeckHomebrew/decky-loader/actions/artifacts/{artifact['id']}.zip"
#Then fetch it and restart itself
await self.download_decky_binary(down_link, f'PR-{pr_id}', is_zip=True, size_in_bytes=artifact.get('size_in_bytes',None))
else:
logger.error("workflow run not found", str(works))
raise Exception("Workflow run not found.")
+452
View File
@@ -0,0 +1,452 @@
from __future__ import annotations
from os import stat_result
import uuid
from urllib.parse import unquote
from json.decoder import JSONDecodeError
from os.path import splitext
import re
from traceback import format_exc
from stat import FILE_ATTRIBUTE_HIDDEN # pyright: ignore [reportAttributeAccessIssue, reportUnknownVariableType]
from asyncio import StreamReader, StreamWriter, start_server, gather, open_connection
from aiohttp import ClientSession
from aiohttp.web import Request, StreamResponse, Response, json_response, post
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.localplatform import ON_WINDOWS
from . import helpers
from .localplatform.localplatform import service_stop, service_start, get_home_path, get_username
class FilePickerObj(TypedDict):
file: Path
filest: stat_result
is_dir: bool
decky_header_regex = re.compile("X-Decky-(.*)")
extra_header_regex = re.compile("X-Decky-Header-(.*)")
excluded_default_headers = ["Host", "Origin", "Sec-Fetch-Site", "Sec-Fetch-Mode", "Sec-Fetch-Dest"]
class Utilities:
def __init__(self, context: PluginManager) -> None:
self.context = context
self.legacy_util_methods: Dict[str, Callable[..., Coroutine[Any, Any, Any]]] = {
"ping": self.ping,
"http_request": self.http_request_legacy,
"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,
"set_setting": self.set_setting,
"get_setting": self.get_setting,
"filepicker_ls": self.filepicker_ls,
"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.ws.add_route("utilities/ping", self.ping)
context.ws.add_route("utilities/settings/get", self.get_setting)
context.ws.add_route("utilities/settings/set", self.set_setting)
context.ws.add_route("utilities/install_plugin", self.install_plugin)
context.ws.add_route("utilities/install_plugins", self.install_plugins)
context.ws.add_route("utilities/cancel_plugin_install", self.cancel_plugin_install)
context.ws.add_route("utilities/confirm_plugin_install", self.confirm_plugin_install)
context.ws.add_route("utilities/uninstall_plugin", self.uninstall_plugin)
context.ws.add_route("utilities/execute_in_tab", self.execute_in_tab)
context.ws.add_route("utilities/inject_css_into_tab", self.inject_css_into_tab)
context.ws.add_route("utilities/remove_css_from_tab", self.remove_css_from_tab)
context.ws.add_route("utilities/allow_remote_debugging", self.allow_remote_debugging)
context.ws.add_route("utilities/disallow_remote_debugging", self.disallow_remote_debugging)
context.ws.add_route("utilities/start_ssh", self.allow_remote_debugging)
context.ws.add_route("utilities/stop_ssh", self.allow_remote_debugging)
context.ws.add_route("utilities/filepicker_ls", self.filepicker_ls)
context.ws.add_route("utilities/disable_rdt", self.disable_rdt)
context.ws.add_route("utilities/enable_rdt", self.enable_rdt)
context.ws.add_route("utilities/get_tab_id", self.get_tab_id)
context.ws.add_route("utilities/get_user_info", self.get_user_info)
context.ws.add_route("utilities/http_request", self.http_request_legacy)
context.ws.add_route("utilities/_call_legacy_utility", self._call_legacy_utility)
context.web_app.add_routes([
post("/methods/{method_name}", self._handle_legacy_server_method_call)
])
for method in ('GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'):
context.web_app.router.add_route(method, "/fetch", self.http_request)
async def _handle_legacy_server_method_call(self, request: Request) -> Response:
method_name = request.match_info["method_name"]
try:
args = await request.json()
except JSONDecodeError:
args = {}
res = {}
try:
r = await self.legacy_util_methods[method_name](**args)
res["result"] = r
res["success"] = True
except Exception as e:
res["result"] = str(e)
res["success"] = False
return json_response(res)
async def _call_legacy_utility(self, method_name: str, kwargs: Dict[Any, Any]) -> Any:
self.logger.debug(f"Calling utility {method_name} with legacy kwargs");
res: Dict[Any, Any] = {}
try:
r = await self.legacy_util_methods[method_name](**kwargs)
res["result"] = r
res["success"] = True
except Exception as e:
res["result"] = str(e)
res["success"] = False
return 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)
# Loosely based on https://gist.github.com/mosquito/4dbfacd51e751827cda7ec9761273e95#file-proxy-py
async def http_request(self, req: Request) -> StreamResponse:
if req.headers.get('X-Decky-Auth', '') != helpers.get_csrf_token() and req.query.get('auth', '') != helpers.get_csrf_token():
return Response(text='Forbidden', status=403)
url = req.headers["X-Decky-Fetch-URL"] if "X-Decky-Fetch-URL" in req.headers else unquote(req.query.get('fetch_url', ''))
self.logger.info(f"Preparing {req.method} request to {url}")
headers = dict(req.headers)
headers["User-Agent"] = helpers.user_agent
for excluded_header in excluded_default_headers:
self.logger.debug(f"Excluding default header {excluded_header}")
if excluded_header in headers:
del headers[excluded_header]
if "X-Decky-Fetch-Excluded-Headers" in req.headers:
for excluded_header in req.headers["X-Decky-Fetch-Excluded-Headers"].split(", "):
self.logger.debug(f"Excluding header {excluded_header}")
if excluded_header in headers:
del headers[excluded_header]
for header in req.headers:
match = extra_header_regex.search(header)
if match:
header_name = match.group(1)
header_value = req.headers[header]
self.logger.debug(f"Adding extra header {header_name}: {header_value}")
headers[header_name] = header_value
for header in list(headers.keys()):
match = decky_header_regex.search(header)
if match:
self.logger.debug(f"Removing decky header {header} from request")
del headers[header]
self.logger.debug(f"Final request headers: {headers}")
body = await req.read() # TODO can this also be streamed?
async with ClientSession() as web:
async with web.request(req.method, url, headers=headers, data=body, ssl=helpers.get_ssl_context()) as web_res:
res = StreamResponse(headers=web_res.headers, status=web_res.status)
if web_res.headers.get('Transfer-Encoding', '').lower() == 'chunked':
res.enable_chunked_encoding()
await res.prepare(req)
self.logger.debug(f"Starting stream for {url}")
async for data in web_res.content.iter_any():
await res.write(data)
if data:
await res.drain()
self.logger.debug(f"Finished stream for {url}")
return res
async def http_request_legacy(self, method: str, url: str, extra_opts: Any = {}):
async with ClientSession() as web:
res = await web.request(method, url, ssl=helpers.get_ssl_context(), **extra_opts)
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) -> str:
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)
assert result is not None # TODO remove this once it has proper typings
if "exceptionDetails" in result["result"]:
raise result["result"]["exceptionDetails"]
return css_id
async def remove_css_from_tab(self, tab: str, css_id: str):
result = await inject_to_tab(tab,
f"""
(function() {{
let style = document.getElementById("{css_id}");
if (style.nodeName.toLowerCase() == 'style')
style.parentNode.removeChild(style);
}})()
""", False)
assert result
if "exceptionDetails" in result["result"]:
raise result["result"]["exceptionDetails"]
return
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 start_ssh(self):
await service_start(helpers.SSHD_UNIT)
return True
async def stop_ssh(self):
await service_stop(helpers.SSHD_UNIT)
return True
async def filepicker_ls(self,
path: str | None = None,
include_files: bool = True,
include_folders: bool = True,
include_ext: list[str] | None = None,
include_hidden: bool = False,
order_by: str = "name_desc",
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 include_ext == None or 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("location.reload();", 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
+135
View File
@@ -0,0 +1,135 @@
from logging import getLogger
from asyncio import AbstractEventLoop, create_task
from aiohttp import WSMsgType, WSMessage
from aiohttp.web import Application, WebSocketResponse, Request, Response, get
from enum import IntEnum
from typing import Callable, Coroutine, Dict, Any, cast, TypeVar
from traceback import format_exc
from .helpers import get_csrf_token
class MessageType(IntEnum):
ERROR = -1
# Call-reply, Frontend -> Backend -> Frontend
CALL = 0
REPLY = 1
# Pub/Sub, Backend -> Frontend
EVENT = 3
# WSMessage with slightly better typings
class WSMessageExtra(WSMessage):
# TODO message typings here too
data: Any # pyright: ignore [reportIncompatibleVariableOverride]
type: WSMsgType # pyright: ignore [reportIncompatibleVariableOverride]
# see wsrouter.ts for typings
DataType = TypeVar("DataType")
Route = Callable[..., Coroutine[Any, Any, Any]]
class WSRouter:
def __init__(self, loop: AbstractEventLoop, server_instance: Application) -> None:
self.loop = loop
self.ws: WebSocketResponse | None = None
self.instance_id = 0
self.routes: Dict[str, Route] = {}
# self.subscriptions: Dict[str, Callable[[Any]]] = {}
self.logger = getLogger("WSRouter")
server_instance.add_routes([
get("/ws", self.handle)
])
async def write(self, data: Dict[str, Any]):
if self.ws != None:
await self.ws.send_json(data)
else:
self.logger.warn("Dropping message as there is no connected socket: %s", data)
def add_route(self, name: str, route: Route):
self.routes[name] = route
def remove_route(self, name: str):
del self.routes[name]
async def _call_route(self, route: str, args: ..., call_id: int):
instance_id = self.instance_id
error = None
try:
res = await self.routes[route](*args)
except Exception as err:
error = {"name":err.__class__.__name__, "message":str(err), "traceback":format_exc()}
res = None
if instance_id != self.instance_id:
try:
self.logger.warn("Ignoring %s reply from stale instance %d with args %s and response %s", route, instance_id, args, res)
except:
self.logger.warn("Ignoring %s reply from stale instance %d (failed to log event data)", route, instance_id)
finally:
return
if error:
await self.write({"type": MessageType.ERROR.value, "id": call_id, "error": error})
else:
await self.write({"type": MessageType.REPLY.value, "id": call_id, "result": res})
async def handle(self, request: Request):
# Auth is a query param as JS WebSocket doesn't support headers
if request.rel_url.query["auth"] != get_csrf_token():
return Response(text='Forbidden', status=403)
self.logger.debug('Websocket connection starting')
ws = WebSocketResponse()
await ws.prepare(request)
self.instance_id += 1
self.logger.debug('Websocket connection ready')
if self.ws != None:
try:
await self.ws.close()
except:
pass
self.ws = None
self.ws = ws
try:
async for msg in ws:
msg = cast(WSMessageExtra, msg)
if msg.type == WSMsgType.TEXT:
if msg.data == 'close':
# TODO DO NOT RELY ON THIS!
break
else:
data = msg.json()
match data["type"]:
case MessageType.CALL.value:
# do stuff with the message
if data["route"] in self.routes:
self.logger.debug(f'Started PY call {data["route"]} ID {data["id"]}')
create_task(self._call_route(data["route"], data["args"], data["id"]))
else:
error = {"error":f'Route {data["route"]} does not exist.', "name": "RouteNotFoundError", "traceback": None}
create_task(self.write({"type": MessageType.ERROR.value, "id": data["id"], "error": error}))
case _:
self.logger.error("Unknown message type", data)
finally:
try:
await ws.close()
self.ws = None
except:
pass
self.logger.debug('Websocket connection closed')
return ws
async def emit(self, event: str, *args: Any):
self.logger.debug(f'Firing frontend event {event} with args {args}')
await self.write({ "type": MessageType.EVENT.value, "event": event, "args": args })
-84
View File
@@ -1,84 +0,0 @@
class PluginEventTarget extends EventTarget { }
method_call_ev_target = new PluginEventTarget();
window.addEventListener("message", function(evt) {
let ev = new Event(evt.data.call_id);
ev.data = evt.data.result;
method_call_ev_target.dispatchEvent(ev);
}, false);
async function call_server_method(method_name, arg_object={}) {
const token = await fetch("http://127.0.0.1:1337/auth/token").then(r => r.text());
const response = await fetch(`http://127.0.0.1:1337/methods/${method_name}`, {
method: 'POST',
credentials: "include",
headers: {
'Content-Type': 'application/json',
Authentication: token
},
body: JSON.stringify(arg_object),
});
const dta = await response.json();
if (!dta.success) throw dta.result;
return dta.result;
}
// Source: https://stackoverflow.com/a/2117523 Thanks!
function uuidv4() {
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}
async function fetch_nocors(url, request={}) {
let args = { method: "POST", headers: {}, body: "" };
request = {...args, ...request};
request.url = url;
request.data = request.body;
delete request.body; //maintain api-compatibility with fetch
return await call_server_method("http_request", request);
}
async function call_plugin_method(method_name, arg_object={}) {
if (plugin_name == undefined)
throw new Error("Plugin methods can only be called from inside plugins (duh)");
const token = await fetch("http://127.0.0.1:1337/auth/token").then(r => r.text());
const response = await fetch(`http://127.0.0.1:1337/plugins/${plugin_name}/methods/${method_name}`, {
method: 'POST',
credentials: "include",
headers: {
'Content-Type': 'application/json',
Authentication: token
},
body: JSON.stringify({
args: arg_object,
}),
});
const dta = await response.json();
if (!dta.success) throw dta.result;
return dta.result;
}
async function execute_in_tab(tab, run_async, code) {
return await call_server_method("execute_in_tab", {
'tab': tab,
'run_async': run_async,
'code': code
});
}
async function inject_css_into_tab(tab, style) {
return await call_server_method("inject_css_into_tab", {
'tab': tab,
'style': style
});
}
async function remove_css_from_tab(tab, css_id) {
return await call_server_method("remove_css_from_tab", {
'tab': tab,
'css_id': css_id
});
}
+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 ثواني"
}
}
+61 -9
View File
@@ -14,7 +14,27 @@
},
"FilePickerIndex": {
"folder": {
"select": "Použít tuto složku"
"select": "Použít tuto složku",
"label": "Složka",
"show_more": "Zobrazit více souborů"
},
"filter": {
"created_asce": "Vytvořeno (Nejstarší)",
"created_desc": "Vytvořeno (Nejnovější)",
"modified_asce": "Upraveno (Nejstarší)",
"modified_desc": "Upraveno (Nejnovější)",
"name_asce": "Z-A",
"name_desc": "A-Z",
"size_asce": "Velikost (Nejmenší)",
"size_desc": "Velikost (Největší)"
},
"files": {
"show_hidden": "Zobrazit skryté soubory",
"all_files": "Všechny soubory",
"file_type": "Typ souboru"
},
"file": {
"select": "Vybrat tento soubor"
}
},
"PluginView": {
@@ -54,7 +74,7 @@
},
"MultiplePluginsInstallModal": {
"title": {
"mixed_one": "Upravit 1 plugin",
"mixed_one": "Upravit {{count}} plugin",
"mixed_few": "Upravit {{count}} pluginů",
"mixed_other": "Upravit {{count}} pluginů",
"reinstall_one": "Přeinstalovat 1 plugin",
@@ -89,7 +109,9 @@
"hide": "Rychlý přístup: Skrýt",
"update_all_one": "Aktualizovat 1 plugin",
"update_all_few": "Aktualizovat {{count}} pluginů",
"update_all_other": "Aktualizovat {{count}} pluginů"
"update_all_other": "Aktualizovat {{count}} pluginů",
"freeze": "Pozastavit aktualizace",
"unfreeze": "Povolit aktualizace"
},
"PluginLoader": {
"decky_title": "Decky",
@@ -129,9 +151,6 @@
"label_url": "Instalovat plugin z URL",
"label_zip": "Instalovat plugin ze ZIP souboru"
},
"toast_zip": {
"title": "Decky"
},
"valve_internal": {
"desc1": "Zapíná interní vývojářské menu Valve.",
"desc2": "Nedotýkejte se ničeho v této nabídce, pokud nevíte, co děláte.",
@@ -160,12 +179,18 @@
},
"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"
"plugins_title": "Pluginy",
"testing_title": "Testování"
},
"Store": {
"store_contrib": {
@@ -191,9 +216,17 @@
"about": "O Decky Plugin Store",
"alph_asce": "Abecedně (Z do A)",
"alph_desc": "Abecedně (A do Z)",
"title": "Procházet"
"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_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": {
@@ -221,5 +254,24 @@
"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"
}
}
+95 -23
View File
@@ -8,13 +8,33 @@
}
},
"Developer": {
"disabling": "Deaktiviere",
"enabling": "Aktiviere",
"disabling": "Deaktiviere React DevTools",
"enabling": "Aktiviere React DevTools",
"5secreload": "Neu laden in 5 Sekunden"
},
"FilePickerIndex": {
"folder": {
"select": "Diesen Ordner verwenden"
"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": {
@@ -51,8 +71,12 @@
"reload": "Neu laden",
"uninstall": "Deinstallieren",
"update_to": "Aktualisieren zu {{name}}",
"update_all_one": "",
"update_all_other": ""
"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",
@@ -92,13 +116,15 @@
"button_install": "Installieren",
"label_url": "Installiere Erweiterung via URL"
},
"toast_zip": {
"title": "Decky"
},
"valve_internal": {
"desc2": "Fasse in diesem Menü nichts an, es sei denn, du weißt was du tust.",
"label": "Aktiviere Valve-internes Menü",
"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": {
@@ -117,12 +143,18 @@
},
"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"
"plugins_title": "Erweiterungen",
"testing_title": "Testen"
},
"Store": {
"store_contrib": {
@@ -148,19 +180,27 @@
"about": "Über",
"alph_asce": "Alphabetisch (Z zu A)",
"alph_desc": "Alphabetisch (A zu Z)",
"title": "Durchstöbern"
"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_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": "Benutzerdefinierter Marktplatz",
"label": "Benutzerdefiniertes Store",
"url_label": "URL"
},
"store_channel": {
"custom": "Benutzerdefiniert",
"default": "Standard",
"label": "Marktplatz Kanal",
"label": "Store Kanal",
"testing": "Test"
}
},
@@ -180,19 +220,51 @@
"no_patch_notes_desc": "Für diese Version gibt es keine Patchnotizen"
},
"PluginView": {
"hidden_one": "",
"hidden_other": ""
"hidden_one": "{{count}} Plugin ist in dieser Liste ausgeblendet",
"hidden_other": "{{count}} Plugins sind in dieser Liste ausgeblendet"
},
"MultiplePluginsInstallModal": {
"title": {
"install_one": "",
"install_other": "",
"mixed_one": "",
"mixed_other": "",
"update_one": "",
"update_other": "",
"reinstall_one": "",
"reinstall_other": ""
"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"
}
}
+87 -24
View File
@@ -13,14 +13,17 @@
"label_url": "Εγκατάσταση επέκτασης απο URL",
"label_zip": "Εγκατάσταση επέκτασης από αρχείο ZIP"
},
"toast_zip": {
"body": "Η εγκατάσταση απέτυχε. Μόνο αρχεία ZIP επιτρέπονται."
},
"valve_internal": {
"desc1": "Ενεργοποιεί το μενού προγραμματιστή της Valve.",
"desc2": "Μην αγγίξετε τίποτα σε αυτό το μενού εκτός και αν ξέρετε τι κάνει.",
"label": "Ενεργοποιήση εσωτερικού μενού Valve"
}
},
"cef_console": {
"button": "Άνοιγμα Κονσόλας",
"desc": "Ανοίγει την Κονσόλα CEF. Χρήσιμο μόνο για εντοπισμό σφαλμάτων. Τα πράγματα εδώ είναι δυνητικά επικίνδυνα και θα πρέπει να χρησιμοποιηθεί μόνο εάν είστε προγραμματιστής επεκτάσεων, ή κατευθυνθήκατε εδώ από έναν προγραμματιστή.",
"label": "Κονσόλα CEF"
},
"header": "Άλλα"
},
"BranchSelect": {
"update_channel": {
@@ -32,8 +35,8 @@
},
"Developer": {
"5secreload": "Γίνεται επαναφόρτωση σε 5 δευτερόλεπτα",
"disabling": "Γίνεται απενεργοποίηση",
"enabling": "Γίνεται ενεργοποίηση"
"disabling": "Γίνεται απενεργοποίηση των React DevTools",
"enabling": "Γίνεται ενεργοποίηση των React DevTools"
},
"PluginCard": {
"plugin_no_desc": "Δεν υπάρχει περιγραφή.",
@@ -69,14 +72,16 @@
"reload": "Επαναφόρτωση",
"uninstall": "Απεγκατάσταση",
"update_to": "Ενημέρωση σε {{name}}",
"update_all_one": "",
"update_all_other": ""
"update_all_one": "Ενημέρωση 1 επέκτασης",
"update_all_other": "Ενημέρωση {{count}} επεκτάσεων",
"show": "Γρήγορη πρόσβαση: Εμφάνιση",
"hide": "Γρήγορη πρόσβαση: Απόκρυψη"
},
"PluginLoader": {
"decky_title": "Decky",
"decky_update_available": "Ενημέρωση σε {{tag_name}} διαθέσιμη!",
"error": "Σφάλμα",
"plugin_error_uninstall": "Πηγαίντε στο <0></0> στο μενού του Decky για να απεγκαταστήσετε αυτή την επέκταση.",
"plugin_error_uninstall": "Η φόρτωση του {{name}} προκάλεσε το παραπάνω σφάλμα. Αυτό συνήθως σημαίνει ότι η επέκταση απαιτεί ενημέρωση για τη νέα έκδοση του SteamUI. Ελέγξτε εάν υπάρχει ενημέρωση ή αξιολογήστε την απεγκαταστήσετε της επέκτασης στις ρυθμίσεις του Decky, στην ενότητα Επεκτάσεις.",
"plugin_load_error": {
"message": "Σφάλμα στη φόρτωση της επέκτασης {{name}}",
"toast": "Σφάλμα φόρτωσης {{name}}"
@@ -86,8 +91,8 @@
"desc": "Σίγουρα θέλετε να απεγκαταστήσετε το {{name}};",
"title": "Απεγκατάσταση {{name}}"
},
"plugin_update_one": "",
"plugin_update_other": ""
"plugin_update_one": "Διαθέσιμη ενημέρωση για 1 επέκταση!",
"plugin_update_other": "Διαθέσιμες ενημερώσεις για {{count}} επεκτάσεις!"
},
"RemoteDebugging": {
"remote_cef": {
@@ -111,6 +116,11 @@
},
"beta": {
"header": "Συμμετοχή στη Beta"
},
"notifications": {
"decky_updates_label": "Διαθέσιμη ενημέρωση του Decky",
"header": "Ειδοποιήσεις",
"plugin_updates_label": "Διαθέσιμες ενημερώσεις επεκτάσεων"
}
},
"SettingsIndex": {
@@ -121,7 +131,7 @@
"Store": {
"store_contrib": {
"label": "Συνεισφέροντας",
"desc": "Αν θέλετε να συνεισφέρετε στο κατάστημα επεκτάσεων του Decky, τσεκάρετε το SteamDeckHomebrew/decky-plugin-template repository στο GitHub. Πληροφοριές σχετικά με τη δημιουργία και τη διανομή επεκτάσεων είναι διαθέσιμες στο README."
"desc": "Αν θέλετε να συνεισφέρετε στο κατάστημα επεκτάσεων του Decky, τσεκάρετε το SteamDeckHomebrew/decky-plugin-template repository στο GitHub. Πληροφορίες σχετικά με τη δημιουργία και τη διανομή επεκτάσεων είναι διαθέσιμες στο README."
},
"store_filter": {
"label": "Φίλτρο",
@@ -144,7 +154,11 @@
"alph_desc": "Αλφαβητικά (Α σε Ζ)",
"title": "Περιήγηση"
},
"store_testing_cta": "Παρακαλώ σκεφτείτε να τεστάρετε νέες επεκτάσεις για να βοηθήσετε την ομάδα του Decky Loader!"
"store_testing_cta": "Παρακαλώ σκεφτείτε να τεστάρετε νέες επεκτάσεις για να βοηθήσετε την ομάδα του Decky Loader!",
"store_testing_warning": {
"desc": "Μπορείτε να χρησιμοποιήσετε αυτό το κανάλι του καταστήματος για να δοκιμάσετε τις νεότερες εκδόσεις των επεκτάσεων. Φροντίστε να αφήσετε σχόλια στο GitHub, ώστε να βοηθήσετε στην ενημέρωση της εκάστοτε επέκταση για όλους τους χρήστες.",
"label": "Καλώς ήρθατε στο Δοκιμαστικό Κανάλι τους Καταστήματος"
}
},
"StoreSelect": {
"custom_store": {
@@ -175,23 +189,72 @@
},
"FilePickerIndex": {
"folder": {
"select": "Χρησιμοποιήστε αυτό το φάκελο"
"select": "Χρησιμοποιήστε αυτό το φάκελο",
"label": "Φάκελος",
"show_more": "Εμφάνιση περισσότερων αρχείων"
},
"filter": {
"modified_asce": "Τροποποιήθηκε (Παλαιότερο)",
"modified_desc": "Τροποποιήθηκε (Νεότερο)",
"created_desc": "Δημιουργήθηκε (Νεότερο)",
"name_asce": "Z-A",
"name_desc": "A-Z",
"created_asce": "Δημιουργήθηκε (Παλαιότερο)",
"size_asce": "Μέγεθος (Μικρότερο)",
"size_desc": "Μέγεθος (Μεγαλύτερο)"
},
"file": {
"select": "Επιλογή αυτού του αρχείου"
},
"files": {
"show_hidden": "Εμφάνιση Κρυφών Αρχείων",
"all_files": "Όλα Τα Αρχεία",
"file_type": "Τύπος Αρχείου"
}
},
"PluginView": {
"hidden_one": "",
"hidden_other": ""
"hidden_one": "1 επέκταση είναι κρυμμένη σε αυτήν τη λίστα",
"hidden_other": "{{count}} επεκτάσεις είναι κρυμμένες σε αυτήν τη λίστα"
},
"MultiplePluginsInstallModal": {
"title": {
"mixed_one": "",
"mixed_other": "",
"update_one": "",
"update_other": "",
"reinstall_one": "",
"reinstall_other": "",
"install_one": "",
"install_other": ""
"mixed_one": "Τροποποίηση 1 επέκτασης",
"mixed_other": "Τροποποίηση {{count}} επεκτάσεων",
"update_one": "Ενημέρωση 1 επέκτασης",
"update_other": "Ενημέρωση {{count}} επεκτάσεων",
"reinstall_one": "Επανεγκατάσταση 1 επέκτασης",
"reinstall_other": "Επανεγκατάσταση {{count}} επεκτάσεων",
"install_one": "Εγκατάσταση 1 επέκτασης",
"install_other": "Εγκατάσταση {{count}} επεκτάσεων"
},
"confirm": "Είστε βέβαιοι ότι θέλετε να κάνετε τις ακόλουθες τροποποιήσεις;",
"description": {
"reinstall": "Επανεγκατάσταση {{name}} {{version}}",
"update": "Ενημέρωση {{name}} to {{version}}",
"install": "Εγκατάσταση {{name}} {{version}}"
},
"ok_button": {
"idle": "Επιβεβαίωση",
"loading": "Φόρτωση"
}
},
"PluginListLabel": {
"hidden": "Κρυφό στο μενού γρήγορης πρόσβασης"
},
"TitleView": {
"settings_desc": "Άνοιγμα Ρυθμίσεων Decky",
"decky_store_desc": "Άνοιγμα Καταστήματος Decky"
},
"DropdownMultiselect": {
"button": {
"back": "Πίσω"
}
},
"FilePickerError": {
"errors": {
"file_not_found": "Η καθορισμένη διαδρομή δεν είναι έγκυρη. Παρακαλούμε ελέγξτε τη και εισάγετέ τη ξανά σωστά.",
"perm_denied": "Δεν έχετε πρόσβαση στην καθορισμένη διαδρομή. Ελέγξτε εάν ο χρήστης σας (deck στο Steam Deck) έχει τα αντίστοιχα δικαιώματα πρόσβασης στον καθορισμένο φάκελο/αρχείο.",
"unknown": "Παρουσιάστηκε άγνωστο σφάλμα. Το σφάλμα είναι: {{raw_error}}"
}
}
}
+280 -249
View File
@@ -1,252 +1,283 @@
{
"BranchSelect": {
"update_channel": {
"label": "Update Channel",
"prerelease": "Prerelease",
"stable": "Stable",
"testing": "Testing"
"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"
},
"download_progress_info": {
"start": "Initializing",
"open_zip": "Opening zip file",
"download_zip": "Downloading plugin",
"increment_count": "Incrementing download count",
"parse_zip": "Parsing zip file",
"uninstalling_previous": "Uninstalling previous copy",
"installing_plugin": "Installing plugin"
}
},
"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",
"header": "The following versions of Decky Loader are built from open third-party Pull Requests. The Decky Loader team has not verified their functionality or security, and they may be outdated.",
"loading": "Loading open Pull Requests...",
"error": "Error Installing PR",
"start_download_toast": "Downloading PR #{{id}}"
}
},
"Developer": {
"5secreload": "Reloading in 5 seconds",
"disabling": "Disabling React DevTools",
"enabling": "Enabling React DevTools"
},
"DropdownMultiselect": {
"button": {
"back": "Back"
}
},
"FilePickerError": {
"errors": {
"file_not_found": "The path specified is not valid. Please check it and reenter it correctly.",
"perm_denied": "You do not have access to the specified directory. Please check if your user (deck on Steam Deck) has the corresponding permission to access the specified folder/file.",
"unknown": "An unknown error occurred. The raw error is: {{raw_error}}"
}
},
"FilePickerIndex": {
"file": {
"select": "Select this file"
},
"files": {
"all_files": "All Files",
"file_type": "File Type",
"show_hidden": "Show Hidden Files"
},
"filter": {
"created_asce": "Created (Oldest)",
"created_desc": "Created (Newest)",
"modified_asce": "Modified (Oldest)",
"modified_desc": "Modified (Newest)",
"name_asce": "Z-A",
"name_desc": "A-Z",
"size_asce": "Size (Smallest)",
"size_desc": "Size (Largest)"
},
"folder": {
"label": "Folder",
"select": "Use this folder",
"show_more": "Show more files"
}
},
"MultiplePluginsInstallModal": {
"confirm": "Are you sure you want to make the following modifications?",
"description": {
"install": "Install {{name}} {{version}}",
"reinstall": "Reinstall {{name}} {{version}}",
"update": "Update {{name}} to {{version}}"
},
"ok_button": {
"idle": "Confirm",
"loading": "Working"
},
"title": {
"install_one": "Install 1 plugin",
"install_other": "Install {{count}} plugins",
"mixed_one": "Modify {{count}} plugin",
"mixed_other": "Modify {{count}} plugins",
"reinstall_one": "Reinstall 1 plugin",
"reinstall_other": "Reinstall {{count}} plugins",
"update_one": "Update 1 plugin",
"update_other": "Update {{count}} plugins"
}
},
"PluginCard": {
"plugin_full_access": "This plugin has full access to your Steam Deck.",
"plugin_install": "Install",
"plugin_no_desc": "No description provided.",
"plugin_version_label": "Plugin Version"
},
"PluginInstallModal": {
"install": {
"button_idle": "Install",
"button_processing": "Installing",
"desc": "Are you sure you want to install {{artifact}} {{version}}?",
"title": "Install {{artifact}}"
},
"no_hash": "This plugin does not have a hash, you are installing it at your own risk.",
"reinstall": {
"button_idle": "Reinstall",
"button_processing": "Reinstalling",
"desc": "Are you sure you want to reinstall {{artifact}} {{version}}?",
"title": "Reinstall {{artifact}}"
},
"update": {
"button_idle": "Update",
"button_processing": "Updating",
"desc": "Are you sure you want to update {{artifact}} {{version}}?",
"title": "Update {{artifact}}"
}
},
"PluginListIndex": {
"hide": "Quick access: Hide",
"no_plugin": "No plugins installed!",
"plugin_actions": "Plugin Actions",
"reinstall": "Reinstall",
"reload": "Reload",
"show": "Quick access: Show",
"uninstall": "Uninstall",
"update_all_one": "Update 1 plugin",
"update_all_other": "Update {{count}} plugins",
"update_to": "Update to {{name}}"
},
"PluginListLabel": {
"hidden": "Hidden from the quick access menu"
},
"PluginLoader": {
"decky_title": "Decky",
"decky_update_available": "Update to {{tag_name}} available!",
"error": "Error",
"plugin_error_uninstall": "Loading {{name}} caused an exception as shown above. This usually means that the plugin requires an update for the new version of SteamUI. Check if an update is present or evaluate its removal in the Decky settings, in the Plugins section.",
"plugin_load_error": {
"message": "Error loading plugin {{name}}",
"toast": "Error loading {{name}}"
},
"plugin_uninstall": {
"button": "Uninstall",
"desc": "Are you sure you want to uninstall {{name}}?",
"title": "Uninstall {{name}}"
},
"plugin_update_one": "Updates available for 1 plugin!",
"plugin_update_other": "Updates available for {{count}} plugins!"
},
"PluginView": {
"hidden_one": "1 plugin is hidden from this list",
"hidden_other": "{{count}} plugins are hidden from this list"
},
"RemoteDebugging": {
"remote_cef": {
"desc": "Allow unauthenticated access to the CEF debugger to anyone in your network",
"label": "Allow Remote CEF Debugging"
}
},
"SettingsDeveloperIndex": {
"cef_console": {
"button": "Open Console",
"desc": "Opens the CEF Console. Only useful for debugging purposes. Stuff here is potentially dangerous and should only be used if you are a plugin dev, or are directed here by one.",
"label": "CEF Console"
},
"header": "Other",
"react_devtools": {
"desc": "Enables connection to a computer running React DevTools. Changing this setting will reload Steam. Set the IP address before enabling.",
"ip_label": "IP",
"label": "Enable React DevTools"
},
"third_party_plugins": {
"button_install": "Install",
"button_zip": "Browse",
"header": "Third-Party Plugins",
"label_desc": "URL",
"label_url": "Install Plugin from URL",
"label_zip": "Install Plugin from ZIP File"
},
"valve_internal": {
"desc1": "Enables the Valve internal developer menu.",
"desc2": "Do not touch anything in this menu unless you know what it does.",
"label": "Enable Valve Internal"
}
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Decky Version",
"header": "About"
},
"beta": {
"header": "Beta participation"
},
"developer_mode": {
"label": "Developer mode"
},
"notifications": {
"decky_updates_label": "Decky update available",
"header": "Notifications",
"plugin_updates_label": "Plugin updates available"
},
"other": {
"header": "Other"
},
"updates": {
"header": "Updates"
}
},
"SettingsIndex": {
"developer_title": "Developer",
"general_title": "General",
"plugins_title": "Plugins"
},
"Store": {
"store_contrib": {
"desc": "If you would like to contribute to the Decky Plugin Store, check the SteamDeckHomebrew/decky-plugin-template repository on GitHub. Information on development and distribution is available in the README.",
"label": "Contributing"
},
"store_filter": {
"label": "Filter",
"label_def": "All"
},
"store_search": {
"label": "Search"
},
"store_sort": {
"label": "Sort",
"label_def": "Last Updated (Newest)"
},
"store_source": {
"desc": "All plugin source code is available on SteamDeckHomebrew/decky-plugin-database repository on GitHub.",
"label": "Source Code"
},
"store_tabs": {
"about": "About",
"alph_asce": "Alphabetical (Z to A)",
"alph_desc": "Alphabetical (A to Z)",
"title": "Browse"
},
"store_testing_cta": "Please consider testing new plugins to help the Decky Loader team!"
},
"StoreSelect": {
"custom_store": {
"label": "Custom Store",
"url_label": "URL"
},
"store_channel": {
"custom": "Custom",
"default": "Default",
"label": "Store Channel",
"testing": "Testing"
}
},
"Updater": {
"decky_updates": "Decky Updates",
"no_patch_notes_desc": "no patch notes for this version",
"patch_notes_desc": "Patch Notes",
"updates": {
"check_button": "Check For Updates",
"checking": "Checking",
"cur_version": "Current version: {{ver}}",
"install_button": "Install Update",
"label": "Updates",
"lat_version": "Up to date: running {{ver}}",
"reloading": "Reloading",
"updating": "Updating"
}
}
}
-3
View File
@@ -13,9 +13,6 @@
"label": "Activar menú interno de Valve",
"desc1": "Activa el menú interno de desarrollo de Valve."
},
"toast_zip": {
"body": "¡Ha fallado la instalación! Solo se permiten archivos ZIP."
},
"cef_console": {
"button": "Abrir consola",
"label": "Consola CEF",
+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"
}
}
+104 -31
View File
@@ -13,14 +13,17 @@
"label_url": "Installer le plugin à partir d'un URL",
"label_zip": "Installer le plugin à partir d'un fichier ZIP"
},
"toast_zip": {
"title": "Decky"
},
"valve_internal": {
"desc1": "Active le menu développeur interne de Valve.",
"desc2": "Ne touchez à rien dans ce menu à moins que vous ne sachiez ce qu'il fait.",
"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": {
@@ -59,12 +62,32 @@
},
"Developer": {
"5secreload": "Rechargement dans 5 secondes",
"disabling": "Désactivation",
"enabling": "Activation"
"disabling": "Désactivation des React DevTools",
"enabling": "Activation des React DevTools"
},
"FilePickerIndex": {
"folder": {
"select": "Utiliser ce dossier"
"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": {
@@ -101,9 +124,13 @@
"uninstall": "Désinstaller",
"update_to": "Mettre à jour vers {{name}}",
"no_plugin": "Aucun plugin installé !",
"update_all_one": "",
"update_all_many": "",
"update_all_other": ""
"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",
@@ -119,9 +146,9 @@
"title": "Désinstaller {{name}}",
"desc": "Êtes-vous sûr.e de vouloir désinstaller {{name}} ?"
},
"plugin_update_one": "",
"plugin_update_many": "",
"plugin_update_other": ""
"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": {
@@ -145,12 +172,18 @@
},
"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"
"plugins_title": "Plugins",
"testing_title": "Essai"
},
"Store": {
"store_contrib": {
@@ -176,29 +209,69 @@
"about": "À propos",
"alph_asce": "Alphabétique (Z à A)",
"alph_desc": "Alphabétique (A à Z)",
"title": "Explorer"
"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_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": "",
"hidden_many": "",
"hidden_other": ""
"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": "",
"reinstall_many": "",
"reinstall_other": "",
"install_one": "",
"install_many": "",
"install_other": "",
"mixed_one": "",
"mixed_many": "",
"mixed_other": "",
"update_one": "",
"update_many": "",
"update_other": ""
"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"
}
}
+22 -7
View File
@@ -113,7 +113,9 @@
"update_all_one": "Aggiorna un plugin",
"update_all_many": "Aggiorna {{count}} plugins",
"update_all_other": "Aggiorna {{count}} plugins",
"update_to": "Aggiorna a {{name}}"
"update_to": "Aggiorna a {{name}}",
"unfreeze": "Permetti aggiornamenti",
"freeze": "Congela aggiornamenti"
},
"PluginListLabel": {
"hidden": "Nascosto dal menu di accesso rapido"
@@ -141,9 +143,6 @@
"hidden_many": "Sono nascosti {{count}} plugin dalla lista",
"hidden_other": "Sono nascosti {{count}} plugin dalla lista"
},
"notifications": {
"plugin_updates_label": "Aggiornamenti dei plugins"
},
"RemoteDebugging": {
"remote_cef": {
"desc": "Permetti l'accesso non autenticato al debugger di CEF da tutti gli indirizzi sulla tua rete locale",
@@ -202,7 +201,8 @@
"SettingsIndex": {
"developer_title": "Sviluppatore",
"general_title": "Generali",
"plugins_title": "Plugins"
"plugins_title": "Plugins",
"testing_title": "Testing"
},
"Store": {
"store_contrib": {
@@ -228,9 +228,17 @@
"about": "Riguardo a",
"alph_asce": "Alfabetico (Z a A)",
"alph_desc": "Alfabetico (A a Z)",
"title": "Sfoglia"
"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_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": {
@@ -258,5 +266,12 @@
"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デバッガへの非認証アクセスを許可します"
}
}
}
+26 -8
View File
@@ -2,9 +2,9 @@
"BranchSelect": {
"update_channel": {
"label": "업데이트 배포 채널",
"stable": "안정",
"testing": "시험판",
"prerelease": "사전 출시"
"stable": "안정",
"testing": "테스트",
"prerelease": "사전 출시"
}
},
"Developer": {
@@ -97,7 +97,9 @@
"hide": "빠른 액세스 메뉴: 숨김",
"update_all_other": "플러그인 {{count}}개 업데이트",
"no_plugin": "설치된 플러그인이 없습니다!",
"update_to": "{{name}}(으)로 업데이트"
"update_to": "{{name}}(으)로 업데이트",
"freeze": "업데이트 일시 중지",
"unfreeze": "업데이트 허용"
},
"PluginLoader": {
"decky_title": "Decky",
@@ -173,7 +175,8 @@
"SettingsIndex": {
"developer_title": "개발자",
"general_title": "일반",
"plugins_title": "플러그인"
"plugins_title": "플러그인",
"testing_title": "테스트"
},
"Store": {
"store_contrib": {
@@ -199,9 +202,17 @@
"about": "정보",
"alph_asce": "알파벳순 (Z-A)",
"alph_desc": "알파벳순 (A-Z)",
"title": "검색"
"title": "검색",
"downloads_asce": "다운로드 수 낮은 순",
"date_desc": "최신 순",
"date_asce": "오래된 순",
"downloads_desc": "다운로드 많은 순"
},
"store_testing_cta": "새로운 플러그인을 테스트하여 Decky Loader 팀을 도와주세요!"
"store_testing_cta": "새로운 플러그인을 테스트하여 Decky Loader 팀을 도와주세요!",
"store_testing_warning": {
"desc": "이 스토어 채널을 사용하여 가장 최신 버전의 플러그인을 테스트할 수 있습니다. GitHub에 피드백을 남겨서 모든 사용자가 업데이트 할 수 있게 해주세요.",
"label": "테스트 스토어 채널에 오신 것을 환영합니다"
}
},
"StoreSelect": {
"custom_store": {
@@ -212,7 +223,7 @@
"custom": "사용자 지정",
"label": "스토어 배포 채널",
"default": "기본",
"testing": "시험"
"testing": "테스트"
}
},
"Updater": {
@@ -241,5 +252,12 @@
"button": {
"back": "뒤로"
}
},
"TitleView": {
"settings_desc": "Decky 설정 열기",
"decky_store_desc": "Decky 스토어 열기"
},
"Testing": {
"download": "다운로드"
}
}
+116 -92
View File
@@ -1,16 +1,16 @@
{
"BranchSelect": {
"update_channel": {
"prerelease": "Vooruitgave",
"prerelease": "Prerelease",
"stable": "Stabiel",
"label": "Update Kanaal",
"testing": "Test"
"label": "Updatekanaal",
"testing": "Testing"
}
},
"Developer": {
"5secreload": "Herlaad in 5 seconden",
"disabling": "Uitschakelen React DevTools",
"enabling": "Inschakelen React DevTools"
"5secreload": "Bezig met herstarten in 5 seconden",
"disabling": "Bezig met uitschakelen van React DevTools",
"enabling": "Bezig met inschakelen van React DevTools"
},
"DropdownMultiselect": {
"button": {
@@ -19,30 +19,34 @@
},
"FilePickerError": {
"errors": {
"unknown": "Een onbekende fout is opgetreden. De ruwe fout is: {{raw_error}}",
"file_not_found": "Het opgegeven pad is niet geldig. Controleer het en voer het opnieuw correct in."
"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": "Toon verborgen bestanden"
"show_hidden": "Verborgen bestanden tonen"
},
"filter": {
"created_desc": "Gecreëerd ( Nieuwste)",
"modified_asce": "Veranderd (Oudste)",
"modified_desc": "Veranderd (Nieuwste)",
"name_asce": "Z-A",
"name_desc": "A-Z",
"size_asce": "Grootte (Kleinste)",
"size_desc": "Grootte (Grootste)",
"created_asce": "Gecreëerd (Oudste)"
"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": "Gebruik deze map",
"show_more": "Toon meer bestanden"
"select": "Deze map gebruiken",
"show_more": "Meer bestanden tonen"
},
"file": {
"select": "Dit bestand selecteren"
}
},
"PluginView": {
@@ -50,13 +54,13 @@
"hidden_other": "{{count}} plug-ins zijn verborgen in deze lijst"
},
"PluginListLabel": {
"hidden": "Verborgen in het snelmenu"
"hidden": "Verborgen in snelle toegang"
},
"PluginCard": {
"plugin_install": "Installeren",
"plugin_no_desc": "Geen beschrijving gegeven.",
"plugin_version_label": "Plugin Versie",
"plugin_full_access": "Deze plug-in heeft volledige toegang tot je Steam Deck."
"plugin_version_label": "Plug-inversie",
"plugin_full_access": "Deze plug-in heeft volledige toegang tot uw Steam Deck."
},
"PluginInstallModal": {
"install": {
@@ -65,59 +69,61 @@
"title": "Installeer {{artifact}}",
"desc": "Weet je zeker dat je {{artifact}} {{version}} wilt installeren?"
},
"no_hash": "Deze plug-in heeft geen hash, u installeert deze op eigen risico.",
"no_hash": "Deze plug-in heeft geen hash, je installeert deze op eigen risico.",
"reinstall": {
"button_idle": "Herinstalleren",
"button_processing": "Bezig te herinstalleren",
"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": "Update",
"button_processing": "Bezig met updaten",
"button_idle": "Bijwerken",
"button_processing": "Bezig met bijwerken",
"title": "{{artifact}} bijwerken",
"desc": "Weet je zeker dat je {{artifact}} {{version}} wilt updaten?"
"desc": "Weet je zeker dat je {{artifact}} {{version}} wilt bijwerken?"
}
},
"MultiplePluginsInstallModal": {
"title": {
"mixed_one": "Wijzig {{count}} plug-in",
"mixed_other": "Pas {{count}} plug-ins aan",
"update_one": "1 plugin bijwerken",
"update_other": "{{count}} plug-ins bijwerken",
"install_one": "Installeer 1 plugin",
"install_other": "Installeer {{count}} plugins",
"reinstall_one": "1 plugin opnieuw installeren",
"reinstall_other": "{{count}} plugins opnieuw installeren"
"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": "Werkend"
"loading": "Bezig"
},
"confirm": "Weet u zeker dat u de volgende wijzigingen wilt aanbrengen?",
"confirm": "Weet je zeker dat je de volgende wijzigingen wilt aanbrengen?",
"description": {
"install": "Installeer {{name}} {{version}}",
"update": "Update {{name}} naar {{version}}",
"reinstall": "Installeer opnieuw {{name}} {{version}}"
"update": "Werk {{name}} bij naar {{version}}",
"reinstall": "Installeer {{name}} {{version}} opnieuw"
}
},
"PluginListIndex": {
"no_plugin": "Geen plugins geïnstalleerd!",
"plugin_actions": "Plugin Acties",
"reload": "Herladen",
"no_plugin": "Geen plug-ins geïnstalleerd!",
"plugin_actions": "Plug-inacties",
"reload": "Herstarten",
"uninstall": "Verwijderen",
"update_to": "Update naar {{name}}",
"hide": "Snelle toegang: Verberg",
"update_all_one": "Update 1 plugin",
"update_all_other": "Update {{count}} plugins",
"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": "Snelle toegang: Toon"
"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 plugin {{name}}",
"message": "Fout bij het laden van plug-in {{name}}",
"toast": "Fout bij het laden van {{name}}"
},
"plugin_uninstall": {
@@ -125,68 +131,71 @@
"desc": "Weet je zeker dat je {{name}} wilt verwijderen?",
"title": "Verwijder {{name}}"
},
"plugin_update_one": "Updates beschikbaar voor 1 plugin!",
"plugin_update_other": "Updates beschikbaar voor {{count}} plugins!",
"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 uitzondering zoals hierboven weergegeven. Dit betekent meestal dat de plug-in een update vereist voor de nieuwe versie van SteamUI. Controleer of er een update aanwezig is of evalueer de verwijdering ervan in de Decky-instellingen, in het gedeelte Plug-ins."
"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-foutopsporing toe aan iedereen in uw netwerk",
"label": "Externe CEF-foutopsporing toestaan"
"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 Bedieningsscherm",
"desc": "Opent de CEF-console. Alleen nuttig voor foutopsporingsdoeleinden. Dingen hier zijn potentieel gevaarlijk en mogen alleen worden gebruikt als u een ontwikkelaar van plug-ins bent, of hier door een ontwikkelaar naartoe wordt geleid."
"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": "Andere",
"header": "Overige",
"react_devtools": {
"ip_label": "IP",
"label": "Aanzetten React DevTools",
"desc": "Maakt verbinding met een computer met React DevTools mogelijk. Als je deze instelling wijzigt, wordt Steam opnieuw geladen. Stel het IP-adres in voordat u het inschakelt."
"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 Plugin van URL",
"label_zip": "Installeer Plugin van Zip bestand",
"label_url": "Installeer plug-in via een URL",
"label_zip": "Installeer plug-in via een ZIP-bestand",
"button_install": "Installeren",
"button_zip": "Bladeren"
},
"toast_zip": {
"body": "Installatie mislukt! Alleen ZIP-bestanden worden ondersteund."
},
"valve_internal": {
"desc1": "Schakelt het interne ontwikkelaarsmenu van Valve in.",
"desc2": "Raak niets in dit menu aan tenzij u weet wat het doet.",
"desc2": "Pas niets in dit menu aan, tenzij je weet wat het doet.",
"label": "Valve Internal inschakelen"
}
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Decky versie",
"decky_version": "Decky-versie",
"header": "Over"
},
"beta": {
"header": "Beta deelname"
"header": "Beta-deelname"
},
"developer_mode": {
"label": "Ontwikkelaars modus"
"label": "Ontwikkelaarsmodus"
},
"other": {
"header": "Overige"
},
"updates": {
"header": "Nieuwe Versies"
"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": "Plugins"
"plugins_title": "Plug-ins",
"testing_title": "Testen"
},
"Store": {
"store_filter": {
@@ -194,53 +203,68 @@
"label_def": "Alles"
},
"store_search": {
"label": "Zoek"
"label": "Zoeken"
},
"store_sort": {
"label": "Sorteren",
"label_def": "Laatste Geupdate (Nieuwste)"
"label_def": "Laatst bijgewerkt (nieuwste)"
},
"store_source": {
"label": "Bron Code",
"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"
"title": "Bladeren",
"date_desc": "Nieuwste eerst",
"downloads_asce": "Minste gedownload eerst",
"downloads_desc": "Meeste gedownload eerst",
"date_asce": "Oudste eerst"
},
"store_testing_cta": "Overweeg alsjeblieft om nieuwe plug-ins te testen om het Decky Loader-team te helpen!",
"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 winkel, kijk dan in de SteamDeckHomebrew/decky-plugin-template repository op GitHub. Informatie over ontwikkeling en distributie is beschikbaar in de README.",
"label": "Bijdragende"
"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": "Aangepassingen winkel",
"label": "Aangepaste winkel",
"url_label": "URL"
},
"store_channel": {
"custom": "Aanpassingen",
"custom": "Aangepast",
"default": "Standaard",
"label": "Winkel Kanaal",
"testing": "Testen"
"label": "Winkelkanaal",
"testing": "Testing"
}
},
"Updater": {
"patch_notes_desc": "Correctie opmerkingen",
"patch_notes_desc": "Patch-opmerkingen",
"updates": {
"check_button": "Controleer op updates",
"checking": "Controleren",
"check_button": "Op updates controleren",
"checking": "Bezig met controleren op updates",
"cur_version": "Huidige versie: {{ver}}",
"install_button": "Installeer Update",
"label": "Update",
"lat_version": "Up-to-date: loopt {{ver}}",
"reloading": "Herstarten",
"updating": "Aan het updaten"
"install_button": "Bijwerken",
"label": "Updates",
"lat_version": "Bijwerkt: versie {{ver}}",
"reloading": "Bezig met herstarten",
"updating": "Bezig met bijwerken"
},
"decky_updates": "Decky Nieuwe Versies",
"no_patch_notes_desc": "geen correctie-opmerkingen voor deze versie"
"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"
}
}
+19 -7
View File
@@ -104,7 +104,8 @@
"update_all_one": "Atualizar 1 plugin",
"update_all_many": "Atualizar {{count}} plugins",
"update_all_other": "Atualizar {{count}} plugins",
"hide": "Acesso Rápido: Ocultar"
"hide": "Acesso Rápido: Ocultar",
"freeze": "Congelar updates"
},
"PluginLoader": {
"decky_title": "Decky",
@@ -120,7 +121,7 @@
},
"decky_update_available": "Atualização para {{tag_name}} disponível!",
"plugin_error_uninstall": "Um erro aconteceu ao carregar {{name}}, como mostrado acima. Isso normalmente significa que o plugin precisa de uma atualização para a nova versão do SteamUI. Confira se existe uma atualização ou avalie a remoção do plugin nas configurações do Decky, na sessão de plugins.",
"plugin_update_one": "Atualizações disponível para 1 plugin!",
"plugin_update_one": "Atualização disponível para 1 plugin!",
"plugin_update_many": "Atualizações disponíveis para {{count}} plugins!",
"plugin_update_other": "Atualizações disponíveis para {{count}} plugins!"
},
@@ -150,9 +151,6 @@
"label_zip": "Instalar Plugin a partir de um arquivo ZIP",
"label_desc": "URL"
},
"toast_zip": {
"body": "Falha na instalação! Somente arquivos ZIP são suportados."
},
"valve_internal": {
"desc1": "Habilita o menu interno de desenvolvedor da Valve.",
"desc2": "Não toque em nada neste menu, a não ser que você saiba o que está fazendo.",
@@ -175,6 +173,11 @@
},
"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": {
@@ -208,7 +211,11 @@
"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_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": {
@@ -250,7 +257,12 @@
"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}}"
"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"
}
}
+47 -5
View File
@@ -1,7 +1,27 @@
{
"FilePickerIndex": {
"folder": {
"select": "Usar esta pasta"
"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": {
@@ -129,9 +149,6 @@
"label_url": "Instalar plugin a partir dum URL",
"label_zip": "Instalar plugin a partir dum ficheiro ZIP"
},
"toast_zip": {
"body": "A instalação falhou! Só ficheiros ZIP são suportados."
},
"valve_internal": {
"label": "Activar menu interno da Valve",
"desc1": "Activa o menu interno de programador da Valve.",
@@ -160,6 +177,11 @@
},
"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": {
@@ -193,7 +215,11 @@
"alph_desc": "Alfabeticamente (A-Z)",
"title": "Navegar"
},
"store_testing_cta": "Testa novos plugins e ajuda a equipa do Decky Loader!"
"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": {
@@ -221,5 +247,21 @@
"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"
}
}
}
+26 -8
View File
@@ -36,7 +36,9 @@
"show": "Быстрый доступ: Показать",
"plugin_actions": "Действия с плагинами",
"no_plugin": "Не установлено ни одного плагина!",
"reinstall": "Переустановить"
"reinstall": "Переустановить",
"freeze": "Остановить обновления",
"unfreeze": "Разрешить обновления"
},
"PluginLoader": {
"plugin_update_one": "Обновления доступны для {{count}} плагина!",
@@ -49,7 +51,7 @@
},
"plugin_uninstall": {
"button": "Удалить",
"desc": "Вы уверенны, что хотите удалить {{name}}?",
"desc": "Вы уверены, что хотите удалить {{name}}?",
"title": "Удалить {{name}}"
},
"decky_title": "Decky",
@@ -97,19 +99,19 @@
"button_processing": "Установка",
"title": "Установить {{artifact}}",
"button_idle": "Установить",
"desc": "Вы уверенны, что хотите установить {{artifact}} {{version}}?"
"desc": "Вы уверены, что хотите установить {{artifact}} {{version}}?"
},
"no_hash": "У данного плагина отсутствует хэш, устанавливайте на свой страх и риск.",
"reinstall": {
"title": "Переустановить {{artifact}}",
"desc": "Вы уверенны, что хотите переустановить {{artifact}} {{version}}?",
"desc": "Вы уверены, что хотите переустановить {{artifact}} {{version}}?",
"button_idle": "Переустановить",
"button_processing": "Переустановка"
},
"update": {
"button_idle": "Обновить",
"button_processing": "Обновление",
"desc": "Вы уверенны, что хотите обновить {{artifact}} {{version}}?",
"desc": "Вы уверены, что хотите обновить {{artifact}} {{version}}?",
"title": "Обновить {{artifact}}"
}
},
@@ -184,7 +186,11 @@
"about": "Информация",
"alph_desc": "По алфавиту (A - Z)",
"title": "Обзор",
"alph_asce": "По алфавиту (Z - A)"
"alph_asce": "По алфавиту (Z - A)",
"date_asce": "Сначала старые",
"date_desc": "Сначала новые",
"downloads_asce": "Наименее загружаемые сначала",
"downloads_desc": "Наиболее загружаемые сначала"
},
"store_testing_cta": "Пожалуйста, рассмотрите возможность тестирования новых плагинов, чтобы помочь команде Decky Loader!",
"store_contrib": {
@@ -197,6 +203,10 @@
},
"store_search": {
"label": "Поиск"
},
"store_testing_warning": {
"label": "Добро пожаловать в тестовый канал магазина",
"desc": "Вы можете использовать этот канал магазина для тестирования новейших версий плагинов. Не забудьте оставить отзыв на GitHub, чтобы плагин можно было обновить для всех пользователей."
}
},
"StoreSelect": {
@@ -208,7 +218,7 @@
"custom": "Сторонний",
"default": "По-умолчанию",
"label": "Канал магазина",
"testing": "Тестирование"
"testing": "Тестовый"
}
},
"Updater": {
@@ -254,6 +264,14 @@
"SettingsIndex": {
"developer_title": "Разработчик",
"general_title": "Общее",
"plugins_title": "Плагины"
"plugins_title": "Плагины",
"testing_title": "Тестирование"
},
"TitleView": {
"decky_store_desc": "Открыть магазин Decky",
"settings_desc": "Открыть настройки Decky"
},
"Testing": {
"download": "Загрузить"
}
}
+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"
}
}
-3
View File
@@ -124,9 +124,6 @@
"label_zip": "Встановити плагін з ZIP-файлу",
"button_zip": "Огляд"
},
"toast_zip": {
"body": "Помилка встановлення! Підтримуються лише ZIP-файли."
},
"valve_internal": {
"desc1": "Вмикає внутрішнє розробницьке меню Valve.",
"label": "Увімкнути Valve Internal",
+29 -11
View File
@@ -73,7 +73,9 @@
"update_to": "更新 {{name}}",
"update_all_other": "更新 {{count}} 个插件",
"show": "在快速访问菜单中显示",
"hide": "在快速访问菜单中隐藏"
"hide": "在快速访问菜单中隐藏",
"unfreeze": "允许更新",
"freeze": "暂停更新"
},
"PluginLoader": {
"decky_title": "Decky",
@@ -93,15 +95,15 @@
},
"RemoteDebugging": {
"remote_cef": {
"desc": "允许你网络中的任何人无需身份验证即可访问CEF调试器",
"label": "允许远程访问CEF调试"
"desc": "允许你网络中的任何人无需身份验证即可访问 CEF 调试器",
"label": "允许 CEF 远程调试"
}
},
"SettingsDeveloperIndex": {
"react_devtools": {
"ip_label": "IP",
"label": "启用 React DevTools",
"desc": "允许连接到运行着 React DevTools 的计算机更改此设置将重新加载Steam请在启用前设置IP地址。"
"desc": "允许连接到运行着 React DevTools 的计算机更改此设置将重新加载 Steam请在启用前设置 IP 地址。"
},
"third_party_plugins": {
"button_install": "安装",
@@ -149,12 +151,13 @@
"SettingsIndex": {
"developer_title": "开发者",
"general_title": "通用",
"plugins_title": "插件"
"plugins_title": "插件",
"testing_title": "测试"
},
"Store": {
"store_contrib": {
"label": "贡献",
"desc": "如果你想要提交你的插件到 Decky 插件商店,请访问 GitHub 上的 SteamDeckHomebrew/decky-plugin-template 存储库,关于开发和分发的相关信息,请查看 README 文件。"
"desc": "如果你想要提交你的插件到 Decky 插件商店,请访问 GitHub 上的 SteamDeckHomebrew/decky-plugin-template 存储库。有关开发和分发插件的信息,请查看 README 文件。"
},
"store_filter": {
"label": "过滤器",
@@ -169,15 +172,23 @@
},
"store_source": {
"label": "源代码",
"desc": "所有插件的源代码都可以在 GitHub 上的 SteamDeckHomebrew/decky-plugin-database 存储库中获得。"
"desc": "所有插件的源代码都可 GitHub 上的 SteamDeckHomebrew/decky-plugin-database 存储库中获得。"
},
"store_tabs": {
"about": "关于",
"alph_asce": "字母排序 (Z 到 A)",
"alph_desc": "字母排序 (A 到 Z)",
"title": "浏览"
"title": "浏览",
"downloads_desc": "下载量倒序",
"date_asce": "更新时间正序",
"date_desc": "更新时间倒序",
"downloads_asce": "下载量正序"
},
"store_testing_cta": "请考虑测试新插件以帮助 Decky Loader 团队!"
"store_testing_cta": "请考虑测试新插件以帮助 Decky Loader 团队!",
"store_testing_warning": {
"desc": "你可以使用该商店频道以体验最新版本的插件。 请在插件 Github 页面留言以使插件可以正式面向所有用户。",
"label": "欢迎来到商店测试频道"
}
},
"StoreSelect": {
"store_channel": {
@@ -238,8 +249,15 @@
"FilePickerError": {
"errors": {
"file_not_found": "指定路径无效。请检查并输入正确的路径。",
"unknown": "发生了一个为止错误。原始错误为:{{raw_error}}",
"perm_denied": "你没有访问特定目录的权限。请检查你的用户(Steam Deck 中的 deck 账户)有着相对应的权限以访问特定的文件夹或文件。"
"unknown": "发生了一个未知错误。原始错误为:{{raw_error}}",
"perm_denied": "你没有访问特定目录的权限。请检查你的用户(Steam Deck 中的 deck 账户)是否有权访问特定的文件夹或文件。"
}
},
"TitleView": {
"decky_store_desc": "打开 Decky 商店",
"settings_desc": "打开 Decky 设置"
},
"Testing": {
"download": "下载"
}
}
+64 -12
View File
@@ -14,12 +14,32 @@
},
"FilePickerIndex": {
"folder": {
"select": "使用此資料夾"
"select": "使用此資料夾",
"show_more": "顯示更多檔案",
"label": "資料夾"
},
"filter": {
"modified_asce": "修改日期(舊到新)",
"created_desc": "建立日期(新到舊)",
"modified_desc": "修改日期(新到舊)",
"name_desc": "子母排序(A到Z",
"name_asce": "子母排序(Z到A",
"size_asce": "檔案大小(小到大)",
"size_desc": "檔案大小(大到小)",
"created_asce": "建立日期(舊到新)"
},
"file": {
"select": "選擇此檔案"
},
"files": {
"all_files": "所有檔案",
"file_type": "檔案類型",
"show_hidden": "顯示隱藏檔"
}
},
"PluginCard": {
"plugin_install": "安裝",
"plugin_no_desc": "未提描述。",
"plugin_no_desc": "未提描述。",
"plugin_version_label": "外掛程式版本",
"plugin_full_access": "此外掛程式擁有您的 Steam Deck 的完整存取權。"
},
@@ -53,7 +73,9 @@
"reload": "重新載入",
"show": "快速存取:顯示",
"hide": "快速存取:隱藏",
"update_all_other": "更新 {{count}} 個外掛程式"
"update_all_other": "更新 {{count}} 個外掛程式",
"freeze": "禁止更新",
"unfreeze": "允許更新"
},
"PluginLoader": {
"decky_title": "Decky",
@@ -79,16 +101,13 @@
},
"SettingsDeveloperIndex": {
"third_party_plugins": {
"button_zip": "開啟",
"button_zip": "瀏覽",
"label_desc": "網址",
"label_url": "從網址安裝外掛程式",
"label_zip": "從 ZIP 檔案安裝外掛程式",
"button_install": "安裝",
"header": "第三方外掛程式"
},
"toast_zip": {
"title": "Decky"
},
"valve_internal": {
"desc2": "除非您知道它的作用,否則不要碰這個選單中的任何東西。",
"desc1": "啟用 Valve 內建開發人員選單。",
@@ -103,7 +122,7 @@
"cef_console": {
"button": "開啟控制台",
"label": "CEF 控制台",
"desc": "開啟 CEF 控制台。僅用於偵錯。這裡的東西有潛在的風險,只有當是一個外掛程式開發者或者被外掛程式開發者引導到這裡時,才應該使用。"
"desc": "開啟 CEF 控制台。僅用於偵錯。這裡的東西有潛在的風險,只有當是一個外掛程式開發者或者被外掛程式開發者引導到這裡時,才應該使用。"
}
},
"SettingsGeneralIndex": {
@@ -122,12 +141,18 @@
},
"updates": {
"header": "更新"
},
"notifications": {
"decky_updates_label": "Decky 可更新",
"header": "通知",
"plugin_updates_label": "外掛程式有更新"
}
},
"SettingsIndex": {
"developer_title": "開發人員",
"general_title": "一般",
"plugins_title": "外掛程式"
"plugins_title": "外掛程式",
"testing_title": "測試"
},
"Store": {
"store_contrib": {
@@ -153,9 +178,17 @@
"about": "關於",
"alph_asce": "依字母排序 (Z 到 A)",
"alph_desc": "依字母排序 (A 到 Z)",
"title": "瀏覽"
"title": "瀏覽",
"downloads_desc": "下載量高到低",
"downloads_asce": "下載量低到高",
"date_asce": "日期舊到新",
"date_desc": "日期新到舊"
},
"store_testing_cta": "請考慮測試新的外掛程式來幫助 Decky Loader 團隊!"
"store_testing_cta": "請考慮測試新的外掛程式來幫助 Decky Loader 團隊!",
"store_testing_warning": {
"label": "歡迎來到測試頻道",
"desc": "您可以使用此商店頻道來體驗測試外掛版本。請務必在 GitHub 上留下回饋,以便為所有用戶更新該外掛程式。"
}
},
"StoreSelect": {
"custom_store": {
@@ -204,8 +237,27 @@
"confirm": "您確定要進行以下的修改嗎?",
"description": {
"install": "安裝 {{name}} {{version}}",
"update": "更新 {{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 -189
View File
@@ -1,190 +1,4 @@
# Change PyInstaller files permissions
import sys
from localplatform import (chmod, chown, service_stop, service_start,
ON_WINDOWS, get_log_level, get_live_reload,
get_server_port, get_server_host, get_chown_plugin_path,
get_unprivileged_user, get_unprivileged_path,
get_privileged_path)
if hasattr(sys, '_MEIPASS'):
chmod(sys._MEIPASS, 755)
# Full imports
from asyncio import new_event_loop, set_event_loop, sleep
from json import dumps, loads
from logging import DEBUG, INFO, basicConfig, getLogger
from os import getenv, path
from traceback import format_exc
import multiprocessing
import aiohttp_cors
# Partial imports
from aiohttp import client_exceptions, WSMsgType
from aiohttp.web import Application, Response, get, run_app, static
from aiohttp_jinja2 import setup as jinja_setup
# local modules
from browser import PluginBrowser
from helpers import (REMOTE_DEBUGGER_UNIT, csrf_middleware, get_csrf_token,
mkdir_as_user, get_system_pythonpaths)
from injector import get_gamepadui_tab, Tab, get_tabs, close_old_tabs
from loader import Loader
from settings import SettingsManager
from updater import Updater
from utilities import Utilities
from customtypes import UserType
basicConfig(
level=get_log_level(),
format="[%(module)s][%(levelname)s]: %(message)s"
)
logger = getLogger("Main")
plugin_path = path.join(get_privileged_path(), "plugins")
def chown_plugin_dir():
if not path.exists(plugin_path): # For safety, create the folder before attempting to do anything with it
mkdir_as_user(plugin_path)
if not chown(plugin_path, UserType.HOST_USER) or not chmod(plugin_path, 555):
logger.error(f"chown/chmod exited with a non-zero exit code")
if get_chown_plugin_path() == True:
chown_plugin_dir()
class PluginManager:
def __init__(self, loop) -> None:
self.loop = loop
self.web_app = Application()
self.web_app.middlewares.append(csrf_middleware)
self.cors = aiohttp_cors.setup(self.web_app, defaults={
"https://steamloopback.host": aiohttp_cors.ResourceOptions(
expose_headers="*",
allow_headers="*",
allow_credentials=True
)
})
self.plugin_loader = Loader(self.web_app, plugin_path, self.loop, get_live_reload())
self.settings = SettingsManager("loader", path.join(get_privileged_path(), "settings"))
self.plugin_browser = PluginBrowser(plugin_path, self.plugin_loader.plugins, self.plugin_loader, self.settings)
self.utilities = Utilities(self)
self.updater = Updater(self)
jinja_setup(self.web_app)
async def startup(_):
if self.settings.getSetting("cef_forward", False):
self.loop.create_task(service_start(REMOTE_DEBUGGER_UNIT))
else:
self.loop.create_task(service_stop(REMOTE_DEBUGGER_UNIT))
self.loop.create_task(self.loader_reinjector())
self.loop.create_task(self.load_plugins())
self.web_app.on_startup.append(startup)
self.loop.set_exception_handler(self.exception_handler)
self.web_app.add_routes([get("/auth/token", self.get_auth_token)])
for route in list(self.web_app.router.routes()):
self.cors.add(route)
self.web_app.add_routes([static("/static", path.join(path.dirname(__file__), 'static'))])
self.web_app.add_routes([static("/legacy", path.join(path.dirname(__file__), 'legacy'))])
def exception_handler(self, loop, context):
if context["message"] == "Unclosed connection":
return
loop.default_exception_handler(context)
async def get_auth_token(self, request):
return Response(text=get_csrf_token())
async def load_plugins(self):
# await self.wait_for_server()
logger.debug("Loading plugins")
self.plugin_loader.import_plugins()
# await inject_to_tab("SP", "window.syncDeckyPlugins();")
if self.settings.getSetting("pluginOrder", None) == None:
self.settings.setSetting("pluginOrder", list(self.plugin_loader.plugins.keys()))
logger.debug("Did not find pluginOrder setting, set it to default")
async def loader_reinjector(self):
while True:
tab = None
nf = False
dc = False
while not tab:
try:
tab = await get_gamepadui_tab()
except (client_exceptions.ClientConnectorError, client_exceptions.ServerDisconnectedError):
if not dc:
logger.debug("Couldn't connect to debugger, waiting...")
dc = True
pass
except ValueError:
if not nf:
logger.debug("Couldn't find GamepadUI tab, waiting...")
nf = True
pass
if not tab:
await sleep(5)
await tab.open_websocket()
await tab.enable()
await self.inject_javascript(tab, True)
try:
async for msg in tab.listen_for_message():
# this gets spammed a lot
if msg.get("method", None) != "Page.navigatedWithinDocument":
logger.debug("Page event: " + str(msg.get("method", None)))
if msg.get("method", None) == "Page.domContentEventFired":
if not await tab.has_global_var("deckyHasLoaded", False):
await self.inject_javascript(tab)
if msg.get("method", None) == "Inspector.detached":
logger.info("CEF has requested that we detach.")
await tab.close_websocket()
break
# If this is a forceful disconnect the loop will just stop without any failure message. In this case, injector.py will handle this for us so we don't need to close the socket.
# This is because of https://github.com/aio-libs/aiohttp/blob/3ee7091b40a1bc58a8d7846e7878a77640e96996/aiohttp/client_ws.py#L321
logger.info("CEF has disconnected...")
# At this point the loop starts again and we connect to the freshly started Steam client once it is ready.
except Exception as e:
logger.error("Exception while reading page events " + format_exc())
await tab.close_websocket()
pass
# while True:
# await sleep(5)
# if not await tab.has_global_var("deckyHasLoaded", False):
# logger.info("Plugin loader isn't present in Steam anymore, reinjecting...")
# await self.inject_javascript(tab)
async def inject_javascript(self, tab: Tab, first=False, request=None):
logger.info("Loading Decky frontend!")
try:
if first:
if await tab.has_global_var("deckyHasLoaded", False):
await close_old_tabs()
await tab.evaluate_js("try{if (window.deckyHasLoaded){setTimeout(() => location.reload(), 100)}else{window.deckyHasLoaded = true;(async()=>{try{while(!window.SP_REACT){await new Promise(r => setTimeout(r, 10))};await import('http://localhost:1337/frontend/index.js')}catch(e){console.error(e)};})();}}catch(e){console.error(e)}", False, False, False)
except:
logger.info("Failed to inject JavaScript into tab\n" + format_exc())
pass
def run(self):
return run_app(self.web_app, host=get_server_host(), port=get_server_port(), loop=self.loop, access_log=None)
# This file is needed to make the relative imports in decky_loader/ work properly.
if __name__ == "__main__":
if ON_WINDOWS:
# Fix windows/flask not recognising that .js means 'application/javascript'
import mimetypes
mimetypes.add_type('application/javascript', '.js')
# Required for multiprocessing support in frozen files
multiprocessing.freeze_support()
# Append the loader's plugin path to the recognized python paths
sys.path.append(path.join(path.dirname(__file__), "plugin"))
# Append the system and user python paths
sys.path.extend(get_system_pythonpaths())
loop = new_event_loop()
set_event_loop(loop)
PluginManager(loop).run()
from decky_loader.main import main
main()
-159
View File
@@ -1,159 +0,0 @@
import multiprocessing
from asyncio import (Lock, get_event_loop, new_event_loop,
set_event_loop, sleep)
from concurrent.futures import ProcessPoolExecutor
from importlib.util import module_from_spec, spec_from_file_location
from json import dumps, load, loads
from logging import getLogger
from traceback import format_exc
from os import path, environ
from signal import SIGINT, signal
from sys import exit, path as syspath
from time import time
from localsocket import LocalSocket
from localplatform import setgid, setuid, get_username, get_home_path
from customtypes import UserType
import helpers
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.method_call_lock = Lock()
self.socket = 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
environ["DECKY_PLUGIN_VERSION"] = self.version
environ["DECKY_PLUGIN_AUTHOR"] = self.author
# append the plugin's `py_modules` to the recognized python paths
syspath.append(path.join(environ["DECKY_PLUGIN_DIR"], "py_modules"))
spec = spec_from_file_location("_", self.file)
module = module_from_spec(spec)
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 _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()
get_event_loop().stop()
while get_event_loop().is_running():
await sleep(0)
get_event_loop().close()
raise Exception("Closing message listener")
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:
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):
if self.passive:
return
async def _(self):
await self.socket.write_single_line(dumps({ "stop": True }, ensure_ascii=False))
await self.socket.close_socket_connection()
get_event_loop().create_task(_(self))
async def execute_method(self, method_name, kwargs):
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"]
+765
View File
@@ -0,0 +1,765 @@
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
[[package]]
name = "aiohttp"
version = "3.9.5"
description = "Async http client/server framework (asyncio)"
optional = false
python-versions = ">=3.8"
files = [
{file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7"},
{file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c"},
{file = "aiohttp-3.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae79c1bc12c34082d92bf9422764f799aee4746fd7a392db46b7fd357d4a17a"},
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d3ebb9e1316ec74277d19c5f482f98cc65a73ccd5430540d6d11682cd857430"},
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84dabd95154f43a2ea80deffec9cb44d2e301e38a0c9d331cc4aa0166fe28ae3"},
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a02fbeca6f63cb1f0475c799679057fc9268b77075ab7cf3f1c600e81dd46b"},
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72"},
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:714d4e5231fed4ba2762ed489b4aec07b2b9953cf4ee31e9871caac895a839c0"},
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7a6a8354f1b62e15d48e04350f13e726fa08b62c3d7b8401c0a1314f02e3558"},
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c413016880e03e69d166efb5a1a95d40f83d5a3a648d16486592c49ffb76d0db"},
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ff84aeb864e0fac81f676be9f4685f0527b660f1efdc40dcede3c251ef1e867f"},
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ad7f2919d7dac062f24d6f5fe95d401597fbb015a25771f85e692d043c9d7832"},
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:702e2c7c187c1a498a4e2b03155d52658fdd6fda882d3d7fbb891a5cf108bb10"},
{file = "aiohttp-3.9.5-cp310-cp310-win32.whl", hash = "sha256:67c3119f5ddc7261d47163ed86d760ddf0e625cd6246b4ed852e82159617b5fb"},
{file = "aiohttp-3.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:471f0ef53ccedec9995287f02caf0c068732f026455f07db3f01a46e49d76bbb"},
{file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342"},
{file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d"},
{file = "aiohttp-3.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424"},
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee"},
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2"},
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233"},
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595"},
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6"},
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d"},
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323"},
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9"},
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771"},
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75"},
{file = "aiohttp-3.9.5-cp311-cp311-win32.whl", hash = "sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6"},
{file = "aiohttp-3.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a"},
{file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c7a4b7a6cf5b6eb11e109a9755fd4fda7d57395f8c575e166d363b9fc3ec4678"},
{file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0a158704edf0abcac8ac371fbb54044f3270bdbc93e254a82b6c82be1ef08f3c"},
{file = "aiohttp-3.9.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d153f652a687a8e95ad367a86a61e8d53d528b0530ef382ec5aaf533140ed00f"},
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82a6a97d9771cb48ae16979c3a3a9a18b600a8505b1115cfe354dfb2054468b4"},
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60cdbd56f4cad9f69c35eaac0fbbdf1f77b0ff9456cebd4902f3dd1cf096464c"},
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8676e8fd73141ded15ea586de0b7cda1542960a7b9ad89b2b06428e97125d4fa"},
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da00da442a0e31f1c69d26d224e1efd3a1ca5bcbf210978a2ca7426dfcae9f58"},
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18f634d540dd099c262e9f887c8bbacc959847cfe5da7a0e2e1cf3f14dbf2daf"},
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:320e8618eda64e19d11bdb3bd04ccc0a816c17eaecb7e4945d01deee2a22f95f"},
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2faa61a904b83142747fc6a6d7ad8fccff898c849123030f8e75d5d967fd4a81"},
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8c64a6dc3fe5db7b1b4d2b5cb84c4f677768bdc340611eca673afb7cf416ef5a"},
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:393c7aba2b55559ef7ab791c94b44f7482a07bf7640d17b341b79081f5e5cd1a"},
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c671dc117c2c21a1ca10c116cfcd6e3e44da7fcde37bf83b2be485ab377b25da"},
{file = "aiohttp-3.9.5-cp312-cp312-win32.whl", hash = "sha256:5a7ee16aab26e76add4afc45e8f8206c95d1d75540f1039b84a03c3b3800dd59"},
{file = "aiohttp-3.9.5-cp312-cp312-win_amd64.whl", hash = "sha256:5ca51eadbd67045396bc92a4345d1790b7301c14d1848feaac1d6a6c9289e888"},
{file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:694d828b5c41255e54bc2dddb51a9f5150b4eefa9886e38b52605a05d96566e8"},
{file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0605cc2c0088fcaae79f01c913a38611ad09ba68ff482402d3410bf59039bfb8"},
{file = "aiohttp-3.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4558e5012ee03d2638c681e156461d37b7a113fe13970d438d95d10173d25f78"},
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbc053ac75ccc63dc3a3cc547b98c7258ec35a215a92bd9f983e0aac95d3d5b"},
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4109adee842b90671f1b689901b948f347325045c15f46b39797ae1bf17019de"},
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6ea1a5b409a85477fd8e5ee6ad8f0e40bf2844c270955e09360418cfd09abac"},
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3c2890ca8c59ee683fd09adf32321a40fe1cf164e3387799efb2acebf090c11"},
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3916c8692dbd9d55c523374a3b8213e628424d19116ac4308e434dbf6d95bbdd"},
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8d1964eb7617907c792ca00b341b5ec3e01ae8c280825deadbbd678447b127e1"},
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5ab8e1f6bee051a4bf6195e38a5c13e5e161cb7bad83d8854524798bd9fcd6e"},
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52c27110f3862a1afbcb2af4281fc9fdc40327fa286c4625dfee247c3ba90156"},
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7f64cbd44443e80094309875d4f9c71d0401e966d191c3d469cde4642bc2e031"},
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b4f72fbb66279624bfe83fd5eb6aea0022dad8eec62b71e7bf63ee1caadeafe"},
{file = "aiohttp-3.9.5-cp38-cp38-win32.whl", hash = "sha256:6380c039ec52866c06d69b5c7aad5478b24ed11696f0e72f6b807cfb261453da"},
{file = "aiohttp-3.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:da22dab31d7180f8c3ac7c7635f3bcd53808f374f6aa333fe0b0b9e14b01f91a"},
{file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1732102949ff6087589408d76cd6dea656b93c896b011ecafff418c9661dc4ed"},
{file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6021d296318cb6f9414b48e6a439a7f5d1f665464da507e8ff640848ee2a58a"},
{file = "aiohttp-3.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:239f975589a944eeb1bad26b8b140a59a3a320067fb3cd10b75c3092405a1372"},
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b7b30258348082826d274504fbc7c849959f1989d86c29bc355107accec6cfb"},
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2adf5c87ff6d8b277814a28a535b59e20bfea40a101db6b3bdca7e9926bc24"},
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a3d838441bebcf5cf442700e3963f58b5c33f015341f9ea86dcd7d503c07e2"},
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3a1ae66e3d0c17cf65c08968a5ee3180c5a95920ec2731f53343fac9bad106"},
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c69e77370cce2d6df5d12b4e12bdcca60c47ba13d1cbbc8645dd005a20b738b"},
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf56238f4bbf49dab8c2dc2e6b1b68502b1e88d335bea59b3f5b9f4c001475"},
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d1469f228cd9ffddd396d9948b8c9cd8022b6d1bf1e40c6f25b0fb90b4f893ed"},
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:45731330e754f5811c314901cebdf19dd776a44b31927fa4b4dbecab9e457b0c"},
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3fcb4046d2904378e3aeea1df51f697b0467f2aac55d232c87ba162709478c46"},
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8cf142aa6c1a751fcb364158fd710b8a9be874b81889c2bd13aa8893197455e2"},
{file = "aiohttp-3.9.5-cp39-cp39-win32.whl", hash = "sha256:7b179eea70833c8dee51ec42f3b4097bd6370892fa93f510f76762105568cf09"},
{file = "aiohttp-3.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:38d80498e2e169bc61418ff36170e0aad0cd268da8b38a17c4cf29d254a8b3f1"},
{file = "aiohttp-3.9.5.tar.gz", hash = "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551"},
]
[package.dependencies]
aiosignal = ">=1.1.2"
async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""}
attrs = ">=17.3.0"
frozenlist = ">=1.1.1"
multidict = ">=4.5,<7.0"
yarl = ">=1.0,<2.0"
[package.extras]
speedups = ["Brotli", "aiodns", "brotlicffi"]
[[package]]
name = "aiohttp-cors"
version = "0.7.0"
description = "CORS support for aiohttp"
optional = false
python-versions = "*"
files = [
{file = "aiohttp-cors-0.7.0.tar.gz", hash = "sha256:4d39c6d7100fd9764ed1caf8cebf0eb01bf5e3f24e2e073fda6234bc48b19f5d"},
{file = "aiohttp_cors-0.7.0-py3-none-any.whl", hash = "sha256:0451ba59fdf6909d0e2cd21e4c0a43752bc0703d33fc78ae94d9d9321710193e"},
]
[package.dependencies]
aiohttp = ">=1.1"
[[package]]
name = "aiohttp-jinja2"
version = "1.6"
description = "jinja2 template renderer for aiohttp.web (http server for asyncio)"
optional = false
python-versions = ">=3.8"
files = [
{file = "aiohttp-jinja2-1.6.tar.gz", hash = "sha256:a3a7ff5264e5bca52e8ae547bbfd0761b72495230d438d05b6c0915be619b0e2"},
{file = "aiohttp_jinja2-1.6-py3-none-any.whl", hash = "sha256:0df405ee6ad1b58e5a068a105407dc7dcc1704544c559f1938babde954f945c7"},
]
[package.dependencies]
aiohttp = ">=3.9.0"
jinja2 = ">=3.0.0"
[[package]]
name = "aiosignal"
version = "1.3.1"
description = "aiosignal: a list of registered asynchronous callbacks"
optional = false
python-versions = ">=3.7"
files = [
{file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"},
{file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"},
]
[package.dependencies]
frozenlist = ">=1.1.0"
[[package]]
name = "altgraph"
version = "0.17.4"
description = "Python graph (network) package"
optional = false
python-versions = "*"
files = [
{file = "altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff"},
{file = "altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406"},
]
[[package]]
name = "async-timeout"
version = "4.0.3"
description = "Timeout context manager for asyncio programs"
optional = false
python-versions = ">=3.7"
files = [
{file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"},
{file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
]
[[package]]
name = "attrs"
version = "23.2.0"
description = "Classes Without Boilerplate"
optional = false
python-versions = ">=3.7"
files = [
{file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"},
{file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"},
]
[package.extras]
cov = ["attrs[tests]", "coverage[toml] (>=5.3)"]
dev = ["attrs[tests]", "pre-commit"]
docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"]
tests = ["attrs[tests-no-zope]", "zope-interface"]
tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"]
tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"]
[[package]]
name = "certifi"
version = "2024.2.2"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
files = [
{file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"},
{file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"},
]
[[package]]
name = "frozenlist"
version = "1.4.1"
description = "A list-like structure which implements collections.abc.MutableSequence"
optional = false
python-versions = ">=3.8"
files = [
{file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"},
{file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"},
{file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"},
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"},
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"},
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"},
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"},
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"},
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"},
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"},
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"},
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"},
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"},
{file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"},
{file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"},
{file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"},
{file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"},
{file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"},
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"},
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"},
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"},
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"},
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"},
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"},
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"},
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"},
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"},
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"},
{file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"},
{file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"},
{file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"},
{file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"},
{file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"},
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"},
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"},
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"},
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"},
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"},
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"},
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"},
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"},
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"},
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"},
{file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"},
{file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"},
{file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"},
{file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"},
{file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"},
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"},
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"},
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"},
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"},
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"},
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"},
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"},
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"},
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"},
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"},
{file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"},
{file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"},
{file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"},
{file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"},
{file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"},
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"},
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"},
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"},
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"},
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"},
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"},
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"},
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"},
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"},
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"},
{file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"},
{file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"},
{file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"},
{file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"},
]
[[package]]
name = "idna"
version = "3.7"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.5"
files = [
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
]
[[package]]
name = "jinja2"
version = "3.1.3"
description = "A very fast and expressive template engine."
optional = false
python-versions = ">=3.7"
files = [
{file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"},
{file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"},
]
[package.dependencies]
MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]]
name = "macholib"
version = "1.16.3"
description = "Mach-O header analysis and editing"
optional = false
python-versions = "*"
files = [
{file = "macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c"},
{file = "macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30"},
]
[package.dependencies]
altgraph = ">=0.17"
[[package]]
name = "markupsafe"
version = "2.1.5"
description = "Safely add untrusted strings to HTML/XML markup."
optional = false
python-versions = ">=3.7"
files = [
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"},
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"},
{file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"},
{file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"},
{file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"},
{file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"},
{file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"},
{file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"},
{file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"},
{file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"},
{file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"},
{file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"},
{file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"},
{file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"},
{file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"},
{file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"},
{file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"},
{file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"},
{file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"},
{file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"},
{file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"},
{file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"},
{file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"},
{file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"},
{file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"},
{file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"},
{file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"},
{file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"},
{file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"},
{file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"},
{file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"},
{file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"},
{file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"},
{file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"},
{file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"},
{file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"},
{file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"},
{file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"},
{file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"},
{file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"},
{file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"},
{file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"},
{file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"},
{file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"},
{file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"},
{file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"},
{file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"},
{file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"},
{file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"},
{file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"},
{file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"},
{file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"},
{file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"},
{file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"},
{file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"},
{file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"},
{file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"},
{file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"},
{file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"},
{file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"},
]
[[package]]
name = "multidict"
version = "6.0.5"
description = "multidict implementation"
optional = false
python-versions = ">=3.7"
files = [
{file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"},
{file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"},
{file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"},
{file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"},
{file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"},
{file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"},
{file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"},
{file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"},
{file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"},
{file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"},
{file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"},
{file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"},
{file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"},
{file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"},
{file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"},
{file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"},
{file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"},
{file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"},
{file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"},
{file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"},
{file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"},
{file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"},
{file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"},
{file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"},
{file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"},
{file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"},
{file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"},
{file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"},
{file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"},
{file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"},
]
[[package]]
name = "nodeenv"
version = "1.8.0"
description = "Node.js virtual environment builder"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
files = [
{file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"},
{file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"},
]
[package.dependencies]
setuptools = "*"
[[package]]
name = "packaging"
version = "23.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.7"
files = [
{file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
]
[[package]]
name = "pefile"
version = "2023.2.7"
description = "Python PE parsing module"
optional = false
python-versions = ">=3.6.0"
files = [
{file = "pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"},
{file = "pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc"},
]
[[package]]
name = "pyinstaller"
version = "5.13.2"
description = "PyInstaller bundles a Python application and all its dependencies into a single package."
optional = false
python-versions = "<3.13,>=3.7"
files = [
{file = "pyinstaller-5.13.2-py3-none-macosx_10_13_universal2.whl", hash = "sha256:16cbd66b59a37f4ee59373a003608d15df180a0d9eb1a29ff3bfbfae64b23d0f"},
{file = "pyinstaller-5.13.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8f6dd0e797ae7efdd79226f78f35eb6a4981db16c13325e962a83395c0ec7420"},
{file = "pyinstaller-5.13.2-py3-none-manylinux2014_i686.whl", hash = "sha256:65133ed89467edb2862036b35d7c5ebd381670412e1e4361215e289c786dd4e6"},
{file = "pyinstaller-5.13.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:7d51734423685ab2a4324ab2981d9781b203dcae42839161a9ee98bfeaabdade"},
{file = "pyinstaller-5.13.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:2c2fe9c52cb4577a3ac39626b84cf16cf30c2792f785502661286184f162ae0d"},
{file = "pyinstaller-5.13.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c63ef6133eefe36c4b2f4daf4cfea3d6412ece2ca218f77aaf967e52a95ac9b8"},
{file = "pyinstaller-5.13.2-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:aadafb6f213549a5906829bb252e586e2cf72a7fbdb5731810695e6516f0ab30"},
{file = "pyinstaller-5.13.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:b2e1c7f5cceb5e9800927ddd51acf9cc78fbaa9e79e822c48b0ee52d9ce3c892"},
{file = "pyinstaller-5.13.2-py3-none-win32.whl", hash = "sha256:421cd24f26144f19b66d3868b49ed673176765f92fa9f7914cd2158d25b6d17e"},
{file = "pyinstaller-5.13.2-py3-none-win_amd64.whl", hash = "sha256:ddcc2b36052a70052479a9e5da1af067b4496f43686ca3cdda99f8367d0627e4"},
{file = "pyinstaller-5.13.2-py3-none-win_arm64.whl", hash = "sha256:27cd64e7cc6b74c5b1066cbf47d75f940b71356166031deb9778a2579bb874c6"},
{file = "pyinstaller-5.13.2.tar.gz", hash = "sha256:c8e5d3489c3a7cc5f8401c2d1f48a70e588f9967e391c3b06ddac1f685f8d5d2"},
]
[package.dependencies]
altgraph = "*"
macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""}
pyinstaller-hooks-contrib = ">=2021.4"
pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""}
setuptools = ">=42.0.0"
[package.extras]
encryption = ["tinyaes (>=1.0.0)"]
hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"]
[[package]]
name = "pyinstaller-hooks-contrib"
version = "2024.5"
description = "Community maintained hooks for PyInstaller"
optional = false
python-versions = ">=3.7"
files = [
{file = "pyinstaller_hooks_contrib-2024.5-py2.py3-none-any.whl", hash = "sha256:0852249b7fb1e9394f8f22af2c22fa5294c2c0366157969f98c96df62410c4c6"},
{file = "pyinstaller_hooks_contrib-2024.5.tar.gz", hash = "sha256:aa5dee25ea7ca317ad46fa16b5afc8dba3b0e43f2847e498930138885efd3cab"},
]
[package.dependencies]
packaging = ">=22.0"
setuptools = ">=42.0.0"
[[package]]
name = "pyright"
version = "1.1.361"
description = "Command line wrapper for pyright"
optional = false
python-versions = ">=3.7"
files = [
{file = "pyright-1.1.361-py3-none-any.whl", hash = "sha256:c50fc94ce92b5c958cfccbbe34142e7411d474da43d6c14a958667e35b9df7ea"},
{file = "pyright-1.1.361.tar.gz", hash = "sha256:1d67933315666b05d230c85ea8fb97aaa2056e4092a13df87b7765bb9e8f1a8d"},
]
[package.dependencies]
nodeenv = ">=1.6.0"
[package.extras]
all = ["twine (>=3.4.1)"]
dev = ["twine (>=3.4.1)"]
[[package]]
name = "pywin32-ctypes"
version = "0.2.2"
description = "A (partial) reimplementation of pywin32 using ctypes/cffi"
optional = false
python-versions = ">=3.6"
files = [
{file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"},
{file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"},
]
[[package]]
name = "setuptools"
version = "69.5.1"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"},
{file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"},
]
[package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]]
name = "watchdog"
version = "2.3.1"
description = "Filesystem events monitoring"
optional = false
python-versions = ">=3.6"
files = [
{file = "watchdog-2.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1f1200d4ec53b88bf04ab636f9133cb703eb19768a39351cee649de21a33697"},
{file = "watchdog-2.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:564e7739abd4bd348aeafbf71cc006b6c0ccda3160c7053c4a53b67d14091d42"},
{file = "watchdog-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:95ad708a9454050a46f741ba5e2f3468655ea22da1114e4c40b8cbdaca572565"},
{file = "watchdog-2.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a073c91a6ef0dda488087669586768195c3080c66866144880f03445ca23ef16"},
{file = "watchdog-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa8b028750b43e80eea9946d01925168eeadb488dfdef1d82be4b1e28067f375"},
{file = "watchdog-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:964fd236cd443933268ae49b59706569c8b741073dbfd7ca705492bae9d39aab"},
{file = "watchdog-2.3.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:91fd146d723392b3e6eb1ac21f122fcce149a194a2ba0a82c5e4d0ee29cd954c"},
{file = "watchdog-2.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:efe3252137392a471a2174d721e1037a0e6a5da7beb72a021e662b7000a9903f"},
{file = "watchdog-2.3.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:85bf2263290591b7c5fa01140601b64c831be88084de41efbcba6ea289874f44"},
{file = "watchdog-2.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8f2df370cd8e4e18499dd0bfdef476431bcc396108b97195d9448d90924e3131"},
{file = "watchdog-2.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ea5d86d1bcf4a9d24610aa2f6f25492f441960cf04aed2bd9a97db439b643a7b"},
{file = "watchdog-2.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6f5d0f7eac86807275eba40b577c671b306f6f335ba63a5c5a348da151aba0fc"},
{file = "watchdog-2.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b848c71ef2b15d0ef02f69da8cc120d335cec0ed82a3fa7779e27a5a8527225"},
{file = "watchdog-2.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0d9878be36d2b9271e3abaa6f4f051b363ff54dbbe7e7df1af3c920e4311ee43"},
{file = "watchdog-2.3.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4cd61f98cb37143206818cb1786d2438626aa78d682a8f2ecee239055a9771d5"},
{file = "watchdog-2.3.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3d2dbcf1acd96e7a9c9aefed201c47c8e311075105d94ce5e899f118155709fd"},
{file = "watchdog-2.3.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:03f342a9432fe08107defbe8e405a2cb922c5d00c4c6c168c68b633c64ce6190"},
{file = "watchdog-2.3.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7a596f9415a378d0339681efc08d2249e48975daae391d58f2e22a3673b977cf"},
{file = "watchdog-2.3.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:0e1dd6d449267cc7d6935d7fe27ee0426af6ee16578eed93bacb1be9ff824d2d"},
{file = "watchdog-2.3.1-py3-none-manylinux2014_i686.whl", hash = "sha256:7a1876f660e32027a1a46f8a0fa5747ad4fcf86cb451860eae61a26e102c8c79"},
{file = "watchdog-2.3.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:2caf77ae137935c1466f8cefd4a3aec7017b6969f425d086e6a528241cba7256"},
{file = "watchdog-2.3.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:53f3e95081280898d9e4fc51c5c69017715929e4eea1ab45801d5e903dd518ad"},
{file = "watchdog-2.3.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:9da7acb9af7e4a272089bd2af0171d23e0d6271385c51d4d9bde91fe918c53ed"},
{file = "watchdog-2.3.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:8a4d484e846dcd75e96b96d80d80445302621be40e293bfdf34a631cab3b33dc"},
{file = "watchdog-2.3.1-py3-none-win32.whl", hash = "sha256:a74155398434937ac2780fd257c045954de5b11b5c52fc844e2199ce3eecf4cf"},
{file = "watchdog-2.3.1-py3-none-win_amd64.whl", hash = "sha256:5defe4f0918a2a1a4afbe4dbb967f743ac3a93d546ea4674567806375b024adb"},
{file = "watchdog-2.3.1-py3-none-win_ia64.whl", hash = "sha256:4109cccf214b7e3462e8403ab1e5b17b302ecce6c103eb2fc3afa534a7f27b96"},
{file = "watchdog-2.3.1.tar.gz", hash = "sha256:d9f9ed26ed22a9d331820a8432c3680707ea8b54121ddcc9dc7d9f2ceeb36906"},
]
[package.extras]
watchmedo = ["PyYAML (>=3.10)"]
[[package]]
name = "yarl"
version = "1.9.4"
description = "Yet another URL library"
optional = false
python-versions = ">=3.7"
files = [
{file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"},
{file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"},
{file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"},
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"},
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"},
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"},
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"},
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"},
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"},
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"},
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"},
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"},
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"},
{file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"},
{file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"},
{file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"},
{file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"},
{file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"},
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"},
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"},
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"},
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"},
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"},
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"},
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"},
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"},
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"},
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"},
{file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"},
{file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"},
{file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"},
{file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"},
{file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"},
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"},
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"},
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"},
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"},
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"},
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"},
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"},
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"},
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"},
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"},
{file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"},
{file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"},
{file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"},
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"},
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"},
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"},
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"},
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"},
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"},
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"},
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"},
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"},
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"},
{file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"},
{file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"},
{file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"},
{file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"},
{file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"},
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"},
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"},
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"},
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"},
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"},
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"},
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"},
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"},
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"},
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"},
{file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"},
{file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"},
{file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"},
{file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"},
{file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"},
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"},
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"},
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"},
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"},
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"},
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"},
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"},
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"},
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"},
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"},
{file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"},
{file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"},
{file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"},
{file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"},
]
[package.dependencies]
idna = ">=2.0"
multidict = ">=4.0"
[metadata]
lock-version = "2.0"
python-versions = ">=3.10,<3.13"
content-hash = "b87af38959be15deb2e6af33ab7cb70e502d20ebeabaae0348f816bc4ee736c6"
+30
View File
@@ -0,0 +1,30 @@
import os
from PyInstaller.building.build_main import Analysis
from PyInstaller.building.api import EXE, PYZ
from PyInstaller.utils.hooks import copy_metadata
a = Analysis(
['main.py'],
datas=[
('locales', 'locales'),
('static', 'static'),
] + copy_metadata('decky_loader'),
hiddenimports=['logging.handlers', 'sqlite3', 'decky_plugin', 'decky'],
)
pyz = PYZ(a.pure, a.zipped_data)
noconsole = bool(os.getenv('DECKY_NOCONSOLE'))
name = "PluginLoader"
if noconsole:
name += "_noconsole"
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name=name,
upx=True,
console=not noconsole,
)
+43
View File
@@ -0,0 +1,43 @@
[tool.poetry]
name = "decky-loader"
version = "0.0.0" # the real version will be autogenerated
description = "A plugin loader for the Steam Deck"
license = "GPLv2"
authors = []
packages = [
{include = "decky_loader"},
{include = "decky_loader/main.py"}
]
include = ["decky_loader/static/*"]
[tool.poetry.dependencies]
python = ">=3.10,<3.13"
aiohttp = "^3.9.5"
aiohttp-jinja2 = "^1.5.1"
aiohttp-cors = "^0.7.0"
watchdog = "^2.1.7"
certifi = "*"
packaging = "^23.2"
multidict = "^6.0.5"
[tool.poetry.group.dev.dependencies]
pyinstaller = "^5.13.0"
pyright = "^1.1.335"
[tool.poetry.scripts]
decky-loader = 'decky_loader.main:main'
[tool.pyright]
strict = ["*"]
[tool.poetry-dynamic-versioning]
enable = true
[tool.poetry-dynamic-versioning.substitution]
# don't replace version in decky_plugin.py
files = []
[build-system]
requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
build-backend = "poetry_dynamic_versioning.backend"
-357
View File
@@ -1,357 +0,0 @@
import uuid
import os
from json.decoder import JSONDecodeError
from os.path import splitext
import re
from traceback import format_exc
from stat import FILE_ATTRIBUTE_HIDDEN
from asyncio import sleep, 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, get_tab
from pathlib import Path
from localplatform import ON_WINDOWS
import helpers
import subprocess
from localplatform import service_stop, service_start, get_home_path, get_username
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,
"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):
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, install_type=0):
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):
return await self.context.plugin_browser.request_multiple_plugin_installs(
requests=requests
)
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 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 = Path(path).resolve()
files, folders = [], []
#Resolving all files/folders in the requested directory
for file in path.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)
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('.') in include_ext:
if (is_hidden and include_hidden) or not is_hidden:
files.append({"file": file, "filest": filest, "is_dir": False})
# Filter logic
if filter_for is not None:
try:
if re.compile(filter_for):
files = filter(lambda file: re.search(filter_for, file.name) != None, files)
except re.error:
files = filter(lambda 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())
#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, 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 != 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("location.reload();", False, True, False)
self.logger.info("React DevTools disabled")
async def get_user_info(self) -> dict:
return {
"username": get_username(),
"path": get_home_path()
}
async def get_tab_id(self, name):
return (await get_tab(name)).id
-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 -4
View File
@@ -34,16 +34,14 @@ curl -L https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/di
cat > "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" <<- EOM
[Unit]
Description=SteamDeck Plugin Loader
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
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 -4
View File
@@ -34,16 +34,14 @@ curl -L https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/di
cat > "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" <<- EOM
[Unit]
Description=SteamDeck Plugin Loader
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
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
-3
View File
@@ -1,14 +1,11 @@
[Unit]
Description=SteamDeck Plugin Loader
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
Restart=always
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
WorkingDirectory=${HOMEBREW_FOLDER}/services
KillSignal=SIGKILL
Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER}
Environment=PRIVILEGED_PATH=${HOMEBREW_FOLDER}
Environment=LOG_LEVEL=DEBUG
-3
View File
@@ -1,14 +1,11 @@
[Unit]
Description=SteamDeck Plugin Loader
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
Restart=always
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
WorkingDirectory=${HOMEBREW_FOLDER}/services
KillSignal=SIGKILL
Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER}
Environment=PRIVILEGED_PATH=${HOMEBREW_FOLDER}
Environment=LOG_LEVEL=INFO
Generated
+175
View File
@@ -0,0 +1,175 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nix-github-actions": {
"inputs": {
"nixpkgs": [
"poetry2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1703863825,
"narHash": "sha256-rXwqjtwiGKJheXB43ybM8NwWB8rO2dSRrEqes0S7F5Y=",
"owner": "nix-community",
"repo": "nix-github-actions",
"rev": "5163432afc817cf8bd1f031418d1869e4c9d5547",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nix-github-actions",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1714763106,
"narHash": "sha256-DrDHo74uTycfpAF+/qxZAMlP/Cpe04BVioJb6fdI0YY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e9be42459999a253a9f92559b1f5b72e1b44c13d",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"poetry2nix": {
"inputs": {
"flake-utils": "flake-utils_2",
"nix-github-actions": "nix-github-actions",
"nixpkgs": [
"nixpkgs"
],
"systems": "systems_3",
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1714855626,
"narHash": "sha256-fqvhXqJVykGHr6OHJ2eLhmNr76vKYqrEnXErLJ5eUe8=",
"owner": "nix-community",
"repo": "poetry2nix",
"rev": "c8766d12a9efd0467998b887d6de6d838091f2b9",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "poetry2nix",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"poetry2nix": "poetry2nix"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_3": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"id": "systems",
"type": "indirect"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"poetry2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1714058656,
"narHash": "sha256-Qv4RBm4LKuO4fNOfx9wl40W2rBbv5u5m+whxRYUMiaA=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "c6aaf729f34a36c445618580a9f95a48f5e4e03f",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
+55
View File
@@ -0,0 +1,55 @@
{
description = "Decky development environment";
# pulls in the python deps from poetry
inputs = {
flake-utils.url = "github:numtide/flake-utils";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
poetry2nix = {
url = "github:nix-community/poetry2nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, flake-utils, poetry2nix }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
p2n = (poetry2nix.lib.mkPoetry2Nix { inherit pkgs; });
in {
devShells.default = (p2n.mkPoetryEnv {
projectDir = self + "/backend";
# pyinstaller fails to compile so precompiled it is
overrides = p2n.overrides.withDefaults (final: prev: {
pyinstaller = prev.pyinstaller.override { preferWheel = true; };
pyright = null;
});
}).env.overrideAttrs (oldAttrs: {
shellHook = ''
set -o noclobber
PYTHONPATH=`which python`
FILE=.vscode/settings.json
if [ -f "$FILE" ]; then
echo "$FILE already exists, not writing interpreter path to it."
else
echo "{\"python.defaultInterpreterPath\": \"''${PYTHONPATH}\"}" > "$FILE"
fi
'';
UV_USE_IO_URING = 0; # work around node#48444
buildInputs = with pkgs; [
nodejs_22
nodePackages.pnpm
poetry
# fixes local pyright not being able to see the pythonpath properly.
(pkgs.writeShellScriptBin "pyright" ''
${pkgs.pyright}/bin/pyright --pythonpath `which python3` "$@" '')
(pkgs.writeShellScriptBin "pyright-langserver" ''
${pkgs.pyright}/bin/pyright-langserver --pythonpath `which python3` "$@" '')
(pkgs.writeShellScriptBin "pyright-python" ''
${pkgs.pyright}/bin/pyright-python --pythonpath `which python3` "$@" '')
(pkgs.writeShellScriptBin "pyright-python-langserver" ''
${pkgs.pyright}/bin/pyright-python-langserver --pythonpath `which python3` "$@" '')
];
});
});
}
+4 -3
View File
@@ -1,9 +1,10 @@
module.exports = {
import importSort from 'prettier-plugin-import-sort';
export default {
semi: true,
trailingComma: 'all',
singleQuote: true,
printWidth: 120,
tabWidth: 2,
endOfLine: 'auto',
plugins: [require('prettier-plugin-import-sort')],
};
plugins: [importSort],
}
+35 -33
View File
@@ -1,41 +1,43 @@
{
"name": "decky_frontend",
"version": "2.1.1",
"name": "@decky/loader-frontend",
"private": true,
"license": "GPLV2",
"type": "module",
"scripts": {
"prepare": "cd .. && husky install frontend/.husky",
"build": "rollup -c",
"watch": "rollup -c -w",
"lint": "prettier -c src",
"typecheck": "tsc --noEmit",
"format": "prettier -c src -w"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^21.1.0",
"@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.5.0",
"@types/react": "16.14.0",
"@types/react-file-icon": "^1.0.1",
"@types/react-router": "5.1.18",
"@types/webpack": "^5.28.1",
"husky": "^8.0.3",
"i18next-parser": "^8.0.0",
"@decky/api": "^1.0.5",
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-image": "^3.0.3",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "^5.0.7",
"@rollup/plugin-typescript": "^11.1.6",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@types/react-file-icon": "^1.0.4",
"@types/react-router": "5.1.20",
"husky": "^9.0.11",
"i18next-parser": "^9.0.0",
"import-sort-style-module": "^6.0.0",
"inquirer": "^8.2.5",
"prettier": "^2.8.8",
"inquirer": "^9.2.23",
"prettier": "^3.3.2",
"prettier-plugin-import-sort": "^0.0.7",
"react": "16.14.0",
"react-dom": "16.14.0",
"rollup": "^2.79.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"rollup": "^4.18.0",
"rollup-plugin-delete": "^2.0.0",
"rollup-plugin-external-globals": "^0.6.1",
"rollup-plugin-polyfill-node": "^0.10.2",
"rollup-plugin-visualizer": "^5.9.2",
"tslib": "^2.5.3",
"typescript": "^4.9.5"
"rollup-plugin-external-globals": "^0.10.0",
"rollup-plugin-polyfill-node": "^0.13.0",
"rollup-plugin-visualizer": "^5.12.0",
"tslib": "^2.6.3",
"typescript": "^5.4.5"
},
"importSort": {
".js, .jsx, .ts, .tsx": {
@@ -44,14 +46,14 @@
}
},
"dependencies": {
"decky-frontend-lib": "3.21.1",
"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"
"@decky/ui": "^4.2.1",
"filesize": "^10.1.2",
"i18next": "^23.11.5",
"i18next-http-backend": "^2.5.2",
"react-file-icon": "^1.5.0",
"react-i18next": "^14.1.2",
"react-icons": "^5.2.1",
"react-markdown": "^9.0.1",
"remark-gfm": "^4.0.0"
}
}
+1663 -1783
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -43,6 +43,8 @@ export default defineConfig({
chunkFileNames: (chunkInfo) => {
return 'chunk-[hash].js';
},
sourcemap: true,
sourcemapPathTransform: (relativeSourcePath) => relativeSourcePath.replace(/^\.\.\//, `decky://decky/loader/`),
},
onwarn: function (message, handleWarning) {
if (hiddenWarnings.some((warning) => message.code === warning)) return;
@@ -0,0 +1,198 @@
import { sleep } from '@decky/ui';
import { FunctionComponent, useEffect, useReducer, useState } from 'react';
import { uninstallPlugin } from '../plugin';
import { VerInfo, doRestart, doShutdown } from '../updater';
import { ValveReactErrorInfo, getLikelyErrorSourceFromValveReactError } from '../utils/errors';
interface DeckyErrorBoundaryProps {
error: ValveReactErrorInfo;
errorKey: string;
identifier: string;
reset: () => void;
}
declare global {
interface Window {
SystemNetworkStore?: any;
}
}
export const startSSH = DeckyBackend.callable('utilities/start_ssh');
export const starrCEFForwarding = DeckyBackend.callable('utilities/allow_remote_debugging');
function ipToString(ip: number) {
return [(ip >>> 24) & 255, (ip >>> 16) & 255, (ip >>> 8) & 255, (ip >>> 0) & 255].join('.');
}
// Intentionally not localized since we can't really trust React here
const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error, identifier, reset }) => {
const [actionLog, addLogLine] = useReducer((log: string, line: string) => (log += '\n' + line), '');
const [actionsEnabled, setActionsEnabled] = useState<boolean>(true);
const [debugAllowed, setDebugAllowed] = useState<boolean>(true);
// Intentionally doesn't use DeckyState.
const [versionInfo, setVersionInfo] = useState<VerInfo>();
const [errorSource, wasCausedByPlugin, shouldReportToValve] = getLikelyErrorSourceFromValveReactError(error);
useEffect(() => {
if (!shouldReportToValve) DeckyPluginLoader.errorBoundaryHook.temporarilyDisableReporting();
DeckyPluginLoader.updateVersion().then(setVersionInfo);
}, []);
return (
<>
<style>
{`
*:has(> .deckyErrorBoundary) {
overflow: scroll !important;
}
`}
</style>
<div
style={{
overflow: 'auto',
marginLeft: '15px',
color: 'white',
fontSize: '16px',
userSelect: 'auto',
backgroundColor: 'black',
marginTop: '48px', // Incase this is a page
}}
className="deckyErrorBoundary"
>
<h1
style={{
fontSize: '20px',
display: 'inline-block',
userSelect: 'auto',
}}
>
An error occured while rendering this content.
</h1>
<pre style={{}}>
<code>
{identifier && `Error Reference: ${identifier}`}
{versionInfo?.current && `\nDecky Version: ${versionInfo.current}`}
</code>
</pre>
<p>This error likely occured in {errorSource}.</p>
{actionLog?.length > 0 && (
<pre>
<code>
Running actions...
{actionLog}
</code>
</pre>
)}
{actionsEnabled && (
<>
<h3>Actions: </h3>
<p>Use the touch screen.</p>
<div style={{ display: 'block', marginBottom: '5px' }}>
<button style={{ marginRight: '5px', padding: '5px' }} onClick={reset}>
Retry
</button>
<button
style={{ marginRight: '5px', padding: '5px' }}
onClick={() => {
addLogLine('Restarting Steam...');
SteamClient.User.StartRestart();
}}
>
Restart Steam
</button>
</div>
<div style={{ display: 'block', marginBottom: '5px' }}>
<button
style={{ marginRight: '5px', padding: '5px' }}
onClick={async () => {
setActionsEnabled(false);
addLogLine('Restarting Decky...');
doRestart();
await sleep(2000);
addLogLine('Reloading UI...');
}}
>
Restart Decky
</button>
<button
style={{ marginRight: '5px', padding: '5px' }}
onClick={async () => {
setActionsEnabled(false);
addLogLine('Stopping Decky...');
doShutdown();
await sleep(5000);
addLogLine('Restarting Steam...');
SteamClient.User.StartRestart();
}}
>
Disable Decky until next boot
</button>
</div>
{debugAllowed && (
<div style={{ display: 'block', marginBottom: '5px' }}>
<button
style={{ marginRight: '5px', padding: '5px' }}
onClick={async () => {
setDebugAllowed(false);
addLogLine('Enabling CEF debugger forwarding...');
await starrCEFForwarding();
addLogLine('Enabling SSH...');
await startSSH();
addLogLine('Ready for debugging!');
if (window?.SystemNetworkStore?.wirelessNetworkDevice?.ip4?.addresses?.[0]?.ip) {
const ip = ipToString(window.SystemNetworkStore.wirelessNetworkDevice.ip4.addresses[0].ip);
addLogLine(`CEF Debugger: http://${ip}:8081`);
addLogLine(`SSH: deck@${ip}`);
}
}}
>
Allow remote debugging and SSH until next boot
</button>
</div>
)}
{wasCausedByPlugin && (
<div style={{ display: 'block', marginBottom: '5px' }}>
{'\n'}
<button
style={{ marginRight: '5px', padding: '5px' }}
onClick={async () => {
setActionsEnabled(false);
addLogLine(`Uninstalling ${errorSource}...`);
await uninstallPlugin(errorSource);
await DeckyPluginLoader.frozenPluginsService.invalidate();
await DeckyPluginLoader.hiddenPluginsService.invalidate();
await sleep(1000);
addLogLine('Restarting Decky...');
doRestart();
await sleep(2000);
addLogLine('Restarting Steam...');
await sleep(500);
SteamClient.User.StartRestart();
}}
>
Uninstall {errorSource} and restart Decky
</button>
</div>
)}
</>
)}
<pre
style={{
marginTop: '15px',
opacity: 0.7,
userSelect: 'auto',
}}
>
<code>
{error.error.stack}
{'\n\n'}
Component Stack:
{error.info.componentStack}
</code>
</pre>
</div>
</>
);
};
export default DeckyErrorBoundary;
@@ -1,4 +1,4 @@
import { FC, createContext, useContext, useEffect, useState } from 'react';
import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
interface PublicDeckyGlobalComponentsState {
components: Map<string, FC>;
@@ -40,6 +40,7 @@ export const useDeckyGlobalComponentsState = () => useContext(DeckyGlobalCompone
interface Props {
deckyGlobalComponentsState: DeckyGlobalComponentsState;
children: ReactNode;
}
export const DeckyGlobalComponentsStateContextProvider: FC<Props> = ({
+2 -1
View File
@@ -1,4 +1,4 @@
import { ComponentType, FC, createContext, useContext, useEffect, useState } from 'react';
import { ComponentType, FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
import type { RouteProps } from 'react-router';
export interface RouterEntry {
@@ -71,6 +71,7 @@ export const useDeckyRouterState = () => useContext(DeckyRouterStateContext);
interface Props {
deckyRouterState: DeckyRouterState;
children: ReactNode;
}
export const DeckyRouterStateContextProvider: FC<Props> = ({ children, deckyRouterState }) => {
+10 -1
View File
@@ -1,4 +1,4 @@
import { FC, createContext, useContext, useEffect, useState } from 'react';
import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
import { DEFAULT_NOTIFICATION_SETTINGS, NotificationSettings } from '../notification-service';
import { Plugin } from '../plugin';
@@ -8,6 +8,7 @@ import { VerInfo } from '../updater';
interface PublicDeckyState {
plugins: Plugin[];
pluginOrder: string[];
frozenPlugins: string[];
hiddenPlugins: string[];
activePlugin: Plugin | null;
updates: PluginUpdateMapping | null;
@@ -26,6 +27,7 @@ export interface UserInfo {
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;
@@ -41,6 +43,7 @@ export class DeckyState {
return {
plugins: this._plugins,
pluginOrder: this._pluginOrder,
frozenPlugins: this._frozenPlugins,
hiddenPlugins: this._hiddenPlugins,
activePlugin: this._activePlugin,
updates: this._updates,
@@ -67,6 +70,11 @@ export class DeckyState {
this.notifyUpdate();
}
setFrozenPlugins(frozenPlugins: string[]) {
this._frozenPlugins = frozenPlugins;
this.notifyUpdate();
}
setHiddenPlugins(hiddenPlugins: string[]) {
this._hiddenPlugins = hiddenPlugins;
this.notifyUpdate();
@@ -126,6 +134,7 @@ export const useDeckyState = () => useContext(DeckyStateContext);
interface Props {
deckyState: DeckyState;
children?: ReactNode;
}
export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) => {
+13 -10
View File
@@ -1,6 +1,6 @@
import { ToastData, joinClassNames } from 'decky-frontend-lib';
import { FC, useEffect, useState } from 'react';
import { ReactElement } from 'react-markdown/lib/react-markdown';
import type { ToastData } from '@decky/api';
import { joinClassNames } from '@decky/ui';
import { FC, ReactElement, useEffect, useState } from 'react';
import { useDeckyToasterState } from './DeckyToasterState';
import Toast, { toastClasses } from './Toast';
@@ -19,7 +19,7 @@ const DeckyToaster: FC<DeckyToasterProps> = () => {
if (toasts.size > 0) {
const [activeToast] = toasts;
if (!renderedToast || activeToast != renderedToast.data) {
// TODO play toast sound
// TODO play toast soundReactElement
console.log('rendering toast', activeToast);
setRenderedToast({ component: <Toast key={Math.random()} toast={activeToast} />, data: activeToast });
}
@@ -28,13 +28,16 @@ const DeckyToaster: FC<DeckyToasterProps> = () => {
}
useEffect(() => {
// not actually node but TS is shit
let interval: NodeJS.Timer | null;
let interval: number | 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 () => {
@@ -1,12 +1,11 @@
import { ToastData } from 'decky-frontend-lib';
import { FC, createContext, useContext, useEffect, useState } from 'react';
import type { ToastData } from '@decky/api';
import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
interface PublicDeckyToasterState {
toasts: Set<ToastData>;
}
export class DeckyToasterState {
// TODO a set would be better
private _toasts: Set<ToastData> = new Set();
public eventBus = new EventTarget();
@@ -41,6 +40,7 @@ export const useDeckyToasterState = () => useContext(DeckyToasterContext);
interface Props {
deckyToasterState: DeckyToasterState;
children: ReactNode;
}
export const DeckyToasterStateContextProvider: FC<Props> = ({ children, deckyToasterState }) => {
-11
View File
@@ -1,11 +0,0 @@
import { VFC } from 'react';
interface Props {
url: string;
}
const LegacyPlugin: VFC<Props> = ({ url }) => {
return <iframe style={{ border: 'none', width: '100%', height: '100%' }} src={url}></iframe>;
};
export default LegacyPlugin;
+3 -3
View File
@@ -1,4 +1,4 @@
import { Focusable, Navigation } from 'decky-frontend-lib';
import { Focusable, Navigation } from '@decky/ui';
import { FunctionComponent, useRef } from 'react';
import ReactMarkdown, { Options as ReactMarkdownOptions } from 'react-markdown';
import remarkGfm from 'remark-gfm';
@@ -13,8 +13,8 @@ const Markdown: FunctionComponent<MarkdownProps> = (props) => {
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
div: (nodeProps) => <Focusable {...nodeProps.node.properties}>{nodeProps.children}</Focusable>,
a: (nodeProps) => {
div: (nodeProps: any) => <Focusable {...nodeProps.node.properties}>{nodeProps.children}</Focusable>,
a: (nodeProps: any) => {
const aRef = useRef<HTMLAnchorElement>(null);
return (
// TODO fix focus ring
+10 -17
View File
@@ -1,13 +1,5 @@
import {
ButtonItem,
Focusable,
PanelSection,
PanelSectionRow,
joinClassNames,
scrollClasses,
staticClasses,
} from 'decky-frontend-lib';
import { VFC, useEffect, useState } from 'react';
import { ButtonItem, ErrorBoundary, Focusable, PanelSection, PanelSectionRow } from '@decky/ui';
import { FC, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaEyeSlash } from 'react-icons/fa';
@@ -17,7 +9,7 @@ import NotificationBadge from './NotificationBadge';
import { useQuickAccessVisible } from './QuickAccessVisibleState';
import TitleView from './TitleView';
const PluginView: VFC = () => {
const PluginView: FC = () => {
const { hiddenPlugins } = useDeckyState();
const { plugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = useDeckyState();
const visible = useQuickAccessVisible();
@@ -36,11 +28,8 @@ const PluginView: VFC = () => {
return (
<Focusable onCancelButton={closeActivePlugin}>
<TitleView />
<div
className={joinClassNames(staticClasses.TabGroupPanel, scrollClasses.ScrollPanel, scrollClasses.ScrollY)}
style={{ height: '100%' }}
>
{(visible || activePlugin.alwaysRender) && activePlugin.content}
<div style={{ height: '100%', paddingTop: '16px' }}>
<ErrorBoundary>{(visible || activePlugin.alwaysRender) && activePlugin.content}</ErrorBoundary>
</div>
</Focusable>
);
@@ -48,7 +37,11 @@ const PluginView: VFC = () => {
return (
<>
<TitleView />
<div className={joinClassNames(staticClasses.TabGroupPanel, scrollClasses.ScrollPanel, scrollClasses.ScrollY)}>
<div
style={{
paddingTop: '16px',
}}
>
<PanelSection>
{pluginList
.filter((p) => p.content)
@@ -1,10 +1,10 @@
import { FC, createContext, useContext, useState } from 'react';
import { FC, ReactNode, createContext, useContext, useState } from 'react';
const QuickAccessVisibleState = createContext<boolean>(false);
export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState);
export const QuickAccessVisibleStateProvider: FC<{ tab: any }> = ({ children, tab }) => {
export const QuickAccessVisibleStateProvider: FC<{ tab: any; children: ReactNode }> = ({ children, tab }) => {
const initial = tab.initialVisibility;
const [visible, setVisible] = useState<boolean>(initial);
// HACK but i can't think of a better way to do this
+12 -6
View File
@@ -1,5 +1,6 @@
import { DialogButton, Focusable, Router, staticClasses } from 'decky-frontend-lib';
import { CSSProperties, VFC } from 'react';
import { DialogButton, Focusable, Router, staticClasses } from '@decky/ui';
import { CSSProperties, FC } 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 TitleView: FC = () => {
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>
);
};
+2 -1
View File
@@ -1,4 +1,5 @@
import { ToastData, findModule, joinClassNames } from 'decky-frontend-lib';
import type { ToastData } from '@decky/api';
import { findModule, joinClassNames } from '@decky/ui';
import { FunctionComponent } from 'react';
interface ToastProps {
+1 -1
View File
@@ -1,4 +1,4 @@
import { Focusable, SteamSpinner } from 'decky-frontend-lib';
import { Focusable, SteamSpinner } from '@decky/ui';
import { FunctionComponent, ReactElement, ReactNode, Suspense } from 'react';
interface WithSuspenseProps {
@@ -2,24 +2,21 @@ import {
DialogButton,
DialogCheckbox,
DialogCheckboxProps,
Export,
Marquee,
Menu,
MenuItem,
findModuleChild,
findModuleExport,
showContextMenu,
} from 'decky-frontend-lib';
} from '@decky/ui';
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];
}
}
});
// TODO add to dfl
const dropDownControlButtonClass = findModuleExport((e: Export) =>
e?.toString()?.includes('gamepaddropdown_DropDownControlButton'),
);
const DropdownMultiselectItem: FC<
{
@@ -62,7 +59,7 @@ const DropdownMultiselect: FC<{
const [itemsSelected, setItemsSelected] = useState<any>(selected);
const { t } = useTranslation();
const handleItemSelect = useCallback((checked, value) => {
const handleItemSelect = useCallback((checked: boolean, value: any) => {
setItemsSelected((x: any) =>
checked ? [...x.filter((y: any) => y !== value), value] : x.filter((y: any) => y !== value),
);
@@ -1,6 +1,7 @@
import { ConfirmModal, Navigation, QuickAccessTab } from 'decky-frontend-lib';
import { FC, useMemo, useState } from 'react';
import { ConfirmModal, Navigation, ProgressBarWithInfo, QuickAccessTab } from '@decky/ui';
import { FC, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaCheck, FaDownload } from 'react-icons/fa';
import { InstallType } from '../../plugin';
@@ -27,8 +28,42 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
closeModal,
}) => {
const [loading, setLoading] = useState<boolean>(false);
const [percentage, setPercentage] = useState<number>(0);
const [pluginsCompleted, setPluginsCompleted] = useState<string[]>([]);
const [pluginInProgress, setInProgress] = useState<string | null>();
const [downloadInfo, setDownloadInfo] = useState<string | null>(null);
const { t } = useTranslation();
function updateDownloadState(percent: number, trans_text: string | undefined, trans_info: Record<string, string>) {
setPercentage(percent);
if (trans_text === undefined) {
setDownloadInfo(null);
} else {
setDownloadInfo(t(trans_text, trans_info));
}
}
function startDownload(name: string) {
setInProgress(name);
setPercentage(0);
}
function finishDownload(name: string) {
setPluginsCompleted((list) => [...list, name]);
}
useEffect(() => {
DeckyBackend.addEventListener('loader/plugin_download_info', updateDownloadState);
DeckyBackend.addEventListener('loader/plugin_download_start', startDownload);
DeckyBackend.addEventListener('loader/plugin_download_finish', finishDownload);
return () => {
DeckyBackend.removeEventListener('loader/plugin_download_info', updateDownloadState);
DeckyBackend.removeEventListener('loader/plugin_download_start', startDownload);
DeckyBackend.removeEventListener('loader/plugin_download_finish', finishDownload);
};
}, []);
// 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 => {
@@ -46,7 +81,7 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
setLoading(true);
await onOK();
setTimeout(() => Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky), 250);
setTimeout(() => window.DeckyPluginLoader.checkPluginUpdates(), 1000);
setTimeout(() => DeckyPluginLoader.checkPluginUpdates(), 1000);
}}
onCancel={async () => {
await onCancel();
@@ -66,7 +101,10 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
return (
<li key={i} style={{ display: 'flex', flexDirection: 'column' }}>
<div>{description}</div>
<span>
{description}{' '}
{(pluginsCompleted.includes(name) && <FaCheck />) || (name === pluginInProgress && <FaDownload />)}
</span>
{hash === 'False' && (
<div style={{ color: 'red', paddingLeft: '10px' }}>{t('PluginInstallModal.no_hash')}</div>
)}
@@ -74,6 +112,17 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
);
})}
</ul>
{/* TODO: center the progress bar and make it 80% width */}
{loading && (
<ProgressBarWithInfo
// when the key changes, react considers this a new component so resets the progress without the smoothing animation
key={pluginInProgress}
bottomSeparator="none"
focusable={false}
nProgress={percentage}
sOperationText={downloadInfo}
/>
)}
</div>
</ConfirmModal>
);
@@ -1,5 +1,5 @@
import { ConfirmModal, Navigation, QuickAccessTab } from 'decky-frontend-lib';
import { FC, useState } from 'react';
import { ConfirmModal, Navigation, ProgressBarWithInfo, QuickAccessTab } from '@decky/ui';
import { FC, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import TranslationHelper, { TranslationClass } from '../../utils/TranslationHelper';
@@ -24,8 +24,26 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
closeModal,
}) => {
const [loading, setLoading] = useState<boolean>(false);
const [percentage, setPercentage] = useState<number>(0);
const [downloadInfo, setDownloadInfo] = useState<string | null>(null);
const { t } = useTranslation();
function updateDownloadState(percent: number, trans_text: string | undefined, trans_info: Record<string, string>) {
setPercentage(percent);
if (trans_text === undefined) {
setDownloadInfo(null);
} else {
setDownloadInfo(t(trans_text, trans_info));
}
}
useEffect(() => {
DeckyBackend.addEventListener('loader/plugin_download_info', updateDownloadState);
return () => {
DeckyBackend.removeEventListener('loader/plugin_download_info', updateDownloadState);
};
}, []);
return (
<ConfirmModal
bOKDisabled={loading}
@@ -34,7 +52,7 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
setLoading(true);
await onOK();
setTimeout(() => Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky), 250);
setTimeout(() => window.DeckyPluginLoader.checkPluginUpdates(), 1000);
setTimeout(() => DeckyPluginLoader.checkPluginUpdates(), 1000);
}}
onCancel={async () => {
await onCancel();
@@ -42,10 +60,10 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
strTitle={
<div>
<TranslationHelper
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
trans_text="title"
i18n_args={{ artifact: artifact }}
install_type={installType}
transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
transText="title"
i18nArgs={{ artifact: artifact }}
installType={installType}
/>
</div>
}
@@ -53,17 +71,17 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
loading ? (
<div>
<TranslationHelper
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
trans_text="button_processing"
install_type={installType}
transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
transText="button_processing"
installType={installType}
/>
</div>
) : (
<div>
<TranslationHelper
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
trans_text="button_idle"
install_type={installType}
transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
transText="button_idle"
installType={installType}
/>
</div>
)
@@ -71,15 +89,23 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
>
<div>
<TranslationHelper
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
trans_text="desc"
i18n_args={{
transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
transText="desc"
i18nArgs={{
artifact: artifact,
version: version,
}}
install_type={installType}
installType={installType}
/>
</div>
{loading && (
<ProgressBarWithInfo
layout="inline"
bottomSeparator="none"
nProgress={percentage}
sOperationText={downloadInfo}
/>
)}
{hash == 'False' && <span style={{ color: 'red' }}>{t('PluginInstallModal.no_hash')}</span>}
</ConfirmModal>
);
@@ -1,6 +1,8 @@
import { ConfirmModal } from 'decky-frontend-lib';
import { ConfirmModal } from '@decky/ui';
import { FC } from 'react';
import { uninstallPlugin } from '../../plugin';
interface PluginUninstallModalProps {
name: string;
title: string;
@@ -14,10 +16,12 @@ const PluginUninstallModal: FC<PluginUninstallModalProps> = ({ name, title, butt
<ConfirmModal
closeModal={closeModal}
onOK={async () => {
await window.DeckyPluginLoader.callServerMethod('uninstall_plugin', { name });
await uninstallPlugin(name);
// uninstalling a plugin resets the 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.hiddenPluginsService.invalidate();
await DeckyPluginLoader.frozenPluginsService.invalidate();
await DeckyPluginLoader.hiddenPluginsService.invalidate();
closeModal?.();
}}
strTitle={title}
strOKButtonText={buttonText}
@@ -10,10 +10,10 @@ import {
SteamSpinner,
TextField,
ToggleField,
} from 'decky-frontend-lib';
} from '@decky/ui';
import { filesize } from 'filesize';
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react';
import { FileIcon, defaultStyles } from 'react-file-icon';
import { DefaultExtensionType, FileIcon, defaultStyles } from 'react-file-icon';
import { useTranslation } from 'react-i18next';
import { FaArrowUp, FaFolder } from 'react-icons/fa';
@@ -95,29 +95,20 @@ const sortOptions = [
},
];
function getList(
path: string,
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,
include_folders: includeFolders,
include_ext: includeExt ? includeExt : [],
include_hidden: includeHidden,
order_by: orderBy,
filter_for: filterFor,
page: pageNumber,
max: max,
});
}
const getList = DeckyBackend.callable<
[
path: string,
includeFiles?: boolean,
includeFolders?: boolean,
includeExt?: string[] | null,
includeHidden?: boolean,
orderBy?: SortOptions,
filterFor?: RegExp | ((file: File) => boolean) | null,
pageNumber?: number,
max?: number,
],
FileListing
>('utilities/filepicker_ls');
const iconStyles = {
paddingRight: '10px',
@@ -126,20 +117,20 @@ const iconStyles = {
const FilePicker: FunctionComponent<FilePickerProps> = ({
startPath,
//What are we allowing to show in the file picker
// What are we allowing to show in the file picker
includeFiles = true,
includeFolders = true,
//Parameter for specifying a specific filename match
// Parameter for specifying a specific filename match
filter = undefined,
//Filter for specific extensions as an array
// Filter for specific extensions as an array
validFileExtensions = undefined,
//Allow to override the fixed extension above
// Allow to override the fixed extension above
allowAllFiles = true,
//If we need to show hidden files and folders (both Win and Linux should work)
// 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
// How many files per page to show, default 1000
max = 1000,
//Which picking option to select by default
// Which picking option to select by default
fileSelType = FileSelectionType.FOLDER,
onSubmit,
closeModal,
@@ -190,21 +181,27 @@ const FilePicker: FunctionComponent<FilePickerProps> = ({
useEffect(() => {
(async () => {
setLoading(true);
const listing = await getList(
path,
includeFiles,
includeFolders,
selectedExts,
showHidden,
sort,
filter,
page,
max,
);
if (!listing.success) {
try {
const listing = await getList(
path,
includeFiles,
includeFolders,
selectedExts,
showHidden,
sort,
filter,
page,
max,
);
setRawError(null);
setError(FileErrorTypes.None);
setFiles(listing.files);
setLoading(false);
setListing(listing);
logger.log('reloaded', path, listing);
} catch (theError: any) {
setListing({ files: [], realpath: path, total: 0 });
setLoading(false);
const theError = listing.result as string;
switch (theError) {
case theError.match(/\[Errno\s2.*/i)?.input:
case theError.match(/\[WinError\s3.*/i)?.input:
@@ -220,14 +217,7 @@ const FilePicker: FunctionComponent<FilePickerProps> = ({
}
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);
})();
}, [error, path, includeFiles, includeFolders, showHidden, sort, selectedExts, page]);
@@ -316,7 +306,12 @@ const FilePicker: FunctionComponent<FilePickerProps> = ({
) : (
<div style={iconStyles}>
{file.realpath.includes('.') ? (
<FileIcon {...defaultStyles[extension]} {...styleDefObj[extension]} extension={''} />
<FileIcon
{...defaultStyles[extension as DefaultExtensionType]}
// @ts-expect-error
{...styleDefObj[extension]}
extension={''}
/>
) : (
<FileIcon />
)}
@@ -1,6 +1,7 @@
import { Patch, findModuleChild, replacePatch, sleep } from 'decky-frontend-lib';
import { Export, Patch, findModuleExport, replacePatch, sleep } from '@decky/ui';
import Logger from '../../../../logger';
import { FileSelectionType } from '..';
const logger = new Logger('LibraryPatch');
@@ -13,8 +14,11 @@ function rePatch() {
const details = window.appDetailsStore.GetAppDetails(appid);
logger.debug('game details', details);
// strShortcutStartDir
const file = await window.DeckyPluginLoader.openFilePicker(
const file = await DeckyPluginLoader.openFilePicker(
FileSelectionType.FILE,
details?.strShortcutStartDir.replaceAll('"', '') || '/',
true,
true,
);
logger.debug('user selected', file);
window.SteamClient.Apps.SetShortcutExe(appid, JSON.stringify(file.path));
@@ -35,12 +39,7 @@ export default async function libraryPatch() {
let History: any;
while (!History) {
History = findModuleChild((m) => {
if (typeof m !== 'object') return undefined;
for (let prop in m) {
if (m[prop]?.m_history) return m[prop].m_history;
}
});
History = findModuleExport((e: Export) => e.m_history)?.m_history;
if (!History) {
logger.debug('Waiting 5s for history to become available.');
await sleep(5000);
@@ -1,4 +1,4 @@
import { Focusable, updaterFieldClasses } from 'decky-frontend-lib';
import { Focusable, updaterFieldClasses } from '@decky/ui';
import { FunctionComponent, ReactNode } from 'react';
interface InlinePatchNotesProps {
+15 -3
View File
@@ -1,7 +1,7 @@
import { SidebarNavigation } from 'decky-frontend-lib';
import { SidebarNavigation } from '@decky/ui';
import { lazy } from 'react';
import { useTranslation } from 'react-i18next';
import { FaCode, FaPlug } from 'react-icons/fa';
import { FaCode, FaFlask, FaPlug } from 'react-icons/fa';
import { useSetting } from '../../utils/hooks/useSetting';
import DeckyIcon from '../DeckyIcon';
@@ -10,6 +10,7 @@ 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);
@@ -24,7 +25,7 @@ export default function SettingsPage() {
},
{
title: t('SettingsIndex.plugins_title'),
content: <PluginList />,
content: <PluginList isDeveloper={isDeveloper} />,
route: '/decky/settings/plugins',
icon: <FaPlug />,
},
@@ -39,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} />;
@@ -7,7 +7,7 @@ import {
Navigation,
TextField,
Toggle,
} from 'decky-frontend-lib';
} from '@decky/ui';
import { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaFileArchive, FaLink, FaReact, FaSteamSymbol, FaTerminal } from 'react-icons/fa';
@@ -28,22 +28,17 @@ const installFromZip = async () => {
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);
});
DeckyPluginLoader.openFilePicker(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);
},
);
};
const getTabID = DeckyBackend.callable<[name: string], string>('utilities/get_tab_id');
export default function DeveloperSettings() {
const [enableValveInternal, setEnableValveInternal] = useSetting<boolean>('developer.valve_internal', false);
const [reactDevtoolsEnabled, setReactDevtoolsEnabled] = useSetting<boolean>('developer.rdt.enabled', false);
@@ -91,13 +86,13 @@ export default function DeveloperSettings() {
>
<DialogButton
onClick={async () => {
let res = await window.DeckyPluginLoader.callServerMethod('get_tab_id', { name: 'SharedJSContext' });
if (res.success) {
try {
let tabId = await getTabID('SharedJSContext');
Navigation.NavigateToExternalWeb(
'localhost:8080/devtools/inspector.html?ws=localhost:8080/devtools/page/' + res.result,
'localhost:8080/devtools/inspector.html?ws=localhost:8080/devtools/page/' + tabId,
);
} else {
console.error('Unable to find ID for SharedJSContext tab ', res.result);
} catch (e) {
console.error('Unable to find ID for SharedJSContext tab ', e);
Navigation.NavigateToExternalWeb('localhost:8080');
}
}}
@@ -1,17 +1,22 @@
import { Dropdown, Field } from 'decky-frontend-lib';
import { Dropdown, Field } from '@decky/ui';
import { FunctionComponent } from 'react';
import { useTranslation } from 'react-i18next';
import Logger from '../../../../logger';
import { callUpdaterMethod } from '../../../../updater';
import { checkForUpdates } from '../../../../updater';
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<{}> = () => {
@@ -24,20 +29,20 @@ const BranchSelect: FunctionComponent<{}> = () => {
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
// 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: tBranches[UpdateBranch[branch]],
data: UpdateBranch[branch],
label: tBranches[branch as number],
data: branch,
}))}
selectedOption={selectedBranch}
onChange={async (newVal) => {
await setSelectedBranch(newVal.data);
callUpdaterMethod('check_for_updates');
checkForUpdates();
logger.log('switching branches!');
}}
/>
@@ -1,4 +1,4 @@
import { Field, Toggle } from 'decky-frontend-lib';
import { Field, Toggle } from '@decky/ui';
import { FC } from 'react';
import { useTranslation } from 'react-i18next';
@@ -6,7 +6,7 @@ import { useDeckyState } from '../../../DeckyState';
const NotificationSettings: FC = () => {
const { notificationSettings } = useDeckyState();
const notificationService = window.DeckyPluginLoader.notificationService;
const notificationService = DeckyPluginLoader.notificationService;
const { t } = useTranslation();
@@ -1,4 +1,4 @@
import { Field, Toggle } from 'decky-frontend-lib';
import { Field, Toggle } from '@decky/ui';
import { useTranslation } from 'react-i18next';
import { FaChrome } from 'react-icons/fa';
@@ -18,8 +18,8 @@ export default function RemoteDebuggingSettings() {
value={allowRemoteDebugging || false}
onChange={(toggleValue) => {
setAllowRemoteDebugging(toggleValue);
if (toggleValue) window.DeckyPluginLoader.callServerMethod('allow_remote_debugging');
else window.DeckyPluginLoader.callServerMethod('disallow_remote_debugging');
if (toggleValue) DeckyBackend.call('utilities/allow_remote_debugging');
else DeckyBackend.call('utilities/disallow_remote_debugging');
}}
/>
</Field>
@@ -1,4 +1,4 @@
import { Dropdown, Field, TextField } from 'decky-frontend-lib';
import { Dropdown, Field, TextField } from '@decky/ui';
import { FunctionComponent } from 'react';
import { useTranslation } from 'react-i18next';
import { FaShapes } from 'react-icons/fa';
@@ -26,10 +26,10 @@ const StoreSelect: FunctionComponent<{}> = () => {
<Field label={t('StoreSelect.store_channel.label')} childrenContainerWidth={'fixed'}>
<Dropdown
rgOptions={Object.values(Store)
.filter((store) => typeof store == 'string')
.filter((store) => typeof store == 'number')
.map((store) => ({
label: tStores[Store[store]],
data: Store[store],
label: tStores[store as number],
data: store,
}))}
selectedOption={selectedStore}
onChange={async (newVal) => {

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