Compare commits

...

494 Commits

Author SHA1 Message Date
Jonas Dellinger c5229c6a62 adjust some small store stylings (#471) 2023-05-30 16:35:42 -07:00
Marco Rodolfi c631d40aa3 Missed a toaster for the react tools 2023-05-30 19:32:41 +02:00
Jonas Dellinger d21b221575 quick fix: overwrite plugin list marginTop to be always 0 2023-05-29 18:40:17 +02:00
Jonas Dellinger 010feddf36 Add update all button to plugin list (#466) 2023-05-29 09:29:36 -07:00
WerWolvTranslationBot 5114bb5711 Translations update from Weblate (#467)
* Translated using Weblate (Italian)

Currently translated at 100.0% (100 of 100 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (100 of 100 strings)

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

---------

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

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

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

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

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

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

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

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

Updated by "Remove blank strings" hook in Weblate.

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

* Added translation using Weblate (German)

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

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

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

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

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

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

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

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

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

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

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

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

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Please, pnpm, cooperate

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

* Typo on translation variable

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

* Please, pnpm, cooperate

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

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

* First iteration for internationalization of the loader

* Cleanup node mess

* Cleanup node mess pt2

* Additional touches

* Latest decky changed merged into i18n and updated translation.

* Styling fixes

* Initial backend hosting implementation

* Added correct url path of the loopback server.

* Added correct url path of the loopback server.

* Some better namespaced text.

* Added whitelist for locales path.

* Refactor languages and fix hooks logic bugs.

* Small typo in language translation structure.

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

* Fix to languages

* Key fixes

* Additional language fixes.

* Additional json changes

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

* Typo in the middleware

* Remove unused imports

* Cleanup whitespaces.

* Import changes

* Revert "Import changes"

This reverts commit 8e8231950f.

* Update index.d.ts

* Clean up unused imports

* Delete pnpm-lock.yaml

* Update rollup.config.js

* Update PluginInstallModal.tsx

* Update index.tsx

* Update plugin-loader.tsx

* Update plugin-loader.tsx

* Revert "Delete pnpm-lock.yaml"

This reverts commit 3a39f36f21.

* Additional strings reworks.

* Fixes for issues coming from github merge.

* Fixes for master

* Styling fixes

* Styling pt2

* Missed a few strings in master,

* Styling fixes

* Additional master merge fixes.

* Final cleanup and adaptation to master.

* Final empty language cleanup and few string added

* Small changes to italian translation

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

* Fixed passing tag to translation.

* Disable debug output for reducing console spam.

* Return correct content type

* Small italian language change

* Added support for country code

* Fixed missing translation for uninstall popup.

* Fix class name shenanigans for  toast notification

* Update dependencies

* Fixed github workflow to include the new locales folder

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

* Missed a file name change

* Updated dev dependencies to latest version

* Missed a few dev dependencies

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

Messed up merge with a different main branch

* Messed up deletion of rollup config.

* Fix broken pnpm lock file

* Missed a localized string during the merge

* Fixed a parameter mistake in the uninstall text parameter

* Fix pnpm random issues

* Small italian language tweaks

* Fix wrong parameter passed to the uninstall function call

* Another fix on a wrong function parameter

* Additional translation text on the store and branch selection channels

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

* Reverted and reworked the last changes

* Distinguish events in UI for installing vs reinstalling plugins

* Additional fixes for reinstall prompt

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

* Missed a routing path in the backend

* Small bugfixes

* Small fixes

* Correctly adding the parameter to the request headers.

* Refactoring of the UI popup modal

* Fix pnpm shenanigans

* Final fixes for the install UI localization

* Clean up unnedeed backend code

* Small rework on text selection.

* Cleaned up parser configuration

* Removed extracttext dependency to pnpmsetup

* Merged translation and cleaned up parser

* Fixed JSON structure after manual merge.

* Added translation to the file picker

* Revert changes to PluginInstallModal

* Reworked the text modal for the final time

* Missed the proper linted text

* Missed the backend change

* Final branch cleanup

* Fixed small translation bleeding

Caused from the manual merge of _old.json files.

* fix extra space in browser.py

* fix extra newline in plugin-loader.tsx

* Cleanup i18next-parser.config.mjs

* Update plugin-loader.tsx

* Cleanup language files

* Better labeling of text

* Fixed language typos in BranchSelect

* Fixed language typos in StoreSelect

* Cleanup plugin-loader.tsx from unused imports

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

* Reimplemented this component as a functional component.

* Updated dependencies and lockfile

* Removed static route from main.py

Already handled in loader.py

* Small italian coherency fixes

* Fix small typography fixes on plugin name uninstall

* Fixed italian typo on removal popup

* Reenabled manual escaping value in i18next

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

* Fixed pnpm wankery

* Added a missed italian text translation string

---------

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

* Update README.md

* Update README.md

* Update README.md

* rip crankshaft, hope you come back one day

* Update README.md

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

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

* feat: implemented local ReorderableList

* feat: reoder complete except for usage of DFL

* switched to using dfl reorderableList

* fix: added missing file and removed frag

* updated to newest dfl

* Update defsettings.json

* fix: plugin order was missing on init

* fix: now await pluginOrder

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

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

* Use os.path rather than genericpath

* Split off socket management in plugin.py

* Don't specify multiprocessing start type

Default on linux is already fork

* Move all platform-specific functions to seperate files

TODO: make plugin.py platform agnostic

* fix import

* add backwards compat to helpers.py

* add backwards compatibility to helpers.py harder

* Testing autobuild for win

* Testing autobuild for win, try 2

* Testing autobuild for win, try 3

* Testing autobuild for win, try 4

* Create the plugins folder before attempting to use it

* Implement win get_username()

* Create win install script

* Fix branch guess from version

* Create .loader.version in install script

* Add .cmd shim to facilitate auto-restarts

* Properly fix branch guess from version

* Fix updater on windows

* Try 2 of fixing updates for windows

* Test

* pain

* Update install script

* Powershell doesn't believe in utf8

* Powershell good

* add ON_LINUX variable to localplatform

* Fix more merge issues

* test

* Move custom imports to main.py

* Move custom imports to after __main__ check 

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

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

split get_system_pythonpaths() on newline

* Remove whitespace in result of get_system_pythonpaths()

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

* Remove fork-specific urls

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

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

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

* Expose the plugin python module as .pyi stub interface

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

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

* Corrected number of iterations
2023-02-06 17:30:44 -08:00
Sky Leite 0b718daa47 Add lint job to build workflow (#363)
* Add lint job to build workflow

* Add prettier-plugin-import-sort

* Install prettier plugins before linting

* Use lint script from package.json

* Move linters to separate workflow

* Remove Python and Shell linters

* Remove popd

* Test that prettier properly fails the lint job
2023-02-03 19:40:29 -08:00
EMERALD 0929b9c5cb Specify linux/amd64 Docker architecture (#356) 2023-02-01 17:17:24 -08:00
EMERALD 43b2269ea7 Fix UI inconsistencies, various improvements (#357)
* Make version gray in plugin list

* Settings/store icons together & plugin list fix

* Navigation name/icon improvements

* Decky settings overhaul and other fixes

- Revert the tab icon to a plug
- Rename DeckyFlat function to DeckyIcon
- Add DialogBody to settings pages to improve scrolling
- Add remote debugging settings to the developer settings
- Fix React devtools interactions to work more easily
- Add spacing to React devtools description
- Specify Decky vs. plugin store
- Compact version information by update button
- Add current version to bottom of settings
- Remove unnecessary settings icons
- Change CEF debugger icon to Chrome (bug icon too generic, is Chromium)
- Make buttons/dropdowns in settings have fixed width
- Make download icon act/appear similar to Valve's for Deck

* Final UI adjustments

* Switch plugin settings icon to plug
2023-02-01 17:16:42 -08:00
Party Wumpus 0c4e27cd34 [readme] add installer issue to common issues (#359) 2023-02-01 12:31:44 -08:00
TrainDoctor 36cf85b08a Comment out un-needed pprint usage 2023-01-29 15:27:52 -08:00
TrainDoctor 994da868af Add python logging to browser and plugin 2023-01-29 15:16:16 -08:00
TrainDoctor 2e53fb217a Add better handling for unloading of plugins 2023-01-29 13:59:02 -08:00
Philipp Richter c2b76d9099 Expose useful env vars to plugin processes (#349)
* recommended paths for storing data
* improve helper functions
2023-01-22 16:54:05 -08:00
TrainDoctor c05e8f9ae0 Update build.yml 2023-01-22 16:33:06 -08:00
TrainDoctor 2dce0646bd Update README.md 2023-01-22 16:29:27 -08:00
Beebles 6569f1b268 Fix http_request not allowing bodys (#352) 2023-01-22 14:33:26 -08:00
EMERALD 3ebaac6752 Store and plugin installation visual improvements (#343)
* Redesign store, add comments for filtering

* Improve installation/uninstallation modals

* Fix store comment to be easier to fix

* Add source code info to about page
2023-01-19 18:00:42 -08:00
dependabot[bot] cbbd564860 Bump certifi from 2022.6.15 to 2022.12.7 (#345)
Bumps [certifi](https://github.com/certifi/python-certifi) from 2022.6.15 to 2022.12.7.
- [Release notes](https://github.com/certifi/python-certifi/releases)
- [Commits](https://github.com/certifi/python-certifi/compare/2022.06.15...2022.12.07)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-19 17:54:50 -08:00
TrainDoctor 635edf7f5b fix releases being called prereleases 2023-01-17 15:37:43 -08:00
Nox 1b6e18bcb3 Updated store CSS (#305)
* PluginCard Store CSS Update

* Fixing CSS

* Updated

* Removed padding
2023-01-16 14:43:16 -08:00
AAGaming 0ad0016c62 move the chown 2023-01-16 14:44:16 -05:00
AAGaming a2716449f9 fix missing await on chown_plugin_dir 2023-01-16 14:17:27 -05:00
AAGaming 649eed89c9 bump dfl 2023-01-16 09:13:30 -05:00
AAGaming 83680fffa2 indicate to DFL that the router has shim applied 2023-01-16 09:12:52 -05:00
AAGaming d695b90baf fix React DevTools again 2023-01-15 21:22:54 -05:00
TrainDoctor 5fdcc56409 Aa/bump dfl navigation fix jan2023 (#341)
* fix React DevTools

* bump DFL to fix Navigation

* Bump DFL and add shims

* fix shims not applying due to timing issue

Co-authored-by: AAGaming <aa@mail.catvibers.me>
2023-01-15 17:40:47 -08:00
Party Wumpus 915997d149 [readme] change terminal commands to point at decky-installer (#342)
* update readme to point at decky-installer for konsole scripts

* test change to make dark/light theme work properly

* Update README.md

* Update README.md

* revert <picture> changes as they didn't actually work
2023-01-14 07:49:23 -08:00
Party Wumpus e8b4c4a307 Fix the download button (#330)
* Remove .desktop file from build.yml

* Make download button link to decky-installer repo

* Point download button to the .desktop file not the .sh file 

woops

* Delete decky_installer.desktop

* Delete user_install_script.sh
2023-01-09 10:16:20 -08:00
Party Wumpus e92b66068a Use the new installer in the readme instructions (#324) 2023-01-08 13:16:44 -08:00
AAGaming b72b327610 Fix reloading UI on updates and restarting steam (#303) 2023-01-07 17:33:28 -08:00
Party Wumpus b8fdff8093 Add feature requests as an issue template (#318)
* Create feature_request.yml

* Update feature_request.yml
2023-01-06 09:23:05 -08:00
AAGaming 880b4c2f8f maybe working fix for jan 05 beta (#316) 2023-01-05 20:00:48 -08:00
TrainDoctor 34af340009 Update config.yml 2023-01-05 18:50:10 -08:00
Party Wumpus 80b6115f6f User Friendlier Installer (#297)
* Add files via upload

* Rename EasierInstallScript.sh to user_install_script.sh

* Add files via upload

* change so it works on deck instead of my desktop

* Update decky_installer.desktop

* make auto password setter work without the password

* Update user_install_script.sh

* make installer exit properly if user does not accept temp password

* Update user_install_script.sh

* add uninstall option

* Update user_install_script.sh

* Update user_install_script.sh

* Update user_install_script.sh

* "optimisation"

* Update user_install_script.sh

* Add sizing to all zenity prompts

* "optimization" part 2

* "Program now runs 50% faster"

:)

* Update user_install_script.sh

* Update user_install_script.sh

* Update user_install_script.sh

* Change text in branch selection in installer

'Select Branch' if choosing between release and prerelease
'Select Option' if choosing between release, prerelease and uninstall

* .desktop file points at where script is going to be

* add comments

* Change "installing" to "uninstalling"

* change it to ask for "sudo/admin" password

* Add secondary loading bar for download progress

Shamelessly stolen (with permission) from emudeck, who stole it from a random blog
No I don't know how that line works, and I don't think I want to.

* Make uninstaller tell user they can exit

* add default text to the download bar just in case

* silence script download

* silence password check
2023-01-02 08:52:11 -08:00
Party Wumpus 3bed83697e Add cef debugging to the installer scripts (#310)
* Update install_prerelease.sh

* Update install_release.sh
2023-01-02 08:46:46 -08:00
Party Wumpus 0ffef6e4bf Better bug report format (#312)
* Add files via upload

* Delete bug_report.md

* Update bug_report.yml

* Update bug_report.yml

* Update bug_report.yml

* Update bug_report.yml

* Update bug_report.yml

* Update bug_report.yml

* Update bug_report.yml
2023-01-02 08:45:42 -08:00
AAGaming 8810a014f3 somehow accidentally left this in 2022-12-29 13:11:11 -05:00
AAGaming 385552451b shut down steam instead of restarting it to avoid broken CEF debugger (gamescope will restart stean for us instead) 2022-12-28 12:24:28 -05:00
AAGaming c2c9d11c66 fix broken valveInternal when on a multi-user deck 2022-12-28 12:23:42 -05:00
Nik 0474095a40 Potentially fix locale issues (#284) 2022-12-16 06:23:04 -08:00
AAGaming 346f80beb3 bump DFL to fix modals, Router -> Navigation in some places 2022-12-15 21:16:22 -05:00
TrainDoctor 2a6bf75f02 Move back to python 3.10.2 in CI 2022-12-10 15:26:51 -08:00
jurassicplayer f73918c902 feat(MoreCustomizableToasts): Allow plugin developers to customize some toast properties (#268)
* Use settingsStore directly

* Change toast etype, add showToast/playSound

* Update DFL and rebase
2022-12-10 15:09:21 -08:00
TrainDoctor ea35af2050 Update build.yml 2022-12-08 15:18:44 -08:00
NGnius (Graham) 6232e3da58 Add custom CDN support for custom stores (#269)
* Add custom CDN support for custom stores

* Update Python for CI
2022-12-07 16:27:32 -08:00
TrainDoctor 35e46f9ccb Update build.yml 2022-12-07 14:31:09 -08:00
TrainDoctor 2b9a80c151 Update install_prerelease.sh 2022-12-04 19:05:29 -08:00
TrainDoctor a90ed38c89 Update install_release.sh 2022-12-04 19:05:16 -08:00
TrainDoctor 3653cf5640 Update plugin_loader-release.service 2022-12-04 19:05:01 -08:00
TrainDoctor 0db45ca71e Update plugin_loader-prerelease.service 2022-12-04 19:04:46 -08:00
AAGaming 16681fabb5 fix http requests 2022-11-19 22:33:51 -05:00
AAGaming c210523a22 fix handleWarning in rollup config 2022-11-19 20:07:08 -05:00
Marco Rodolfi 5d8601347a Fix for wrong path for settings json files (#258)
Co-authored-by: AAGaming <aa@mail.catvibers.me>
2022-11-19 19:34:38 -05:00
AAGaming 1e02fcf394 fix broken trycatch causing occasional injection failures 2022-11-19 19:22:30 -05:00
TrainDoctor f923306a7f Update issue templates 2022-11-19 14:19:33 -08:00
TrainDoctor 478fe32527 Revert "Fix for setting json files ending up in ~/homebrew"
This reverts commit aec7063139.
2022-11-15 15:01:26 -08:00
AAGaming 50764600c8 Refactoring in preparation for WebSockets (#254)
* Fix injector race conditions

* add some more tasks

* hide useless rollup warnings

* goodbye to clientsession errors

* completely fix desktop mode switch race condition

* fix typos and TS warning in plugin error handler

* fix chown error

* start debugger if needed and not already started

* fix get_steam_resource for the like 2 legacy plugins still using it lol

* add ClientOSError to get_tabs error handling
2022-11-15 13:44:24 -08:00
TrainDoctor aec7063139 Fix for setting json files ending up in ~/homebrew 2022-11-13 17:48:46 -08:00
TrainDoctor c9ee98e0c0 Fix desktop mode loop (#253)
* fix desktop mode making injector get stuck

* Fix imports and variable references

* Get data for all messages

Co-authored-by: AAGaming <aa@mail.catvibers.me>
2022-11-13 17:36:00 -08:00
TrainDoctor 093b064a4e Inline icon propely 2022-11-13 16:44:15 -08:00
TrainDoctor 2955681975 Show plugin as error and give guidance for uninstall 2022-11-13 16:31:05 -08:00
TrainDoctor de42639726 Add errored plugins to plugin array for uninstalls 2022-11-13 16:01:06 -08:00
TrainDoctor 17742e947a Fix import revert 2022-11-08 17:05:10 -08:00
TrainDoctor 898271b33d Revert "Disable store selection until PRs actually deploy to testing"
This reverts commit d389b403b5.
2022-11-06 15:37:02 -08:00
AAGaming b44896524f Fix on steam client beta, restart steam instead of reloading tab since that is broken 2022-11-04 21:49:05 -04:00
TrainDoctor db7bb236d8 facepalm 2022-10-30 18:45:18 -07:00
TrainDoctor 5e3de747d3 Systemd service updating (#240)
* Add services and updated installer files

* Loader updates service file during update!

* Testing update branch doesn't exist lol

* Update to dfl 3.7.12

* Fix services and add working service updater

* Revert services but replace their aliases

* Fix install scripts as well

* Move leftover service files to .systemd dir

* No wonder it's not trimming the file...

* fix whitespace

* Remove unused imports

* Remove another un-used import

Co-authored-by: AAGaming <aa@mail.catvibers.me>
2022-10-30 16:37:19 -07:00
TrainDoctor d389b403b5 Disable store selection until PRs actually deploy to testing 2022-10-30 10:38:49 -07:00
TrainDoctor bace5143d2 Merge Tabs and Injection Fixes, bring back native Valve toaster (#238)
* Bring back component patch-based tabshook

* better injection point

* finally fix dumb loading error

* fix QAM injection breaking after lock

* shut up typescript

* fix lock screen focusing issues

* Bring back the Valve toaster!

* Add support for stable steamos

* fix focus bug on lock screen but actually

* oops: remove extra console log

* shut up typescript again

* better fix for lockscreen bug

* better probably

* actually fix focus issues (WTF)

Co-authored-by: AAGaming <aa@mail.catvibers.me>
2022-10-30 10:32:05 -07:00
suchmememanyskill f5fc205384 Add timeout to get tabs request, wait for network online target before booting decky (#239)
* Add timeout to tabs get request

* Wait for network interfaces to be ready before booting decky
2022-10-29 15:13:19 -07:00
TrainDoctor 4d30339c34 Add StoreSelect component 2022-10-29 15:03:21 -07:00
TrainDoctor 5996a3f88b Remove unused nightly and tweak prerelease 2022-10-29 12:43:38 -07:00
AAGaming 1b635c74b1 chore(readme): link change for store, port clarification 2022-10-29 00:55:19 -04:00
AAGaming a9bd5079de remove ver suffix 2022-10-24 21:08:35 -04:00
AAGaming c1fabe5b35 pin pyinstaller to 5.5-py3 2022-10-24 21:07:49 -04:00
AAGaming ed82f51bb7 use same python ver as deck 2022-10-24 20:55:38 -04:00
AAGaming df1524e15f nevermind dont 2022-10-24 20:51:56 -04:00
AAGaming 2edd910df3 ci: upgrade python to 3.11 2022-10-24 20:48:58 -04:00
AAGaming 1cd69097ad import uuid in injector, update DFL 2022-10-24 20:41:27 -04:00
AAGaming 84c3b039c3 preview 10/21/2022 fixes (#234)
* initial fixes: everything working except toasts and patch notes

* tabshook changes, disable toaster for now

* prettier

* oops

* implement custom toaster because I am tired of Valve's shit

also fix QAM not injecting sometimes

* remove extra logging

* add findSP, fix patch notes, fix vscode screwup

* fix patch notes

* show error when plugin frontends fail to load

* add get_tab_lambda

* add css and has_element helpers to Tab

* small modals fixup

* Don't forceUpdate QuickAccess on stable

* add routes prop used to get tabs component

* add more dev utils to DFL global
2022-10-24 16:14:56 -07:00
TrainDoctor 2e6b3834da Properly utilize image_url from Store 2022-10-23 14:00:29 -07:00
TrainDoctor 6749c78ed7 During update, download updates first before removing old plugin files (#223)
* Remove old nightly support and unused logging

* Removed legacy code + added logic to account for offline update attempts

* Update backend/browser.py

Co-authored-by: AAGaming <aa@mail.catvibers.me>

* Update backend/browser.py

Co-authored-by: AAGaming <aa@mail.catvibers.me>

* Update frontend/src/toaster.tsx

Co-authored-by: AAGaming <aa@mail.catvibers.me>

* Use str instead of String (I was tired okay...)

* Remove false logic

* look for plugins not having remote_binary in pkg

Co-authored-by: AAGaming <aa@mail.catvibers.me>
2022-10-23 13:41:12 -07:00
TrainDoctor 4ad15568cd Merge drop-legacy to main. 2022-10-23 13:04:00 -07:00
TrainDoctor 58849b3002 Reduce amount of actions taken when using updateandrun 2022-10-22 19:11:00 -07:00
TrainDoctor 6346da6fe5 Actually utilize the unload function 2022-10-22 18:44:44 -07:00
TrainDoctor af51a29055 Added unload hook for plugins. 2022-10-22 18:36:49 -07:00
TrainDoctor c546a818f1 Send version when asking for plugin list 2022-10-22 16:52:48 -07:00
TrainDoctor 0226bd2bf8 Update build.yml 2022-10-22 13:02:23 -07:00
TrainDoctor 7b16b623c8 Fix toaster initialization 2022-10-16 15:52:21 -07:00
AAGaming 6e3c05072c Developer menu (#211)
* add settings utils to use settings outside of components

* initial implementation of developer menu

*  Add support for addScriptToEvaluateOnNewDocument

* React DevTools support

* increase chance of RDT successfully injecting

* Rewrite toaster hook to not re-create the window

* remove friends focus workaround because it's fixed

* Expose various DFL utilities as DFL in dev mode

* try to fix text field focuss

* move focusable to outside field

* add onTouchEnd and onClick to focusable

* Update pnpm-lock.yaml

Co-authored-by: FinalDoom <7464170-FinalDoom@users.noreply.gitlab.com>
Co-authored-by: TrainDoctor <traindoctor@protonmail.com>
2022-10-15 20:46:42 -07:00
TrainDoctor 9b405e4bdc Fix default pre-release bump 2022-10-15 11:31:53 -07:00
AAGaming 8007dd4dac bump decky-frontend-lib 2022-10-14 23:48:56 -04:00
AAGaming 91d4e5dfc3 fix the fix 2022-10-14 23:47:57 -04:00
AAGaming c885ee600d replace deprecated set-output in CI 2022-10-14 23:46:46 -04:00
AAGaming 739b57e100 add alwaysRender 2022-10-14 23:43:45 -04:00
AAGaming 87a7361dc7 Allow B button to close active plugin and return to menu. (#218) 2022-10-14 23:33:16 -04:00
AAGaming acdea6da44 pnpm uses frozen instead of ci 2022-10-14 23:29:32 -04:00
AAGaming f23ea5b841 IDIOT 2022-10-14 23:28:37 -04:00
AAGaming d51cd4605c wtf 2022-10-14 23:27:59 -04:00
AAGaming 7d73c7aa79 setup-pnpm action is bad, do it ourself 2022-10-14 23:26:42 -04:00
AAGaming fd187a6710 forgot with 2022-10-14 23:25:13 -04:00
AAGaming 43ef9e65ea this arg format is terrible 2022-10-14 23:24:39 -04:00
AAGaming 9233ee58c6 fix ci 2022-10-14 23:22:01 -04:00
AAGaming fd59456f8b more CI cleanup 2022-10-14 23:13:36 -04:00
AAGaming 9b241101dd Upgrade NodeJS to 18 in CI 2022-10-14 23:08:27 -04:00
AAGaming bebe9428a6 fix old toast patch to not re-create window 2022-10-14 22:52:46 -04:00
AAGaming 7445f066ed Revert "Rewrite toaster hook to not re-create the window (#217)"
This reverts commit 3ac0abc82b.
2022-10-14 22:41:46 -04:00
AAGaming 6e48aefce8 fix os.path 2022-10-14 20:44:59 -04:00
AAGaming 0bc0a0dadb remove friends focus workaround 2022-10-14 20:22:26 -04:00
AAGaming 3ac0abc82b Rewrite toaster hook to not re-create the window (#217) 2022-10-14 17:09:11 -07:00
EMERALD 618abec97a README rewrite (#214)
* Improved header

* Fix header screenshot distance, improve about

* Installation section improvements

* Add mouse and keyboard steps

* Formatting all README sections

* README SVG overhaul

* Move uninstallation to installation section

* Various clarifications

* Clarification on password creation

* Add common issues section

* Add port 1337 warning
2022-10-12 15:09:09 -07:00
TrainDoctor 2518d1a0b3 Update README.md 2022-10-08 18:10:03 -07:00
TrainDoctor 010e6a22ab don't test on live folks -_- 2022-10-08 17:25:34 -07:00
TrainDoctor 134b896e01 Append "-pre" before bumping pre-release version 2022-10-08 17:17:40 -07:00
TrainDoctor 047813b965 Merge branch 'update-ci' w/ needed updates 2022-10-08 17:08:36 -07:00
AAGaming dbcb549ae2 use hasattr instead of in 2022-10-08 19:04:09 -04:00
AAGaming d689614c78 possibly fix zip permissions 2022-10-08 17:57:51 -04:00
Devon Schneider ec907627b8 Use a conditional while loop for when the scroll root is not found. (#212)
* Changed back to a while loop for grabbing the scroll root\nAs recursive function exits before it hits 30 iterations in testing.

* Changed so if recursive limit is hit, it returns null instead of attempting to continue.

* Added log messages regarding recursion limits, as well as fixed recursion tracking.

* Removed errant return remaining from scroll root check.
2022-10-08 06:35:21 -07:00
AAGaming a3809222f9 disable file watcher until after 10 seconds 2022-10-07 22:38:20 -04:00
Party Wumpus 86dc706892 README: password typing clarification (#208)
While the video does *show* the password not appearing, he doesn't mention it.
This should reduce the occasional user confusion of passwords not appearing.
2022-10-07 09:35:59 -07:00
Eddie Dover 0e409a9f96 Add plugin description to Store PluginCard (#196)
* Add plugin description to Store PluginCard

The description for plugins is available on the web but not in the store, this attempts to fix the issue.

Unfortunately, my current env is completely Windows based so I cannot test this change locally, and setting up an Arch env in WSL2 is proving to be more time consuming than such a simple PR deserves.

* Removed prefix from description

* Apply suggestions from code review

Co-authored-by: Party Wumpus <48649272+PartyWumpus@users.noreply.github.com>

* Inline style fix.

Co-authored-by: Party Wumpus <48649272+PartyWumpus@users.noreply.github.com>

* Prettier formatting fix

* Apply suggestions from code review

Co-authored-by: Party Wumpus <48649272+PartyWumpus@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Party Wumpus <48649272+PartyWumpus@users.noreply.github.com>

Co-authored-by: Party Wumpus <48649272+PartyWumpus@users.noreply.github.com>
2022-10-05 14:24:02 -07:00
Devon Schneider d58001c323 Fixed an issue regarding the scroll root path (#201)
* Changed so searching for the scroll root node will look through siblings of children nodes.

* Added a missing await

Seem to have missed an await from when I was converting my proof-of-concept to an async function.

* Minor stylistic change

* Changed where program retries to find the scroll root.
2022-10-04 11:54:54 -07:00
TrainDoctor d727ba72f3 Fix outdated line about manual install 2022-10-02 10:56:06 -07:00
TrainDoctor fa028fa525 Update build.yml 2022-10-02 08:19:41 -07:00
TrainDoctor c947548064 Update build.yml 2022-10-02 08:11:00 -07:00
AAGaming 19d5527bdf ACTUALLY FIX the friends focus workaround 2022-10-02 10:29:33 -04:00
AAGaming ef51b96f08 fix styling on links in markdown 2022-10-02 10:14:51 -04:00
AAGaming 617916e8e5 actually fix focus issue on startup 2022-10-01 23:10:25 -04:00
TrainDoctor 6c4a4d0a44 Update install_release.sh 2022-10-01 19:04:05 -07:00
TrainDoctor bedcb0fb71 Update README.md 2022-10-01 18:58:59 -07:00
AAGaming 2461f52ca7 change console.log to styled log 2022-10-01 21:24:10 -04:00
AAGaming 3c00eb8cf4 fix friends menu focusing itself 2022-10-01 21:22:30 -04:00
TrainDoctor 21e1d8504a Update README.md 2022-10-01 16:17:40 -07:00
Party Wumpus ba93c4add2 Change 'PluginLoader' to 'Decky Loader' in readme (#190)
* (readme) Make store link go to beta.deckbrew + Change wording for clarification

* further change

* change line 26 to say plugin menu

* Change all 'PluginLoader' to 'Decky Loader' in readme

Seems like a reasonable change as Decky was rebranded a while ago
2022-10-01 14:41:09 -07:00
TrainDoctor 61fea41c8a Update package.json 2022-09-30 21:40:51 -07:00
TrainDoctor e40d3e4db5 Removal of legacy link 2022-09-30 21:31:19 -07:00
Lukas Senionis bbad6bf2be fix(pluginview): align icon and plugin name (#185) 2022-09-26 21:51:39 -07:00
Party Wumpus 4e04455163 (readme) Make store link go to beta.deckbrew + Change wording (#184)
* (readme) Make store link go to beta.deckbrew + Change wording for clarification

* further change

* change line 26 to say plugin menu
2022-09-25 10:04:50 -07:00
Dan Burkhardt 314292b042 updated installation steps to reflect confirmation and system reboot step (#180)
Co-authored-by: TrainDoctor <traindoctor@protonmail.com>
2022-09-21 15:05:18 -07:00
Aamir Tahir a264f36966 Add note about opening terminal to README (#176)
Quick note about how to open the terminal using the pre-installed Konsole app for users that are either new to linux or Steam Deck.
2022-09-21 14:56:45 -07:00
AAGaming 60c8c5db42 check for plugin updates after installing a plugin 2022-09-19 21:13:06 -04:00
Trent Callan 852c52c59a Grab Plugin path from find_plugin_folder plugin name is not always folder anme (#178) 2022-09-18 22:13:45 -07:00
Trent Callan 3136ad72ed Download Remote Binaries during Store Install (#177)
* Download Remote Binaries during store install.
Fix Manual Install Modal Type

* Use Unix Line endings. (Thanks WSL)

* Fix Merge Commit with main branch.
2022-09-18 17:20:19 -07:00
AAGaming 3700dd7437 fix plugin updating 2022-09-18 15:44:16 -04:00
AAGaming c6d48389c9 bump lib 2022-09-18 15:44:01 -04:00
AAGaming 490fc18008 enable debug logs in dev 2022-09-18 15:43:26 -04:00
AAGaming 797c7ea3b0 hopefully actually fix toaster 2022-09-18 15:03:59 -04:00
AAGaming 0f06bc1ef0 Updater UI tweaks, Markdown tweaks, bump lib 2022-09-18 14:53:36 -04:00
AAGaming c774451ff4 don't crash when plugin backends do 2022-09-18 14:51:17 -04:00
AAGaming 62a5bdbbb0 add more URLs to bypass authentication 2022-09-18 14:49:56 -04:00
AAGaming 7716c73014 fix plugin loading after install, move updater reloads to loader 2022-09-18 14:49:32 -04:00
AAGaming 8829adc5b6 update tasks 2022-09-18 14:48:08 -04:00
AAGaming 62bd3e76bd small changes making breakpoint debugging easier 2022-09-18 11:07:30 -04:00
AAGaming 9867d7bea0 Possibly actually fix the toasts issue 2022-09-18 10:17:39 -04:00
AAGaming c4d6731401 fix updater for new installs, fix file picker patch, fix scrolling on patch notes, fix tasks dir 2022-09-17 23:23:51 -04:00
AAGaming fded2fa8bf fix plugin install modal 2022-09-17 16:02:03 -04:00
TrainDoctor 90c523ec45 Update updater.py 2022-09-17 09:26:24 -07:00
AAGaming c5ccb4dfb8 fix TS errors in Toast 2022-09-17 11:36:53 -04:00
AAGaming 8b1925bc53 fix spacing [skip ci] 2022-09-17 11:35:31 -04:00
AAGaming a8c7c2f18f fix asyncio error on get_branch 2022-09-17 11:35:05 -04:00
AAGaming 463258febb wait for toaster ready, hopefully fix file browser patch 2022-09-16 18:49:35 -04:00
TrainDoctor 304fc0f94c Update build.yml 2022-09-11 15:40:19 -07:00
AAGaming b5b041fdee add file picker, add library file picker patch, bump lib, logger tweaks 2022-09-09 16:25:52 -04:00
AAGaming 9d980618a7 spice up js-side logs from store and branch select 2022-09-08 20:19:54 -04:00
Nik 6dad3f81e8 Updated screenshot 2022-09-08 23:22:47 +02:00
TrainDoctor adc1a792fb Whoopsie 2022-09-05 20:13:08 -07:00
TrainDoctor 6347ad0856 Added handling for release to pre-release, pre-rel to rel and rel to rel 2022-09-05 20:03:09 -07:00
TrainDoctor 1377d83023 Better handling for release to new pre-release 2022-09-05 19:37:33 -07:00
TrainDoctor 43d36d2b35 temp 2022-09-05 19:32:27 -07:00
AAGaming 591c58330c fix(toaster): fix toasts rendering multiple times 2022-09-05 14:28:52 -04:00
AAGaming 501145a210 fix(toasts): handle toasts ingame properly without useComposition hack 2022-09-05 11:24:04 -04:00
TrainDoctor a3659ba425 Oops I need that 2022-09-04 21:14:38 -07:00
TrainDoctor d1887870f5 return default value for out to "" 2022-09-04 21:03:45 -07:00
TrainDoctor 1892403044 better equivalency checking 2022-09-04 20:56:38 -07:00
TrainDoctor f5a1837227 OUT 2022-09-04 20:51:16 -07:00
TrainDoctor 97f95705f8 Fix pre-release, none 2022-09-04 20:39:57 -07:00
TrainDoctor 7c99af9a9a Add more logging and change release bump logic 2022-09-04 20:25:44 -07:00
TrainDoctor b35bd056d5 Work on bump logic 2022-09-04 20:11:06 -07:00
TrainDoctor d2da85460d Update CI to accommodate custom version bumps 2022-09-04 20:04:11 -07:00
AAGaming 843e03b42c fix(toaster): allow toasts to show ingame 2022-09-04 22:14:35 -04:00
TrainDoctor 5f469bfb16 Merge ci-for-auto-update to main 2022-09-04 18:18:51 -07:00
AAGaming acaf6c72e4 Update decky-frontend-lib and refactor patches 2022-09-04 13:33:42 -04:00
TrainDoctor eb439574be Addition of proper branch slection (#168)
* This is a bit better, but branch selection still isn't working -_-

* I'm the king of oversight

* Selecting different branch checks for updates

* Stable doesn't detect old versions, which indicates it doesn't work

* Start adding deckyState for plugin updating

* Few tweaks

* Disable nightly selection

* Update decky-frontend-lib and move useSetting set setting to async
2022-09-01 13:46:49 -07:00
TrainDoctor 16a6e9b6a9 Give SettingsManager a default directory 2022-08-31 12:20:31 -07:00
Philipp Richter 6f84cf94b5 Fix LimitOverrunError when a lot of data is passed to the backend (fixes #158) (#161)
* Fix LimitOverrunError when a lot of data is passed to the backend

* Raise socket's buffer limit to something more sensible
2022-08-31 10:46:40 -07:00
TrainDoctor 7c06db5ece Moved traceback to higher log level for cleaner logs 2022-08-29 15:18:18 -07:00
TrainDoctor aeb2decfc1 Fix plugin uninstalling 2022-08-29 14:41:10 -07:00
botato b7d7ca04e1 Refractor plugin backend (#111)
* refractor uninstall plugin backend

* refractor plugin installation method

* Change formatting in browser.py

* Manually format main.py

* Manually format utilities.py

* remove inconsistency

* remove unnecessary linebreaks

* lol what

* last minute pythoning

* Fix async missing

* lint

* more refractor

* await forgotten

* fix: menu not disappearing after first click

* lint

* bug: fix double click on uninstall

* depricate request installs

* basic patch notes viewer, lazy-load settings and store, build frontend as esmodule, add lazy-loaded react-markdown, backend changes to accomodate ESModule frontend

* refractor uninstall plugin backend

* Change formatting in browser.py

* Manually format main.py

* Manually format utilities.py

* remove unnecessary linebreaks

* lol what

* last minute pythoning

* Fix async missing

* rebase onto main

* fix error, fix React crash if patch notes are opened before remote version info is loaded

Co-authored-by: TrainDoctor <traindoctor@protonmail.com>
Co-authored-by: AAGaming <aa@mail.catvibers.me>
2022-08-26 21:01:23 -07:00
AAGaming d4d1c2bbab basic patch notes viewer, lazy-load settings and store, build frontend as esmodule, add lazy-loaded react-markdown, backend changes to accomodate ESModule frontend 2022-08-26 01:18:28 -04:00
TrainDoctor effc4ab0f5 Hide branch select until it's working 2022-08-24 21:18:38 -07:00
AAGaming 79db0c779d Settings API for loader, preview branch select 2022-08-24 23:51:20 -04:00
AAGaming fe2b6b0283 fix plugins menu scrolling, shut up typecript 2022-08-24 20:49:39 -04:00
Chris Simons b9a87cd785 set gid along with uid (#152) 2022-08-24 17:22:07 -07:00
AAGaming 98e9ce881f move DeckyUpdater to effect 2022-08-24 20:03:29 -04:00
TrainDoctor e49bdd9c05 Move conditional check to correct location 2022-08-24 16:56:20 -07:00
TrainDoctor d0fd2ac674 Move to using deckyState 2022-08-24 13:47:21 -07:00
TrainDoctor de1c89af21 Fix updater being corrupted by multiple attempts to download 2022-08-23 15:23:23 -07:00
AAGaming 8b3f569a09 Add plugin updater, notification badge, fixes 2022-08-21 16:41:25 -04:00
Collin Diekvoss 1930400032 Better wrapping of plugin tags (#150) 2022-08-20 21:40:57 -04:00
Sefa Eyeoglu 43dee863cd Add CEF Remote Debugging toggle (#129)
* feat: add CEF Remote Debugging toggle

* feat: disable remote debugger on startup

* refactor: stop debugger instead of disable

* feat: add option to allow remote debugging by default

Co-authored-by: TrainDoctor <traindoctor@protonmail.com>
2022-08-18 14:50:59 -07:00
TrainDoctor 55a7682663 fix ButtonItem shim applying when not needed 2022-08-17 17:32:50 -07:00
TrainDoctor d05e8d36b4 Update to latest decky-frontend-lib 2022-08-17 17:28:30 -07:00
AAGaming 0018b8e957 bump lib and add temporary shims for webpack v5 2022-08-17 20:03:45 -04:00
TrainDoctor 59038f65ac Fix log spam from injection related errors 2022-08-17 12:57:58 -07:00
AAGaming 5960c11d60 add class names to PluginCard for theming 2022-08-17 15:27:22 -04:00
Sefa Eyeoglu 8d065eab1f Add Plugin Reload Button to Settings (#128)
* feat: add reload button to plugin list

Signed-off-by: Sefa Eyeoglu <contact@scrumplex.net>

* refactor: move plugin actions into context menu

Signed-off-by: Sefa Eyeoglu <contact@scrumplex.net>
2022-08-16 16:51:39 -07:00
AAGaming 3b1b6d28d6 add some classes for nicer scrolling, update lib 2022-08-15 13:22:38 -04:00
AAGaming 0a735886c9 fix toasts breaking sometimes 2022-08-14 21:59:55 -04:00
AAGaming c9430f5be4 less stupid method 2022-08-14 13:17:39 -04:00
AAGaming a4e2237fc0 fix loader not re-injecting on restart 2022-08-14 12:51:07 -04:00
AAGaming 85d0398e62 shut typescript up 2022-08-14 00:02:01 -04:00
AAGaming 30a538e85e FINALLY fix the multiple injections bug 2022-08-13 23:58:57 -04:00
AAGaming 84a19203c5 fix injecting twice 2022-08-13 11:57:52 -04:00
AAGaming 99cda2907d fix TS errors 2022-08-12 21:02:11 -04:00
AAGaming a38582d158 Fix toaster deinit error 2022-08-12 16:49:28 -04:00
TrainDoctor 9556994e14 fix empty settings and store screens after reboot 2022-08-12 11:45:29 -07:00
OMGDuke dee2cfa47b remove console.log that was causing lots of log spam (#138) 2022-08-12 09:54:57 -04:00
TrainDoctor 463403be23 Update build.yml 2022-08-11 20:37:46 -07:00
TrainDoctor b68eaca55d Updater should now find all version tags 2022-08-11 20:12:17 -07:00
AAGaming 114c54c9b0 Fix route unpatching 2022-08-11 20:34:55 -04:00
TrainDoctor 47e0661773 Add releases back to the mix 2022-08-11 17:10:37 -07:00
TrainDoctor 6c48dfe7f6 Actually send the proper variable out 2022-08-11 16:54:13 -07:00
TrainDoctor ed0ae7c9e2 Removed un-needed trimming 2022-08-11 16:48:50 -07:00
TrainDoctor ea265ae6df Corrected dummy tag, added echoing 2022-08-11 16:18:21 -07:00
TrainDoctor 860caf440b Add semver tool, temporarily disable triggered pre-releases 2022-08-11 16:10:00 -07:00
TrainDoctor 64040879f5 Update to latest version of decky-frontend-lib 2022-08-10 15:48:48 -07:00
AAGaming e92073162a oops: remove test log 2022-08-10 16:34:53 -04:00
AAGaming 67426af3ef Add api for showing toast notifications 2022-08-09 21:52:03 -04:00
Sefa Eyeoglu 0dbdb4a143 fix: don't pass unzip job to event loop (#136)
For some reason this broke installation of plugins when another specific
plugin was present (vibrantDeck)
2022-08-09 12:06:33 -07:00
TrainDoctor c9e9c45b37 Standardize logging in browser.py 2022-08-08 13:06:04 -07:00
TrainDoctor 6bc8a4fb1d Add missing import 2022-08-08 12:38:35 -07:00
Derek J. Clark 20094c5f75 Use Environment Variables (#123)
Uses environment variables instead of hard coding the "deck" user/group.
This adds support for systems other than the steam deck that are using the DeckUI.

* Use Environment Variables

* Use method to get USER from a systemd root process

* Fix imports. Add get_user and get_user_group methods in helpers.py. Removed duplicated code

* Add separate setters/getters for user vars. Ensure sleep prevents race condition of user setter in while loop
2022-08-08 11:32:14 -07:00
AAGaming 198591dbd7 whoops don't need it here 2022-08-05 21:18:19 -04:00
AAGaming f21d34506d Implement CSRF protection 2022-08-05 21:16:29 -04:00
AAGaming ab6ec98160 API for patching existing routes, lower power use 2022-08-02 18:54:55 -04:00
Beebles f1e809781a forgot one update (#125) 2022-07-30 19:42:16 -07:00
Beebles 789058b72f Readme.md references incorrect github repo (#124) 2022-07-30 18:40:44 -07:00
TrainDoctor 4a68b1430d Update README.md 2022-07-28 13:50:03 -07:00
TrainDoctor 66c4a7e16e Update README.md 2022-07-25 17:29:56 -07:00
TrainDoctor b929b2dddf Update README.md 2022-07-25 17:08:10 -07:00
TrainDoctor fb0b703438 Fix unintended question mark in "Installing" modal 2022-07-25 16:07:16 -07:00
AAGaming afb2c7c0ed Better install process UX, fix reinstalling 2022-07-25 17:13:50 -04:00
AAGaming 52dded85ed quick fix for routes refreshing constantly 2022-07-24 11:51:42 -04:00
AAGaming 2004bdebbf fix calibration menu in controller settings 2022-07-24 11:37:38 -04:00
AAGaming c9bf8d357e use fstring 2022-07-21 22:03:11 -04:00
AAGaming 09eee761a5 change log to debug 2022-07-21 22:02:47 -04:00
AAGaming 20f43b2fd4 fix plugin uninstalling 2022-07-21 22:02:13 -04:00
AAGaming e6dd1c29d8 remove modal box shadow 2022-07-17 16:42:24 -04:00
AAGaming 6e88c7c9ac Show warning when installing legacy plugins 2022-07-17 16:09:42 -04:00
AAGaming f015e00561 more updater fixes 2022-07-15 12:57:51 -04:00
AAGaming e07827cdb5 catch rm errors 2022-07-15 12:36:16 -04:00
AAGaming 103d43e7c9 fix updater 2022-07-15 12:31:30 -04:00
AAGaming 23b7df0ce2 wait 30s before first update check 2022-07-15 12:25:27 -04:00
AAGaming a5671e19ce fix ci AGAIN 2022-07-15 12:20:05 -04:00
AAGaming f2fbd399fe allow users to manually check for updates 2022-07-15 12:16:57 -04:00
AAGaming 28b91963a9 fix ci part 4
fix ci part 5

fix ci part 6

fix ci part 7

fix ci part 8

fix ci part 9

fix ci part 10

fix ci part 11
2022-07-15 11:55:39 -04:00
AAGaming ce2268370f this slipped through the linter 2022-07-15 10:53:41 -04:00
AAGaming 59462041b1 fix ci part 3: shopt edition 2022-07-15 10:52:08 -04:00
AAGaming d4d32c8d55 fix ci part 2 2022-07-15 10:45:23 -04:00
AAGaming e600aeccc7 fix ci startup failure 2022-07-15 10:38:03 -04:00
AAGaming 162d1b561b fix lockup in _open_socket_if_not_exists, probably fix ci prereleases 2022-07-15 10:34:47 -04:00
Brian Choy ba824fc921 Fix jq errors in prerelease script (#118)
* Fix jq errors in prerelease script

* Use multivariable output, add back RELEASE var
2022-07-15 09:12:07 -04:00
AAGaming 8c8cf180fa Updater for decky-loader (#117)
* Add an updater in settings for decky-loader

* add chmod

* remove junk comments
2022-07-14 22:51:55 -04:00
AAGaming 05d11cfff0 fix get_tabs oopsie 2022-07-13 23:24:29 -04:00
botato 3c24b37247 change ci again (#116) 2022-07-13 21:19:19 -04:00
AAGaming dbb4bc5ab4 another CI fix from botato 2022-07-12 12:11:42 -04:00
botato b00b04ceeb Fix action not detecting prerelease 2022-07-11 17:41:11 -04:00
botato 470f16adda CI revamp (#110)
* ci: automatically make releases, ...

- option to run manually (for full-fledged releases)
- cron schedule for pre-releases (every day at 1 pm UTC)
- semantic versioning
- Automatically generated release description

* formatting

* more formatting .-.

* Tweak according to latest release
2022-07-11 09:13:56 +02:00
botato 76424174ed Use call instead of Popen (#113) 2022-07-11 08:56:36 +02:00
AAGaming b618fe1e97 bump lib 2022-07-07 00:03:56 -04:00
AAGaming 45949e8456 support non-ui plugins 2022-07-07 00:03:20 -04:00
TrainDoctor e3a965329d Update install_prerelease.sh 2022-07-04 08:27:44 -07:00
TrainDoctor 6ee41578ea Update plugin-loader.tsx 2022-07-03 16:56:35 -07:00
TrainDoctor 9404215399 Make legacy tag text readable 2022-07-03 16:18:07 -07:00
AAGaming b8bf150a74 fix legacy coloring 2022-07-03 19:12:10 -04:00
AAGaming add3f77c1a colorize legacy tag 2022-07-03 18:48:58 -04:00
AAGaming 6c42661f86 hack: temp hide example plugin 2022-07-03 17:37:39 -04:00
TrainDoctor 2b3c219e38 * Async onOK
* await confirm_plugin_install

* wait until we've exited store to re-open QAM
2022-07-03 14:28:48 -07:00
TrainDoctor 8eb89da373 Update README.md 2022-07-03 13:30:58 -07:00
TrainDoctor ace9f61e50 Redirect to QAM after installing a plugin, QOL. 2022-07-03 12:52:22 -07:00
WerWolv baa02c129f Fixed plugin installation ssl verification issue (#101)
* Added cert location debugging

* Install certifi

* Try adding manual cacert in install request

* Properly use ssl

* More efficiently load ssl certificate
2022-07-03 08:29:46 +02:00
TrainDoctor 1e6b3edbf2 Merge remote-tracking branch 'origin/main' 2022-07-02 23:14:51 -07:00
botato 085aacea06 Use deckyState in uninstall menu (fixes #98) (#100) 2022-07-02 22:14:43 -04:00
TrainDoctor 675e667a9e Catch uninstall plugin 2022-07-02 17:09:21 -07:00
TrainDoctor 58b2c4208d Remove bugged rename invocation 2022-07-02 16:37:23 -07:00
TrainDoctor c2693869a7 Fix debug logging 2022-07-02 16:04:09 -07:00
TrainDoctor 683c51ceac Properly await uninstall 2022-07-02 15:59:15 -07:00
TrainDoctor 630e8b7213 Update prerelease script 2022-07-02 15:37:20 -07:00
TrainDoctor 246b31794a Update workflow 2022-07-02 14:55:27 -07:00
TrainDoctor b7d57de378 Add pre-release install script 2022-07-02 14:42:41 -07:00
TrainDoctor ee8aa98446 Update README.md, password is needed (#70)
(cherry picked from commit 1199c080bc)
Added some context and changed wording on uninstall.
2022-07-02 12:41:25 -07:00
TrainDoctor 557a00aed7 Update README.md 2022-07-01 17:15:32 -07:00
botato 4daf028e7a Uninstall functionality (#97)
* feat: POC uninstallation feature

* Fixes, placeholder

* bugfix: wrong function call

* add oncancel and change function called

* clean up plugin uninstall code

* bugfix, uninstall in store

* Limit scope of feature branch

* feat: PluginLoader.unloadPlugin

* problematic logs
2022-07-01 16:43:17 -07:00
AAGaming 934a50f683 fix legacy plugin duplication 2022-07-01 11:50:08 -04:00
TrainDoctor aa4f1b1e87 pnpm update 2022-06-30 15:15:15 -07:00
AAGaming 67495d30d6 fix packager 2022-06-30 16:48:49 -04:00
AAGaming d72f364a8d backwards-compatible plugin store, legacy plugin library 2022-06-30 16:04:29 -04:00
TrainDoctor da0f7dd337 Tone down hash missing warning. 2022-06-29 12:23:11 -07:00
TrainDoctor 518b01f571 Installing from plugin store now works as intended 2022-06-29 11:46:06 -07:00
AAGaming 3f2a2bbc04 fix installing plugins 2022-06-29 12:25:50 -04:00
AAGaming 79e8af8be6 update store for new format, awaiting cors to test 2022-06-29 12:17:25 -04:00
AAGaming 18d444e8fc fix tab type, bump lib for tree shaking 2022-06-29 11:57:59 -04:00
hulkrelax abc5ce5382 remove body property in args (#91) 2022-06-28 21:12:55 -04:00
AAGaming 9619c52720 add settings page with install from URL option 2022-06-22 23:22:27 -04:00
TrainDoctor 80b223180e Remove old info and redirect to wiki for in-development info 2022-06-21 14:32:02 -07:00
TrainDoctor 1d5d14b492 Added remote launch option 2022-06-21 13:49:12 -07:00
TrainDoctor ce23534ccc Remove argument parity between scripts, not sustainable solution 2022-06-21 12:36:43 -07:00
AAGaming e6e74d8e9d Don't allow overriding name 2022-06-21 09:52:54 -04:00
TrainDoctor 6289578f68 Update pnpm-lock.yaml 2022-06-20 20:40:50 -07:00
AAGaming e7c44ee202 Replace tabs hook, fix panels, bump lib 2022-06-20 23:37:52 -04:00
TrainDoctor 39f6a7688d Converted install script to pnpm 2022-06-20 20:24:44 -07:00
TrainDoctor 47ca3ece4a Added python depdency install, fixed use-case phrasing 2022-06-20 18:56:22 -07:00
Jonas Dellinger 3e250dd180 Fix importPlugin queue 2022-06-20 15:54:31 +02:00
Jonas Dellinger 711af3bca3 Fix onDismount 2022-06-20 15:34:08 +02:00
Jonas Dellinger 9a6930571c Fix onDismount 2022-06-20 15:29:40 +02:00
Jonas Dellinger d9dd09c69b Revert "fix onDismount"
This reverts commit daca482ed8.
2022-06-20 15:28:30 +02:00
AAGaming daca482ed8 fix onDismount 2022-06-19 18:56:02 -04:00
AAGaming 99b4b939bd Implement React-based plugin store (#81)
Co-authored-by: TrainDoctor <11465594+TrainDoctor@users.noreply.github.com>
Co-authored-by: WerWolv <werwolv98@gmail.com>
2022-06-17 18:43:53 -04:00
Jonas Dellinger a95bf94d87 fix(loader): multiprocessing.set_start_method once, queue for plugin import 2022-06-13 10:57:16 +02:00
Jonas Dellinger 12f4c7faff fix(loader): eplixcitly set process start method and import fsevents on mac 2022-06-13 10:34:46 +02:00
TrainDoctor bbf49470fc Update nodeck.sh 2022-06-06 13:58:02 -07:00
TrainDoctor a1a4d5902b Update deck.sh 2022-06-06 13:57:52 -07:00
TrainDoctor 90a65dbace Removed a line that would exclude passwords with non-alnum characters. 2022-06-06 13:34:58 -07:00
TrainDoctor f828480715 Clarified password is for deck user 2022-06-06 13:16:44 -07:00
TrainDoctor ed1a9222b4 Rename pc.sh to nodeck.sh to represent intent 2022-06-06 13:03:39 -07:00
TrainDoctor 73b36b776a Actually preserve enviorment variables properly 2022-06-06 12:58:37 -07:00
TrainDoctor 4a2299f3ff Update README.md 2022-06-06 12:54:59 -07:00
TrainDoctor 6128cbec6b Typo... 2022-06-06 12:44:54 -07:00
TrainDoctor c93af19ffa Typo... 2022-06-06 12:37:59 -07:00
TrainDoctor cadb687cd7 Add contributor install script (#69)
* Add contributor install script

* Switched to non-tmp directory

* Fixed potential issue with passwords being not being parsed properly

* Replace up hardcoded ports and silence npm

* Removed legacy support version, changed to https git clones

* Add non-deck compatible version of script

* Switch to arguments parsed while running script for contrib pc

* Now compatible with curl install from terminal, but it's a bit fragile

* Incorrect install directory for plugintemplate

* Functionalized a ton of stuff

* Changed in anticipation of merge to react-frontend-plugins branch

* Added guide to enable Steam Deck UI and clarification about Windows

* Moved contribution scripts to contrib and provided "how to run"

* Reordered README for clarity and better placement for contribution sect.

* Looks better

* Removed un-needed file-transfers and added better checks and run info

* Improved how to run given at end of script.

* Improved warning, improved ssh invocation globally and how-to-use/run

* Link to new plugin template and added link to the wiki in readme

* testing for remote invocation

* Fixed bug with invocation via curl

* Just in case...
2022-06-02 18:24:24 -04:00
Jonas Dellinger 1114d55931 Bump components library 2022-06-02 17:59:18 +02:00
AAGaming 0f20fe691f fix oops 2022-06-01 17:55:49 -04:00
AAGaming 86e23686aa React Plugin install dialog (closes #75) 2022-06-01 17:50:10 -04:00
Jonas Dellinger bd1b2e82fd Move store opening to frontend only 2022-05-31 18:05:26 +02:00
Jonas Dellinger 660e34664e Explicit import type 2022-05-30 20:57:22 +02:00
Jonas Dellinger 8fcaadd8f3 All props of route, expose routerHook 2022-05-30 20:55:51 +02:00
AAGaming 007860f8f7 react: Add Router hook & fix typescript issues (#68)
* add rollup watch command, add pnpm lockfile

* wait for react

* add WIP patcher, window hook, and webpack

* fix typescript, fix React, lint, add pnpm to gitignore

* actually fix react

* show frontend JS errors in console

* cleanup

* Add Router hook

* Remove console.log

* Expose routerHook in createPluginAPI

Co-authored-by: Jonas Dellinger <jonas@dellinger.dev>
2022-05-30 20:26:54 +02:00
marios 44776b393e added open store button 2022-05-26 21:14:32 +03:00
Jonas Dellinger ad1f57795e Fix LegacyPlugin 2022-05-26 13:31:18 +02:00
Jonas Dellinger 71dd0ea449 Cleanup after merge 2022-05-26 13:30:14 +02:00
Jonas Dellinger a06efc08bc Run build on all branches 2022-05-26 09:33:55 +02:00
Jonas Dellinger 39e56fed3d Switch to inotify, RegexMatchingEventHandler and use set for reloading plugins 2022-05-26 09:29:49 +02:00
marios 4b923c1dc7 display overhaul, compatibility with legacy plugins, fixes 2022-05-26 04:00:18 +03:00
Jonas Dellinger d23f1ac56c Added support for static assets, remove frontend_bundle field 2022-05-25 21:35:03 +02:00
Jonas Dellinger 74438a3145 Work on react frontend loader 2022-05-13 19:14:47 +02:00
135 changed files with 14442 additions and 1055 deletions
+74
View File
@@ -0,0 +1,74 @@
name: Bug report
description: File a bug/issue
title: "[BUG] <title>"
labels: [bug]
body:
- type: checkboxes
id: low-effort-checks
attributes:
label: Please confirm
description: Issues without all checks may be ignored/closed.
options:
- 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).
- type: textarea
attributes:
label: Bug Report Description
description: A clear and concise description of what the bug is and if possible, the steps you used to get to the bug. If appropriate, include screenshots or videos.
placeholder: |
When I try to use ...
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
attributes:
label: Expected Behaviour
description: A brief description of the expected behavior.
placeholder: It should be ...
validations:
required: true
- type: input
attributes:
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"
validations:
required: true
- type: dropdown
attributes:
label: Selected Update Channel
description: Which branch of Decky are you on?
multiple: false
options:
- Stable
- Prerelease
validations:
required: true
- type: input
attributes:
label: Have you modified the read-only filesystem at any point?
description: Describe how here, if you haven't done anything you can leave this blank
placeholder: Yes, I've installed neofetch via pacman.
validations:
required: false
- type: textarea
attributes:
label: 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
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Steam Deck Homebrew Discord Server
url: https://discord.gg/ZU74G2NJzk
about: Please ask and answer questions here.
@@ -0,0 +1,35 @@
name: Feature request
description: Request a new feature (NOT A PLUGIN)
title: "[Request] <title>"
labels: [feature request]
body:
- type: checkboxes
id: low-effort-checks
attributes:
label: Please confirm
description: Issues without all checks may be ignored/closed.
options:
- label: I have searched existing issues
- label: This issue is not a duplicate of an existing one
- label: This is not a request for a plugin
- type: textarea
attributes:
label: Feature Request Description
description: A clear and concise description of what the new feature.
placeholder: |
Decky plugins should be sortable in the quick access menu
validations:
required: true
- type: textarea
attributes:
label: Further Description
description: A further explanation of the feature. If appropriate, include screenshots or videos.
placeholder: |
This would help make the UI clearer and easier to use as there is less clutter in the QAM.
It would also make it faster to access plugins that are used more.
This could be implemented by adding ...
validations:
required: false
+13
View File
@@ -0,0 +1,13 @@
Please tick as appropriate:
- [ ] I have tested this code on a steam deck or on a PC
- [ ] My changes generate no new errors/warnings
- [ ] This is a bugfix/hotfix
- [ ] This is a new feature
If you're wanting to update a translation or add a new one, please use the weblate page: https://weblate.werwolv.net/projects/decky/
# Description
This fixes issue: #
Please provide a clear and concise description of what the new feature is. If appropriate, include screenshots or videos.
+277 -25
View File
@@ -2,41 +2,293 @@ name: Builder
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
# 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: read
contents: write
jobs:
build:
name: Build PluginLoader
runs-on: ubuntu-20.04
name: Packager
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
- 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
[ -f requirements.txt ] && pip install -r requirements.txt
- name: Install JS dependencies ⬇️
working-directory: ./frontend
run: |
npm i -g pnpm
pnpm i --frozen-lockfile
- name: Build JS Frontend 🛠️
working-directory: ./frontend
run: pnpm run build
- name: Build Python Backend 🛠️
run: pyinstaller --noconfirm --onefile --name "PluginLoader" --add-data ./backend/static:/static --add-data ./backend/locales:/locales --add-data ./backend/legacy:/legacy --add-data ./plugin:/plugin ./backend/*.py
- name: Upload package artifact ⬆️
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v3
with:
name: PluginLoader
path: ./dist/PluginLoader
- name: Download package artifact locally
if: ${{ env.ACT }}
uses: actions/upload-artifact@v3
with:
path: ./dist/PluginLoader
build-win:
name: Build PluginLoader for Win
runs-on: windows-2022
steps:
- name: Checkout 🧰
uses: actions/checkout@v3
- name: Set up NodeJS 18 💎
uses: actions/setup-node@v3
with:
node-version: 18
- name: Set up Python 3.10.2 🐍
uses: actions/setup-python@v4
with:
python-version: "3.10.2"
- name: Install Python dependencies ⬇️
run: |
python -m pip install --upgrade pip
pip install pyinstaller==5.5
pip install -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
- name: Checkout 🧰
uses: actions/checkout@v3
- name: 🐍 Set up Python 3.10
uses: actions/setup-python@v3
- name: Install semver-tool asdf
uses: asdf-vm/actions/install@v1
with:
python-version: "3.10"
- name: ⬇️ Install dependencies
run: |
python -m pip install --upgrade pip
pip install pyinstaller
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: 🛠️ Build
run: |
pyinstaller --noconfirm --onefile --name "PluginLoader" --add-data ./plugin_loader/static:/static --add-data ./plugin_loader/templates:/templates ./plugin_loader/*.py
- name: ⬆️ Upload package
uses: actions/upload-artifact@v2
tool_versions: |
semver 3.3.0
- name: Fetch package artifact ⬇️
uses: actions/download-artifact@v3
if: ${{ !env.ACT }}
with:
name: Plugin Loader
path: |
./dist/*
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
+55
View File
@@ -0,0 +1,55 @@
name: Push Updated Plugin Stub to Template
on: push
jobs:
copy-stub:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@8230315d06ad95c617244d2f265d237a1682d445
with:
ref: ${{ github.sha }}
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v35.6.3
with:
separator: ","
files: |
plugin/*
- name: Is stub changed
id: changed-stub
run: |
STUB_CHANGED="false"
PATHS=(plugin plugin/decky_plugin.pyi)
SHA=${{ github.sha }}
SHA_PREV=HEAD^
FILES=$(git diff $SHA_PREV..$SHA --name-only -- ${PATHS[@]} | jq -Rsc 'split("\n")[:-1] | join (",")')
if [[ "$FILES" == *"plugin/decky_plugin.pyi"* ]]; then
$STUB_CHANGED="true"
echo "Stub has changed, pushing updated stub"
else
echo "Stub has not changed, exiting."
echo "has_changed=$STUB_CHANGED" >> $GITHUB_OUTPUT
exit 0
fi
echo "has_changed=$STUB_CHANGED" >> $GITHUB_OUTPUT
- name: Push updated stub
if: steps.changed-stub.outputs.has_changed == true
uses: dmnemec/copy_file_to_another_repo_action@bbebd3da22e4a37d04dca5f782edd5201cb97083
env:
API_TOKEN_GITHUB: ${{ secrets.GITHUB_TOKEN }}
with:
source_file: 'plugin/decky_plugin.pyi'
destination_repo: 'SteamDeckHomebrew/decky-plugin-template'
user_email: '11465594+TrainDoctor@users.noreply.github.com'
user_name: 'TrainDoctor'
commit_message: 'Updated template with latest plugin stub changes'
+17
View File
@@ -0,0 +1,17 @@
name: Lint
on:
push:
jobs:
lint:
name: Run linters
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2 # Check out the repository first.
- name: Run prettier (JavaScript & TypeScript)
run: |
pushd frontend
npm install
npm run lint
+13 -1
View File
@@ -149,4 +149,16 @@ dmypy.json
.pytype/
# Cython debug symbols
cython_debug/
cython_debug/
# static files are built
backend/static
# ignore settings.json
# prevents leaking login details
.vscode/settings.json
# plugins folder for local launches
plugins/*
act/.directory
act/artifacts/*
Vendored Executable
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]:-$0}"; )" &> /dev/null && pwd 2> /dev/null; )";
# printf "${SCRIPT_DIR}\n"
# printf "$(dirname $0)\n"
if ! [[ -e "${SCRIPT_DIR}/settings.json" ]]; then
printf '.vscode/settings.json does not exist. Creating it with default settings. Exiting afterwards. Run your task again.\n\n'
cp "${SCRIPT_DIR}/defsettings.json" "${SCRIPT_DIR}/settings.json"
exit 1
else
printf '.vscode/settings.json does exist. Congrats.\n'
printf 'Make sure to change settings.json to match your deck.\n'
fi
+7
View File
@@ -0,0 +1,7 @@
{
"deckip" : "0.0.0.0",
"deckport" : "22",
"deckpass" : "ssap",
"deckkey" : "-i ${env:HOME}/.ssh/id_rsa",
"deckdir" : "/home/deck"
}
+16 -4
View File
@@ -2,13 +2,25 @@
"version": "0.2.0",
"configurations": [
{
"name": "Debug",
"name": "Run (Remote)",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/plugin_loader/main.py",
"preLaunchTask": "Stop Service",
"console": "integratedTerminal",
"justMyCode": true
"preLaunchTask": "remoterun",
"cwd": "",
"program": "",
},
{
"name": "Run (Local)",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/backend/main.py",
"cwd": "${workspaceFolder}/backend",
"console": "integratedTerminal",
"env": {
"PLUGIN_PATH": "${workspaceFolder}/plugins"
},
"preLaunchTask": "localrun"
}
]
}
+183 -3
View File
@@ -1,10 +1,190 @@
{
"version": "2.0.0",
"tasks": [
// OTHER
{
"label": "Stop Service",
"label": "checkforsettings",
"type": "shell",
"command":"systemctl --user stop plugin_loader",
"group": "none",
"detail": "Check that settings.json has been created",
"command": "bash -c ${workspaceFolder}/.vscode/config.sh",
"problemMatcher": []
},
{
"label": "localrun",
"type": "shell",
"group": "none",
"dependsOn": [
"buildall"
],
"detail": "Check for local runs, create a plugins folder",
"command": "mkdir -p plugins",
"problemMatcher": []
},
{
"label": "remoterun",
"type": "shell",
"group": "none",
"dependsOn": [
"updateremote",
"runpydeck"
],
"detail": "Task for remote run launches",
"command": "exit 0",
"problemMatcher": []
},
{
"label": "dependencies",
"type": "shell",
"group": "none",
"detail": "Check for local runs, create a plugins folder",
"command": "rsync -azp --rsh='ssh -p ${config:deckport} ${config:deckkey}' requirements.txt deck@${config:deckip}:${config:deckdir}/homebrew/dev/pluginloader/requirements.txt && ssh deck@${config:deckip} -p ${config:deckport} ${config:deckkey} 'python -m ensurepip && python -m pip install --upgrade pip && python -m pip install --upgrade setuptools && python -m pip install -r ${config:deckdir}/homebrew/dev/pluginloader/requirements.txt'",
"problemMatcher": []
},
{
"label": "extracttext",
"type": "shell",
"group": "none",
"detail": "Check for new strings in the frontend source code and extract it into the corresponding json language files",
"command": "cd frontend && ./node_modules/.bin/i18next --config ./i18next-parser.config.mjs",
"problemMatcher": []
},
// BUILD
{
"label": "pnpmsetup",
"type": "shell",
"group": "build",
"detail": "Setup pnpm",
"command": "cd frontend && pnpm i",
"problemMatcher": []
},
{
"script": "watch",
"type": "npm",
"path": "frontend",
"group": "build",
"problemMatcher": [],
"label": "watchfrontend",
"detail": "rollup -c -w",
"isBackground": true
},
{
"label": "buildfrontend",
"type": "npm",
"group": "build",
"detail": "rollup -c",
"script": "build",
"path": "frontend",
"problemMatcher": []
},
{
"label": "buildall",
"group": "build",
"detail": "Deploy pluginloader to deck",
"dependsOrder": "sequence",
"dependsOn": [
"pnpmsetup",
"buildfrontend"
],
"problemMatcher": []
},
// DEPLOY
{
"label": "createfolders",
"detail": "Create plugins folder in expected directory",
"type": "shell",
"group": "none",
"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'",
"problemMatcher": []
},
{
"label": "deploy",
"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",
"problemMatcher": []
},
// RUN
{
"label": "runpydeck",
"detail": "Run indev PluginLoader on Deck",
"type": "shell",
"group": "none",
"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'",
"problemMatcher": []
},
{
"label": "runpylocal",
"detail": "Run PluginLoader from python locally",
"type": "shell",
"group": "none",
"command": "export PLUGIN_PATH=${workspaceFolder}/plugins; export CHOWN_PLUGIN_PATH=0; sudo -E python3 ${workspaceFolder}/backend/main.py",
"problemMatcher": []
},
// ALL-IN-ONES
{
"label": "deployandrun",
"detail": "Deploy and run, skipping JS build. Useful when combined with npm:watch",
"dependsOrder": "sequence",
"group": {
"kind": "build",
"isDefault": true
},
"dependsOn": [
"deploy",
"runpydeck"
],
"problemMatcher": []
},
{
"label": "updateremote",
"detail": "Build and deploy",
"dependsOrder": "sequence",
"group": "none",
"dependsOn": [
"buildall",
"deploy"
],
"problemMatcher": []
},
{
"label": "updateandrun",
"detail": "Build, deploy and run",
"dependsOrder": "sequence",
"group": {
"kind": "build",
"isDefault": true
},
"dependsOn": [
"buildfrontend",
"deploy",
"runpydeck"
],
"problemMatcher": []
},
{
"label": "allinone",
"detail": "Build, install dependencies, deploy and run",
"dependsOrder": "sequence",
"group": {
"kind": "build",
"isDefault": false
},
"dependsOn": [
"buildall",
"createfolders",
"dependencies",
"deploy",
"runpydeck"
],
"problemMatcher": []
}
]
}
}
+104 -33
View File
@@ -1,43 +1,114 @@
# Plugin Loader [![Chat](https://img.shields.io/badge/chat-on%20discord-7289da.svg)](https://discord.gg/ZU74G2NJzk)
<h1 align="center">
<a name="logo" href="https://deckbrew.xyz/"><img src="https://deckbrew.xyz/static/icon-45ca1f5aea376a9ad37e92db906f283e.png" alt="Deckbrew logo" width="200"></a>
<br>
Decky Loader
<br>
<a name="download button" href="https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/decky_installer.desktop"><img src="./docs/images/download_button.svg" alt="Download decky" width="350px" style="padding-top: 15px;"></a>
</h1>
![steamuserimages-a akamaihd](https://user-images.githubusercontent.com/10835354/161068262-ca723dc5-6795-417a-80f6-d8c1f9d03e93.jpg)
<p align="center">
<a href="https://github.com/SteamDeckHomebrew/decky-loader/releases"><img src="https://img.shields.io/github/downloads/SteamDeckHomebrew/decky-loader/total" /></a>
<a href="https://github.com/SteamDeckHomebrew/decky-loader/stargazers"><img src="https://img.shields.io/github/stars/SteamDeckHomebrew/decky-loader" /></a>
<a href="https://github.com/SteamDeckHomebrew/decky-loader/commits/main"><img src="https://img.shields.io/github/last-commit/SteamDeckHomebrew/decky-loader.svg" /></a>
<a href="https://weblate.werwolv.net/engage/decky/"><img src="https://weblate.werwolv.net/widgets/decky/-/decky/svg-badge.svg" alt="Translation status" /></a>
<a href="https://github.com/SteamDeckHomebrew/decky-loader/blob/main/LICENSE"><img src="https://img.shields.io/github/license/SteamDeckHomebrew/decky-loader" /></a>
<a href="https://deckbrew.xyz/discord"><img src="https://img.shields.io/discord/960281551428522045?color=%235865F2&label=discord" /></a>
<br>
<br>
<img src="https://media.discordapp.net/attachments/966017112244125756/1012466063893610506/main.jpg" alt="Decky screenshot" width="80%">
</p>
## Installation
1. Go into the Steam Deck Settings
2. Under System -> System Settings toggle `Enable Developer Mode`
3. Scroll the sidebar all the way down and click on `Developer`
4. Under Miscellaneous, enable `CEF Remote Debugging`
5. Click on the `STEAM` button and select `Power` -> `Switch to Desktop`
6. Open a terminal and paste the following command into it:
- For users:
- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/main/dist/install_release.sh | sh`
- For developers:
~~- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/main/dist/install_nightly.sh | sh`~~
Nightly releases are currently broken.
8. Done! Reboot back into Gaming mode and enjoy your plugins!
## 📖 About
### Install Plugins
- Simply copy the plugin's folder into `~/homebrew/plugins`
Decky Loader is a homebrew plugin launcher for the Steam Deck. It can be used to [stylize your menus](https://github.com/suchmememanyskill/SDH-CssLoader), [change system sounds](https://github.com/EMERALD0874/SDH-AudioLoader), [adjust your screen saturation](https://github.com/libvibrant/vibrantDeck), [change additional system settings](https://github.com/NGnius/PowerTools), and [more](https://plugins.deckbrew.xyz/).
### Uninstall
- Open a terminal and paste the following command into it:
- For both users and developers:
- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/main/dist/uninstall.sh | sh`
For more information about Decky Loader as well as documentation and development tools, please visit [our wiki](https://deckbrew.xyz).
### Developing plugins
- There is no complete plugin development documentation yet. However a good starting point is the [Plugin Template](https://github.com/SteamDeckHomebrew/Plugin-Template) repository
### 🎨 Features
## Features
- Clean injecting and loading of one or more plugins
- Persistent. It doesn't need to be reinstalled after every system update
- Allows 2-way communication between the plugins and the loader.
- Allows plugins to define python functions and run them from javascript.
- Allows plugins to make fetch calls, bypassing cors completely.
🧹 Clean injecting and loading of multiple plugins.
🔒 Stays installed between system updates and reboots.
🔗 Allows two-way communication between plugins and the loader.
🐍 Supports Python functions run from TypeScript React.
🌐 Allows plugins to make fetch calls that bypass CORS completely.
## Caveats
### 🤔 Common Issues
- You can only interact with the Plugin Menu via touchscreen.
- 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.
- 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).
## Credit
## 💾 Installation
- This installation can be done without an admin/sudo password set.
1. Prepare a mouse and keyboard if possible.
- Keyboards and mice can be connected to the Steam Deck via USB-C or Bluetooth.
- Many Bluetooth keyboard and mouse apps are available for iOS and Android. (KDE connect is preinstalled on the steam deck)
- The Steam Link app is available on [Windows](https://media.steampowered.com/steamlink/windows/latest/SteamLink.zip), [macOS](https://apps.apple.com/us/app/steam-link/id1246969117), and [Linux](https://flathub.org/apps/details/com.valvesoftware.SteamLink). It works well as a remote desktop substitute.
- If you have no other options, use the right trackpad as a mouse and press <img src="./docs/images/light/steam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/steam.svg#gh-light-mode-only" height=16>+<img src="./docs/images/light/x.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/x.svg#gh-light-mode-only" height=16> to open the on-screen keyboard as needed.
1. Press the <img src="./docs/images/light/steam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/steam.svg#gh-light-mode-only" height=16> button and open the Power menu.
1. Select "Switch to Desktop".
1. Navigate to this Github page on a browser of your choice.
1. Download the [installer file](https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/decky_installer.desktop).
1. Drag the file onto your desktop and double click it to run it.
1. Either type your admin password or allow Decky to temporarily set your admin password to `Decky!` (this password will be removed after the installer finishes)
1. Choose the version of Decky Loader you want to install.
- **Latest Release**
Intended for most users. This is the latest stable version of Decky Loader.
- **Latest Pre-Release**
Intended for plugin developers. Pre-releases are unlikely to be fully stable but contain the latest changes. For more information on plugin development, please consult [the wiki page](https://wiki.deckbrew.xyz/en/loader-dev/development).
1. Open the Return to Gaming Mode shortcut on your desktop.
The original idea for the concept is based on the work of [marios8543's steamdeck-ui-inject](https://github.com/marios8543/steamdeck-ui-inject) project.
- There is also a fast install for those who can use Konsole. Run `curl -L https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/install_release.sh | sh` and type your password when prompted.
### 👋 Uninstallation
We are sorry to see you go! If you are considering uninstalling because you are having issues, please consider [opening an issue](https://github.com/SteamDeckHomebrew/decky-loader/issues) or [joining our Discord](https://deckbrew.xyz/discord) so we can help you and other users.
1. Press the <img src="./docs/images/light/steam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/steam.svg#gh-light-mode-only" height=16> button and open the Power menu.
1. Select "Switch to Desktop".
1. Run the installer file again, and select `uninstall decky loader`.
- There is also a fast uninstall for those who can use Konsole. Run `curl -L https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/uninstall.sh | sh` and type your password when prompted.
## 🚀 Getting Started
Now that you have Decky Loader installed, you can start using plugins. Each plugin is maintained by a different developer and has its own uses, but most follow a general structure outlined below.
### 📦 Plugins
1. Press the <img src="./docs/images/light/qam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/qam.svg#gh-light-mode-only" height=16> button and navigate to the <img src="./docs/images/light/plug.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/plug.svg#gh-light-mode-only" height=16> icon. This is the Decky menu used for interacting with plugins and the loader itself.
1. Select the <img src="./docs/images/light/store.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/store.svg#gh-light-mode-only" height=16> icon to open the Plugins Browser. This is where you can find and install plugins.
- You can also install from URL in the Settings menu. We do not recommend installing plugins from untrusted sources.
1. To install a plugin, select the "Install" button on the plugin you want. You can also select a version from a dropdown menu, but this is not recommended.
1. To update, uninstall, and reload plugins, navigate to the Decky menu and select the <img src="./docs/images/light/gear.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/gear.svg#gh-light-mode-only" height=16> icon.
- Keep in mind that uninstalling a plugin will only remove its plugin files, not any other files it may have created.
### 🛠️ Plugin Development
There is no complete plugin development documentation yet. However a good starting point is the [plugin template repository](https://github.com/SteamDeckHomebrew/decky-plugin-template). Consider [joining our Discord](https://deckbrew.xyz/discord) if you have any questions.
### 🤝 Contributing
Please consult [the wiki page regarding development](https://wiki.deckbrew.xyz/en/loader-dev/development) for more information on installing development versions of Decky Loader. You can also install the Steam Deck UI on a Windows or Linux computer for testing by following [this YouTube guide](https://youtu.be/1IAbZte8e7E?t=112).
1. Clone the repository using the latest commit to main before starting your PR.
1. In your clone of the repository, run these commands.
```bash
cd frontend
pnpm i
pnpm run build
```
1. If you are modifying the UI, these commands will need to be run before deploying the changes to your Steam Deck.
1. Use the VS Code tasks or `deck.sh` script to deploy your changes to your Steam Deck to test them.
1. You will be testing your changes with the Python script version. You will need to build, deploy, and reload each time.
⚠️ If you are recieving build errors due to an out of date library, you should run this command inside of your repository.
```bash
pnpm update decky-frontend-lib --latest
```
Source control and deploying plugins are left to each respective contributor for the cloned repos in order to keep dependencies up to date.
## 📜 Credits
The original idea for the plugin loader concept is based on the work of [marios8543's Steam Deck UI Inject project](https://github.com/marios8543/steamdeck-ui-inject).
+10
View File
@@ -0,0 +1,10 @@
this directory contains artifacts generated by invocations of https://github.com/nektos/act in order to do local testing of binary builds
how to?
run:
./act/run-act.sh prerelease
or
./act/run-act.sh release
+6
View File
@@ -0,0 +1,6 @@
{
"inputs": {
"release": "prerelease",
"bump": "none"
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"inputs": {
"release": "release",
"bump": "none"
}
}
Executable
+45
View File
@@ -0,0 +1,45 @@
#!/bin/bash
type=$1
# bump=$2
oldartifactsdir="old"
parent_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P )
cd "$parent_path"
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
cd ..
if [[ "$type" == "release" ]]; then
printf "release!\n"
act workflow_dispatch -e act/release.json --artifact-server-path act/artifacts --container-architecture linux/amd64
elif [[ "$type" == "prerelease" ]]; then
printf "prerelease!\n"
act workflow_dispatch -e act/prerelease.json --artifact-server-path act/artifacts --container-architecture linux/amd64
else
printf "Release type unspecified/badly specified.\n"
printf "Options: 'release' or 'prerelease'\n"
fi
cd act/artifacts
if [[ -d "1" ]]; then
cd "1/artifact"
cp "PluginLoader.gz__" "PluginLoader.gz"
gzip -d "PluginLoader.gz"
chmod +x PluginLoader
fi
+236
View File
@@ -0,0 +1,236 @@
# Full imports
import json
# import pprint
# from pprint import pformat
# Partial imports
from aiohttp import ClientSession, web
from asyncio import get_event_loop, sleep
from concurrent.futures import ProcessPoolExecutor
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 shutil import rmtree
from time import time
from zipfile import ZipFile
from localplatform import chown, chmod
# Local modules
from helpers import get_ssl_context, download_remote_binary_to_path
from injector import get_gamepadui_tab
logger = getLogger("Browser")
class PluginInstallContext:
def __init__(self, artifact, name, version, hash) -> None:
self.artifact = artifact
self.name = name
self.version = version
self.hash = hash
class PluginBrowser:
def __init__(self, plugin_path, plugins, loader, settings) -> None:
self.plugin_path = plugin_path
self.plugins = plugins
self.loader = loader
self.settings = settings
self.install_requests = {}
def _unzip_to_plugin_dir(self, zip, name, hash):
zip_hash = sha256(zip.getbuffer()).hexdigest()
if hash and (zip_hash != hash):
return False
zip_file = ZipFile(zip)
zip_file.extractall(self.plugin_path)
plugin_dir = path.join(self.plugin_path, self.find_plugin_folder(name))
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):
rv = False
try:
packageJsonPath = path.join(pluginBasePath, 'package.json')
pluginBinPath = path.join(pluginBasePath, 'bin')
if access(packageJsonPath, R_OK):
with open(packageJsonPath, "r", encoding="utf-8") as f:
packageJson = json.load(f)
if "remote_binary" in packageJson and len(packageJson["remote_binary"]) > 0:
# create bin directory if needed.
chmod(pluginBasePath, 777)
if access(pluginBasePath, W_OK):
if not path.exists(pluginBinPath):
mkdir(pluginBinPath)
if not access(pluginBinPath, W_OK):
chmod(pluginBinPath, 777)
rv = True
for remoteBinary in packageJson["remote_binary"]:
# Required Fields. If any Remote Binary is missing these fail the install.
binName = remoteBinary["name"]
binURL = remoteBinary["url"]
binHash = remoteBinary["sha256hash"]
if not await download_remote_binary_to_path(binURL, binHash, path.join(pluginBinPath, binName)):
rv = False
raise Exception(f"Error Downloading Remote Binary {binName}@{binURL} with hash {binHash} to {path.join(pluginBinPath, binName)}")
chown(self.plugin_path)
chmod(pluginBasePath, 555)
else:
rv = True
logger.debug(f"No Remote Binaries to Download")
except Exception as e:
rv = False
logger.debug(str(e))
return rv
"""Return the filename (only) for the specified plugin"""
def find_plugin_folder(self, name):
for folder in listdir(self.plugin_path):
try:
with open(path.join(self.plugin_path, folder, 'plugin.json'), "r", encoding="utf-8") as f:
plugin = json.load(f)
if plugin['name'] == name:
return folder
except:
logger.debug(f"skipping {folder}")
async def uninstall_plugin(self, name):
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))
try:
logger.info("uninstalling " + name)
logger.info(" at dir " + plugin_dir)
logger.debug("calling frontend unload for %s" % str(name))
res = await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')")
logger.debug("result of unload from UI: %s", res)
# plugins_snapshot = self.plugins.copy()
# snapshot_string = pformat(plugins_snapshot)
# logger.debug("current plugins: %s", snapshot_string)
if name in self.plugins:
logger.debug("Plugin %s was found", name)
self.plugins[name].stop()
logger.debug("Plugin %s was stopped", name)
del self.plugins[name]
logger.debug("Plugin %s was removed from the dictionary", name)
current_plugin_order = self.settings.getSetting("pluginOrder")
current_plugin_order.remove(name)
self.settings.setSetting("pluginOrder", current_plugin_order)
logger.debug("Plugin %s was removed from the pluginOrder setting", name)
logger.debug("removing files %s" % str(name))
rmtree(plugin_dir)
except FileNotFoundError:
logger.warning(f"Plugin {name} not installed, skipping uninstallation")
except Exception as e:
logger.error(f"Plugin {name} in {plugin_dir} was not uninstalled")
logger.error(f"Error at %s", exc_info=e)
if self.loader.watcher:
self.loader.watcher.disabled = False
async def _install(self, artifact, name, version, hash):
# Will be set later in code
res_zip = None
# Check if plugin is installed
isInstalled = False
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})")
res_zip = BytesIO(open(artifact[7:], "rb").read())
else:
logger.info(f"Installing {name} from URL (Version: {version})")
async with ClientSession() as client:
logger.debug(f"Fetching {artifact}")
res = await client.get(artifact, ssl=get_ssl_context())
if res.status == 200:
logger.debug("Got 200. Reading...")
data = await res.read()
logger.debug(f"Read {len(data)} bytes")
res_zip = BytesIO(data)
else:
logger.fatal(f"Could not fetch from URL. {await res.text()}")
# Check to make sure we got the file
if res_zip is None:
logger.fatal(f"Could not fetch {artifact}")
return
# If plugin is installed, uninstall it
if isInstalled:
try:
logger.debug("Uninstalling existing plugin...")
await self.uninstall_plugin(name)
except:
logger.error(f"Plugin {name} could not be uninstalled.")
# Install the plugin
logger.debug("Unzipping...")
ret = self._unzip_to_plugin_dir(res_zip, name, hash)
if ret:
plugin_folder = self.find_plugin_folder(name)
plugin_dir = path.join(self.plugin_path, plugin_folder)
ret = await self._download_remote_binaries_for_plugin_with_name(plugin_dir)
if ret:
logger.info(f"Installed {name} (Version: {version})")
if name in self.loader.plugins:
self.loader.plugins[name].stop()
self.loader.plugins.pop(name, None)
await sleep(1)
current_plugin_order = self.settings.getSetting("pluginOrder")
current_plugin_order.append(name)
self.settings.setSetting("pluginOrder", current_plugin_order)
logger.debug("Plugin %s was added to the pluginOrder setting", name)
self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder)
else:
logger.fatal(f"Failed Downloading Remote Binaries")
else:
self.log.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
if self.loader.watcher:
self.loader.watcher.disabled = False
async def request_plugin_install(self, artifact, name, version, hash, install_type):
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):
request_id = str(time())
self.install_requests[request_id] = [PluginInstallContext(req['artifact'], req['name'], req['version'], req['hash']) for req in requests]
js_requests_parameter = ','.join([
f"{{ name: '{req['name']}', version: '{req['version']}', hash: '{req['hash']}', install_type: {req['install_type']}}}" for req in requests
])
tab = await get_gamepadui_tab()
await tab.open_websocket()
await tab.evaluate_js(f"DeckyPluginLoader.addMultiplePluginsInstallPrompt('{request_id}', [{js_requests_parameter}])")
async def confirm_plugin_install(self, request_id):
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):
self.install_requests.pop(request_id)
+6
View File
@@ -0,0 +1,6 @@
from enum import Enum
class UserType(Enum):
HOST_USER = 1
EFFECTIVE_USER = 2
ROOT = 3
+162
View File
@@ -0,0 +1,162 @@
import re
import ssl
import uuid
import os
import sys
import subprocess
from hashlib import sha256
from io import BytesIO
import certifi
from aiohttp.web import Response, middleware
from aiohttp import ClientSession
import localplatform
from customtypes import UserType
from logging import getLogger
REMOTE_DEBUGGER_UNIT = "steam-web-debug-portforward.service"
# global vars
csrf_token = str(uuid.uuid4())
ssl_ctx = ssl.create_default_context(cafile=certifi.where())
assets_regex = re.compile("^/plugins/.*/assets/.*")
frontend_regex = re.compile("^/frontend/.*")
logger = getLogger("Main")
def get_ssl_context():
return ssl_ctx
def get_csrf_token():
return csrf_token
@middleware
async def csrf_middleware(request, handler):
if str(request.method) == "OPTIONS" or request.headers.get('Authentication') == csrf_token or str(request.rel_url) == "/auth/token" or str(request.rel_url).startswith("/plugins/load_main/") or str(request.rel_url).startswith("/static/") or str(request.rel_url).startswith("/legacy/") or str(request.rel_url).startswith("/steam_resource/") or str(request.rel_url).startswith("/frontend/") or assets_regex.match(str(request.rel_url)) or frontend_regex.match(str(request.rel_url)):
return await handler(request)
return Response(text='Forbidden', status='403')
# Get the default homebrew path unless a home_path is specified. home_path argument is deprecated
def get_homebrew_path(home_path = None) -> str:
return localplatform.get_unprivileged_path()
# Recursively create path and chown as user
def mkdir_as_user(path):
path = os.path.realpath(path)
os.makedirs(path, exist_ok=True)
localplatform.chown(path)
# Fetches the version of loader
def get_loader_version() -> str:
try:
with open(os.path.join(os.getcwd(), ".loader.version"), "r", encoding="utf-8") as version_file:
return version_file.readline().strip()
except Exception as e:
logger.warn(f"Failed to execute get_loader_version(): {str(e)}")
return "unknown"
# returns the appropriate system python paths
def get_system_pythonpaths() -> list[str]:
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:
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)
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:
rv = False
try:
if os.access(os.path.dirname(path), os.W_OK):
async with ClientSession() as client:
res = await client.get(url, ssl=get_ssl_context())
if res.status == 200:
data = BytesIO(await res.read())
remoteHash = sha256(data.getbuffer()).hexdigest()
if binHash == remoteHash:
data.seek(0)
with open(path, 'wb') as f:
f.write(data.getbuffer())
rv = True
else:
raise Exception(f"Fatal Error: Hash Mismatch for remote binary {path}@{url}")
else:
rv = False
except:
rv = False
return rv
# Deprecated
def set_user():
pass
# Deprecated
def set_user_group() -> str:
return get_user_group()
#########
# Below is legacy code, provided for backwards compatibility. This will break on windows
#########
# Get the user id hosting the plugin loader
def get_user_id() -> int:
return localplatform.localplatform._get_user_id()
# Get the user hosting the plugin loader
def get_user() -> str:
return localplatform.localplatform._get_user()
# Get the effective user id of the running process
def get_effective_user_id() -> int:
return localplatform.localplatform._get_effective_user_id()
# Get the effective user of the running process
def get_effective_user() -> str:
return localplatform.localplatform._get_effective_user()
# 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()
# Get the effective user group of the running process
def get_effective_user_group() -> str:
return localplatform.localplatform._get_effective_user_group()
# Get the user owner of the given file path.
def get_user_owner(file_path) -> str:
return localplatform.localplatform._get_user_owner(file_path)
# 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 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()
# Get the default home path unless a user is specified
def get_home_path(username = None) -> str:
return localplatform.get_home_path(UserType.ROOT if username == "root" else UserType.HOST_USER)
async def is_systemd_unit_active(unit_name: str) -> bool:
return await localplatform.service_active(unit_name)
async def stop_systemd_unit(unit_name: str) -> bool:
return await localplatform.service_stop(unit_name)
async def start_systemd_unit(unit_name: str) -> bool:
return await localplatform.service_start(unit_name)
+421
View File
@@ -0,0 +1,421 @@
# Injector code from https://github.com/SteamDeckHomebrew/steamdeck-ui-inject. More info on how it works there.
from asyncio import sleep
from logging import getLogger
from traceback import format_exc
from typing import List
from aiohttp import ClientSession, WSMsgType
from aiohttp.client_exceptions import ClientConnectorError, ClientOSError
from asyncio.exceptions import TimeoutError
import uuid
BASE_ADDRESS = "http://localhost:8080"
logger = getLogger("Injector")
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"]
self.websocket = None
self.client = None
async def open_websocket(self):
self.client = ClientSession()
self.websocket = await self.client.ws_connect(self.ws_url)
async def close_websocket(self):
await self.websocket.close()
await self.client.close()
async def listen_for_message(self):
async for message in self.websocket:
data = message.json()
yield data
logger.warn(f"The Tab {self.title} socket has been disconnected while listening for messages.")
await self.close_websocket()
async def _send_devtools_cmd(self, dc, receive=True):
if self.websocket:
self.cmd_id += 1
dc["id"] = self.cmd_id
await self.websocket.send_json(dc)
if receive:
async for msg in self.listen_for_message():
if "id" in msg and msg["id"] == dc["id"]:
return msg
return None
raise RuntimeError("Websocket not opened")
async def evaluate_js(self, js, run_async=False, manage_socket=True, get_result=True):
try:
if manage_socket:
await self.open_websocket()
res = await self._send_devtools_cmd({
"method": "Runtime.evaluate",
"params": {
"expression": js,
"userGesture": True,
"awaitPromise": run_async
}
}, get_result)
finally:
if manage_socket:
await self.close_websocket()
return res
async def has_global_var(self, var_name, manage_socket=True):
res = await self.evaluate_js(f"window['{var_name}'] !== null && window['{var_name}'] !== undefined", False, manage_socket)
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):
try:
if manage_socket:
await self.open_websocket()
res = await self._send_devtools_cmd({
"method": "Page.close",
}, False)
finally:
if manage_socket:
await self.close_websocket()
return res
async def enable(self):
"""
Enables page domain notifications.
"""
await self._send_devtools_cmd({
"method": "Page.enable",
}, False)
async def disable(self):
"""
Disables page domain notifications.
"""
await self._send_devtools_cmd({
"method": "Page.disable",
}, False)
async def refresh(self, manage_socket=True):
try:
if manage_socket:
await self.open_websocket()
await self._send_devtools_cmd({
"method": "Page.reload",
}, False)
finally:
if manage_socket:
await self.close_websocket()
return
async def reload_and_evaluate(self, js, manage_socket=True):
"""
Reloads the current tab, with JS to run on load via debugger
"""
try:
if manage_socket:
await self.open_websocket()
await self._send_devtools_cmd({
"method": "Debugger.enable"
}, True)
await self._send_devtools_cmd({
"method": "Runtime.evaluate",
"params": {
"expression": "location.reload();",
"userGesture": True,
"awaitPromise": False
}
}, False)
breakpoint_res = await self._send_devtools_cmd({
"method": "Debugger.setInstrumentationBreakpoint",
"params": {
"instrumentation": "beforeScriptExecution"
}
}, True)
logger.info(breakpoint_res)
# Page finishes loading when breakpoint hits
for x in range(20):
# this works around 1/5 of the time, so just send it 8 times.
# the js accounts for being injected multiple times allowing only one instance to run at a time anyway
await self._send_devtools_cmd({
"method": "Runtime.evaluate",
"params": {
"expression": js,
"userGesture": True,
"awaitPromise": False
}
}, False)
await self._send_devtools_cmd({
"method": "Debugger.removeBreakpoint",
"params": {
"breakpointId": breakpoint_res["result"]["breakpointId"]
}
}, False)
for x in range(4):
await self._send_devtools_cmd({
"method": "Debugger.resume"
}, False)
await self._send_devtools_cmd({
"method": "Debugger.disable"
}, True)
finally:
if manage_socket:
await self.close_websocket()
return
async def add_script_to_evaluate_on_new_document(self, js, add_dom_wrapper=True, manage_socket=True, get_result=True):
"""
How the underlying call functions is not particularly clear from the devtools docs, so stealing puppeteer's description:
Adds a function which would be invoked in one of the following scenarios:
* whenever the page is navigated
* whenever the child frame is attached or navigated. In this case, the
function is invoked in the context of the newly attached frame.
The function is invoked after the document was created but before any of
its scripts were run. This is useful to amend the JavaScript environment,
e.g. to seed `Math.random`.
Parameters
----------
js : str
The script to evaluate on new document
add_dom_wrapper : bool
True to wrap the script in a wait for the 'DOMContentLoaded' event.
DOM will usually not exist when this execution happens,
so it is necessary to delay til DOM is loaded if you are modifying it
manage_socket : bool
True to have this function handle opening/closing the websocket for this tab
get_result : bool
True to wait for the result of this call
Returns
-------
int or None
The identifier of the script added, used to remove it later.
(see remove_script_to_evaluate_on_new_document below)
None is returned if `get_result` is False
"""
try:
wrappedjs = """
function scriptFunc() {
{js}
}
if (document.readyState === 'loading') {
addEventListener('DOMContentLoaded', () => {
scriptFunc();
});
} else {
scriptFunc();
}
""".format(js=js) if add_dom_wrapper else js
if manage_socket:
await self.open_websocket()
res = await self._send_devtools_cmd({
"method": "Page.addScriptToEvaluateOnNewDocument",
"params": {
"source": wrappedjs
}
}, get_result)
finally:
if manage_socket:
await self.close_websocket()
return res
async def remove_script_to_evaluate_on_new_document(self, script_id, manage_socket=True):
"""
Removes a script from a page that was added with `add_script_to_evaluate_on_new_document`
Parameters
----------
script_id : int
The identifier of the script to remove (returned from `add_script_to_evaluate_on_new_document`)
"""
try:
if manage_socket:
await self.open_websocket()
res = await self._send_devtools_cmd({
"method": "Page.removeScriptToEvaluateOnNewDocument",
"params": {
"identifier": script_id
}
}, False)
finally:
if manage_socket:
await self.close_websocket()
async def has_element(self, element_name, manage_socket=True):
res = await self.evaluate_js(f"document.getElementById('{element_name}') != null", False, manage_socket)
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):
try:
css_id = str(uuid.uuid4())
result = await self.evaluate_js(
f"""
(function() {{
const style = document.createElement('style');
style.id = "{css_id}";
document.head.append(style);
style.textContent = `{style}`;
}})()
""", False, manage_socket)
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(self, css_id, manage_socket=True):
try:
result = await self.evaluate_js(
f"""
(function() {{
let style = document.getElementById("{css_id}");
if (style.nodeName.toLowerCase() == 'style')
style.parentNode.removeChild(style);
}})()
""", False, manage_socket)
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_steam_resource(self, url):
res = await self.evaluate_js(f'(async function test() {{ return await (await fetch("{url}")).text() }})()', True)
return res["result"]["result"]["value"]
def __repr__(self):
return self.title
async def get_tabs() -> List[Tab]:
res = {}
na = False
while True:
try:
async with ClientSession() as web:
res = await web.get(f"{BASE_ADDRESS}/json", timeout=3)
except ClientConnectorError:
if not na:
logger.debug("Steam isn't available yet. Wait for a moment...")
na = True
await sleep(5)
except ClientOSError:
logger.warn(f"The request to {BASE_ADDRESS}/json was reset")
await sleep(1)
except TimeoutError:
logger.warn(f"The request to {BASE_ADDRESS}/json timed out")
await sleep(1)
else:
break
if res.status == 200:
r = await res.json()
return [Tab(i) for i in r]
else:
raise Exception(f"/json did not return 200. {await res.text()}")
async def get_tab(tab_name) -> 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:
tabs = await get_tabs()
tab = next((i for i in tabs if test(i)), None)
if not tab:
raise ValueError(f"Tab not found by lambda")
return tab
SHARED_CTX_NAMES = ["SharedJSContext", "Steam Shared Context presented by Valve™", "Steam", "SP"]
DO_NOT_CLOSE_URL = "Valve Steam Gamepad/default" # 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
async def get_gamepadui_tab() -> Tab:
tabs = await get_tabs()
tab = next((i for i in tabs if tab_is_gamepadui(i)), None)
if not tab:
raise ValueError(f"GamepadUI Tab not found")
return tab
async def inject_to_tab(tab_name, js, run_async=False):
tab = await get_tab(tab_name)
return await tab.evaluate_js(js, run_async)
async def close_old_tabs():
tabs = await get_tabs()
for t in tabs:
if not t.title or (t.title not in SHARED_CTX_NAMES and DO_NOT_CLOSE_URL not in t.url):
logger.debug("Closing tab: " + getattr(t, "title", "Untitled"))
await t.close()
await sleep(0.5)
@@ -8,18 +8,20 @@ window.addEventListener("message", function(evt) {
}, false);
async function call_server_method(method_name, arg_object={}) {
let id = `${uuidv4()}`;
console.debug(JSON.stringify({
"id": id,
"method": method_name,
"args": arg_object
}));
return new Promise((resolve, reject) => {
method_call_ev_target.addEventListener(`${id}`, function (event) {
if (event.data.success) resolve(event.data.result);
else reject(event.data.result);
});
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!
@@ -41,11 +43,22 @@ async function fetch_nocors(url, 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)");
return await call_server_method("plugin_method", {
'plugin_name': plugin_name,
'method_name': method_name,
'args': 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/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) {
+219
View File
@@ -0,0 +1,219 @@
from asyncio import Queue, sleep
from json.decoder import JSONDecodeError
from logging import getLogger
from os import listdir, path
from pathlib import Path
from traceback import print_exc
from aiohttp import web
from os.path import exists
from watchdog.events import RegexMatchingEventHandler
from watchdog.observers import Observer
from injector import get_tab, get_gamepadui_tab
from plugin import PluginWrapper
class FileChangeHandler(RegexMatchingEventHandler):
def __init__(self, queue, plugin_path) -> None:
super().__init__(regexes=[r'^.*?dist\/index\.js$', r'^.*?main\.py$'])
self.logger = getLogger("file-watcher")
self.plugin_path = plugin_path
self.queue = queue
self.disabled = True
def maybe_reload(self, src_path):
if self.disabled:
return
plugin_dir = Path(path.relpath(src_path, self.plugin_path)).parts[0]
if exists(path.join(self.plugin_path, plugin_dir, "plugin.json")):
self.queue.put_nowait((path.join(self.plugin_path, plugin_dir, "main.py"), plugin_dir, True))
def on_created(self, event):
src_path = event.src_path
if "__pycache__" in src_path:
return
# check to make sure this isn't a directory
if path.isdir(src_path):
return
# get the directory name of the plugin so that we can find its "main.py" and reload it; the
# file that changed is not necessarily the one that needs to be reloaded
self.logger.debug(f"file created: {src_path}")
self.maybe_reload(src_path)
def on_modified(self, event):
src_path = event.src_path
if "__pycache__" in src_path:
return
# check to make sure this isn't a directory
if path.isdir(src_path):
return
# get the directory name of the plugin so that we can find its "main.py" and reload it; the
# file that changed is not necessarily the one that needs to be reloaded
self.logger.debug(f"file modified: {src_path}")
self.maybe_reload(src_path)
class Loader:
def __init__(self, server_instance, plugin_path, loop, live_reload=False) -> None:
self.loop = loop
self.logger = getLogger("Loader")
self.plugin_path = plugin_path
self.logger.info(f"plugin_path: {self.plugin_path}")
self.plugins = {}
self.watcher = None
self.live_reload = live_reload
if live_reload:
self.reload_queue = Queue()
self.observer = Observer()
self.watcher = FileChangeHandler(self.reload_queue, plugin_path)
self.observer.schedule(self.watcher, self.plugin_path, recursive=True)
self.observer.start()
self.loop.create_task(self.handle_reloads())
self.loop.create_task(self.enable_reload_wait())
server_instance.add_routes([
web.get("/frontend/{path:.*}", self.handle_frontend_assets),
web.get("/locales/{path:.*}", self.handle_frontend_locales),
web.get("/plugins", self.get_plugins),
web.get("/plugins/{plugin_name}/frontend_bundle", self.handle_frontend_bundle),
web.post("/plugins/{plugin_name}/methods/{method_name}", self.handle_plugin_method_call),
web.get("/plugins/{plugin_name}/assets/{path:.*}", self.handle_plugin_frontend_assets),
# The following is legacy plugin code.
web.get("/plugins/load_main/{name}", self.load_plugin_main_view),
web.get("/plugins/plugin_resource/{name}/{path:.+}", self.handle_sub_route),
web.get("/steam_resource/{path:.+}", self.get_steam_resource)
])
async def enable_reload_wait(self):
if self.live_reload:
await sleep(10)
self.logger.info("Hot reload enabled")
self.watcher.disabled = False
async def handle_frontend_assets(self, request):
file = path.join(path.dirname(__file__), "static", request.match_info["path"])
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
async def handle_frontend_locales(self, request):
req_lang = request.match_info["path"]
file = path.join(path.dirname(__file__), "locales", req_lang)
if exists(file):
return web.FileResponse(file, headers={"Cache-Control": "no-cache", "Content-Type": "application/json"})
else:
self.logger.info(f"Language {req_lang} not available, returning an empty dictionary")
return web.json_response(data={}, headers={"Cache-Control": "no-cache"})
async def get_plugins(self, request):
plugins = list(self.plugins.values())
return web.json_response([{"name": str(i) if not i.legacy else "$LEGACY_"+str(i), "version": i.version} for i in plugins])
def handle_plugin_frontend_assets(self, request):
plugin = self.plugins[request.match_info["plugin_name"]]
file = path.join(self.plugin_path, plugin.plugin_directory, "dist/assets", request.match_info["path"])
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
def handle_frontend_bundle(self, request):
plugin = self.plugins[request.match_info["plugin_name"]]
with open(path.join(self.plugin_path, plugin.plugin_directory, "dist/index.js"), "r", encoding="utf-8") as bundle:
return web.Response(text=bundle.read(), content_type="application/javascript")
def import_plugin(self, file, plugin_directory, refresh=False, batch=False):
try:
plugin = PluginWrapper(file, plugin_directory, self.plugin_path)
if plugin.name in self.plugins:
if not "debug" in plugin.flags and refresh:
self.logger.info(f"Plugin {plugin.name} is already loaded and has requested to not be re-loaded")
return
else:
self.plugins[plugin.name].stop()
self.plugins.pop(plugin.name, None)
if plugin.passive:
self.logger.info(f"Plugin {plugin.name} is passive")
self.plugins[plugin.name] = plugin.start()
self.logger.info(f"Loaded {plugin.name}")
if not batch:
self.loop.create_task(self.dispatch_plugin(plugin.name if not plugin.legacy else "$LEGACY_" + plugin.name, plugin.version))
except Exception as e:
self.logger.error(f"Could not load {file}. {e}")
print_exc()
async def dispatch_plugin(self, name, version):
gpui_tab = await get_gamepadui_tab()
await gpui_tab.evaluate_js(f"window.importDeckyPlugin('{name}', '{version}')")
def import_plugins(self):
self.logger.info(f"import plugins from {self.plugin_path}")
directories = [i for i in listdir(self.plugin_path) if path.isdir(path.join(self.plugin_path, i)) and path.isfile(path.join(self.plugin_path, i, "plugin.json"))]
for directory in directories:
self.logger.info(f"found plugin: {directory}")
self.import_plugin(path.join(self.plugin_path, directory, "main.py"), directory, False, True)
async def handle_reloads(self):
while True:
args = await self.reload_queue.get()
self.import_plugin(*args)
async def handle_plugin_method_call(self, request):
res = {}
plugin = self.plugins[request.match_info["plugin_name"]]
method_name = request.match_info["method_name"]
try:
method_info = await request.json()
args = method_info["args"]
except JSONDecodeError:
args = {}
try:
if method_name.startswith("_"):
raise RuntimeError("Tried to call private method")
res["result"] = await plugin.execute_method(method_name, args)
res["success"] = True
except Exception as e:
res["result"] = str(e)
res["success"] = False
return web.json_response(res)
"""
The following methods are used to load legacy plugins, which are considered deprecated.
I made the choice to re-add them so that the first iteration/version of the react loader
can work as a drop-in replacement for the stable branch of the PluginLoader, so that we
can introduce it more smoothly and give people the chance to sample the new features even
without plugin support. They will be removed once legacy plugins are no longer relevant.
"""
async def load_plugin_main_view(self, request):
plugin = self.plugins[request.match_info["name"]]
with open(path.join(self.plugin_path, plugin.plugin_directory, plugin.main_view_html), "r", encoding="utf-8") as template:
template_data = template.read()
ret = f"""
<script src="/legacy/library.js"></script>
<script>window.plugin_name = '{plugin.name}' </script>
<base href="http://127.0.0.1:1337/plugins/plugin_resource/{plugin.name}/">
{template_data}
"""
return web.Response(text=ret, content_type="text/html")
async def handle_sub_route(self, request):
plugin = self.plugins[request.match_info["name"]]
route_path = request.match_info["path"]
self.logger.info(path)
ret = ""
file_path = path.join(self.plugin_path, plugin.plugin_directory, route_path)
with open(file_path, "r", encoding="utf-8") as resource_data:
ret = resource_data.read()
return web.Response(text=ret)
async def get_steam_resource(self, request):
tab = await get_tab("SP")
try:
return web.Response(text=await tab.get_steam_resource(f"https://steamloopback.host/{request.match_info['path']}"), content_type="text/html")
except Exception as e:
return web.Response(text=str(e), status=400)
+183
View File
@@ -0,0 +1,183 @@
{
"BranchSelect": {
"update_channel": {
"label": "Updatekanal",
"prerelease": "Vorabveröffentlichung",
"stable": "Standard",
"testing": "Test"
}
},
"Developer": {
"disabling": "Deaktiviere",
"enabling": "Aktiviere",
"5secreload": "Neu laden in 5 Sekunden"
},
"FilePickerIndex": {
"folder": {
"select": "Diesen Ordner verwenden"
}
},
"PluginCard": {
"plugin_install": "Installieren",
"plugin_no_desc": "Keine Beschreibung angegeben.",
"plugin_version_label": "Erweiterungs Version",
"plugin_full_access": "Diese Erweiterung hat uneingeschränkten Zugriff auf dein Steam Deck."
},
"PluginInstallModal": {
"install": {
"button_idle": "Installieren",
"button_processing": "Wird installiert",
"desc": "Bist du dir sicher, dass du {{artifact}}{{version}} installieren willst?",
"title": "Installiere {{artifact}}"
},
"reinstall": {
"button_idle": "Neu installieren",
"button_processing": "Wird neu installiert",
"desc": "Bist du dir sicher, dass du {{artifact}}{{version}} neu installieren willst?",
"title": "Neu installation {{artifact}}"
},
"update": {
"button_idle": "Aktualisieren",
"button_processing": "Wird aktualisiert",
"title": "Aktualisiere {{artifact}}",
"desc": "Bist du dir sicher, dass du {{artifact}}{{version}} aktualisieren willst?"
},
"no_hash": "Diese Erweiterung besitzt keine Prüfsumme, Installation auf eigene Gefahr."
},
"PluginListIndex": {
"no_plugin": "Keine Erweiterungen installiert!",
"plugin_actions": "Erweiterungs Aktionen",
"reinstall": "Neu installieren",
"reload": "Neu laden",
"uninstall": "Deinstallieren",
"update_to": "Aktualisieren zu {{name}}"
},
"PluginLoader": {
"decky_title": "Decky",
"decky_update_available": "Eine neue Version ({{tag_name}}) ist verfügbar!",
"error": "Fehler",
"plugin_load_error": {
"toast": "Fehler beim Laden von {{name}}",
"message": "Fehler beim Laden von {{name}}"
},
"plugin_uninstall": {
"button": "Deinstallieren",
"desc": "Bist du dir sicher, dass du {{name}} deinstallieren willst?",
"title": "Deinstalliere {{name}}"
},
"plugin_error_uninstall": "Das Laden von {{name}} hat einen Fehler verursacht. Dies bedeutet normalerweise, dass die Erweiterung ein Update für die neue Version von SteamUI benötigt. Prüfe in den Decky-Einstellungen im Bereich Erweiterungen, ob ein Update vorhanden ist.",
"plugin_update_one": "1 Erweiterung kann aktualisiert werden!",
"plugin_update_other": "{{count}} Erweiterungen können aktualisiert werden!"
},
"RemoteDebugging": {
"remote_cef": {
"label": "Remote CEF Debugging Zugriff",
"desc": "Erlaubt jedem aus dem Neztwerk unautorisierten Zugriff auf den CEF Debugger"
}
},
"SettingsDeveloperIndex": {
"header": "Sonstiges",
"react_devtools": {
"ip_label": "IP",
"label": "Aktiviere React DevTools",
"desc": "Erlaubt die Verbindung mit einem anderen Rechner, auf welchem React DevTools läuft. Eine Änderung startet Steam neu. Die IP Adresse muss vor Aktivierung ausgefüllt sein."
},
"third_party_plugins": {
"button_zip": "Durchsuchen",
"header": "Erweiterungen von Drittanbietern",
"label_desc": "URL",
"label_zip": "Installiere Erweiterung via ZIP Datei",
"button_install": "Installieren",
"label_url": "Installiere Erweiterung via URL"
},
"toast_zip": {
"body": "Installation fehlgeschlagen! Nur ZIP Datein werden unterstützt.",
"title": "Decky"
},
"valve_internal": {
"desc2": "Fasse in diesem Menü nichts an, es sei denn, du weißt was du tust.",
"label": "Aktiviere Valve-internes Menü",
"desc1": "Aktiviert das Valve-interne Entwickler Menü."
}
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Decky Version",
"header": "Über"
},
"beta": {
"header": "Beta Teilnahme"
},
"developer_mode": {
"desc": "Aktiviere Deckys Entwickleroptionen.",
"label": "Entwickleroptionen"
},
"other": {
"header": "Sonstiges"
},
"updates": {
"header": "Aktualisierungen"
}
},
"SettingsIndex": {
"developer_title": "Entwickler",
"general_title": "Allgemein",
"plugins_title": "Erweiterungen",
"navbar_settings": "Decky Einstellungen"
},
"Store": {
"store_contrib": {
"label": "Mitwirken",
"desc": "Wenn du Erweiterungen im Decky Store veröffentlichen willst, besuche die SteamDeckHomebrew/decky-plugin-template Repository auf GitHub. Informationen rund um Entwicklung und Veröffentlichung findest du in der README."
},
"store_filter": {
"label": "Filter",
"label_def": "Alle"
},
"store_search": {
"label": "Suche"
},
"store_sort": {
"label": "Sortierung",
"label_def": "Zuletzt aktualisiert"
},
"store_source": {
"desc": "Jeder Erweiterungs Quellcode ist in der SteamDeckHomebrew/decky-plugin-database Repository auf GitHub verfügbar.",
"label": "Quellcode"
},
"store_tabs": {
"about": "Über",
"alph_asce": "Alphabetisch (Z zu A)",
"alph_desc": "Alphabetisch (A zu Z)",
"title": "Durchstöbern"
},
"store_testing_cta": "Unterstütze das Decky Loader Team mit dem Testen von neuen Erweiterungen!"
},
"StoreSelect": {
"custom_store": {
"label": "Benutzerdefinierter Marktplatz",
"url_label": "URL"
},
"store_channel": {
"custom": "Benutzerdefiniert",
"default": "Standard",
"label": "Marktplatz Kanal",
"testing": "Test"
}
},
"Updater": {
"decky_updates": "Decky Aktualisierungen",
"patch_notes_desc": "Patchnotizen",
"updates": {
"check_button": "Auf Aktualisierungen prüfen",
"checking": "Wird überprüft",
"cur_version": "Aktualle Version: {{ver}}",
"install_button": "Aktualisierung installieren",
"label": "Aktualisierungen",
"lat_version": "{{ver}} ist die aktuellste",
"reloading": "Lade neu",
"updating": "Aktualisiere"
},
"no_patch_notes_desc": "Für diese Version gibt es keine Patchnotizen"
}
}
+180
View File
@@ -0,0 +1,180 @@
{
"SettingsDeveloperIndex": {
"react_devtools": {
"desc": "Επιτρέπει την σύνδεση με υπολογιστή που τρέχει React DevTools. Η αλλαγή αυτής της ρύθμισης θα προκαλέσει επαναφόρτωση του Steam. Ωρίστε την διεύθυνση IP πριν την ενεργοποιήσετε.",
"ip_label": "IP",
"label": "Ενεργοποίηση React DevTools"
},
"third_party_plugins": {
"button_install": "Εγκατάσταση",
"button_zip": "Περιήγηση",
"header": "Επεκτάσεις τρίτων",
"label_desc": "URL",
"label_url": "Εγκατάσταση επέκτασης απο URL",
"label_zip": "Εγκατάσταση επέκτασης από αρχείο ZIP"
},
"toast_zip": {
"title": "Decky",
"body": "Η εγκατάσταση απέτυχε. Μόνο αρχεία ZIP επιτρέπονται."
},
"valve_internal": {
"desc1": "Ενεργοποιεί το μενού προγραμματιστή της Valve.",
"desc2": "Μην αγγίξετε τίποτα σε αυτό το μενού εκτός και αν ξέρετε τι κάνει.",
"label": "Ενεργοποιήση εσωτερικού μενού Valve"
}
},
"BranchSelect": {
"update_channel": {
"prerelease": "Προ-κυκλοφορία",
"stable": "Σταθερό",
"label": "Κανάλι ενημερώσεων",
"testing": "Δοκιμαστικό"
}
},
"Developer": {
"5secreload": "Γίνεται επαναφόρτωση σε 5 δευτερόλεπτα",
"disabling": "Γίνεται απενεργοποίηση",
"enabling": "Γίνεται ενεργοποίηση"
},
"PluginCard": {
"plugin_no_desc": "Δεν υπάρχει περιγραφή.",
"plugin_full_access": "Αυτή η επέκταση έχει πλήρη πρόσβαση στο Steam Deck σας.",
"plugin_install": "Εγκατάσταση",
"plugin_version_label": "Έκδοση επέκτασης"
},
"PluginInstallModal": {
"install": {
"desc": "Σίγουρα θέλετε να εγκαταστήσετε το {{artifact}}{{version}};",
"button_idle": "Εγκατάσταση",
"button_processing": "Γίνεται εγκατάσταση",
"title": "Εγκατάσταση {{artifact}}"
},
"no_hash": "Αυτή η επέκταση δεν έχει υπογραφή, την εγκαθηστάτε με δικό σας ρίσκο.",
"reinstall": {
"button_idle": "Επανεγκατάσταση",
"button_processing": "Γίνεται επανεγκατάσταση",
"desc": "Σίγουρα θέλετε να επανεγκαταστήσετε το {{artifact}}{{version}};",
"title": "Επανεγκατάσταση {{artifact}}"
},
"update": {
"button_idle": "Ενημέρωση",
"desc": "Σίγουρα θέλετε να ενημερώσετε το {{artifact}} {{version}};",
"title": "Ενημέρωση {{artifact}}",
"button_processing": "Γίνεται ενημέρωση"
}
},
"PluginListIndex": {
"no_plugin": "Δεν υπάρχουν εγκατεστημένες επεκτάσεις!",
"plugin_actions": "Ενέργειες επεκτάσεων",
"reinstall": "Επανεγκατάσταση",
"reload": "Επαναφόρτωση",
"uninstall": "Απεγκατάσταση",
"update_to": "Ενημέρωση σε {{name}}"
},
"PluginLoader": {
"decky_title": "Decky",
"decky_update_available": "Ενημέρωση σε {{tag_name}} διαθέσιμη!",
"error": "Σφάλμα",
"plugin_error_uninstall": "Πηγαίντε στο <0></0> στο μενού του Decky για να απεγκαταστήσετε αυτή την επέκταση.",
"plugin_load_error": {
"message": "Σφάλμα στη φόρτωση της επέκτασης {{name}}",
"toast": "Σφάλμα φόρτωσης {{name}}"
},
"plugin_uninstall": {
"button": "Απεγκατάσταση",
"desc": "Σίγουρα θέλετε να απεγκαταστήσετε το {{name}};",
"title": "Απεγκατάσταση {{name}}"
}
},
"RemoteDebugging": {
"remote_cef": {
"label": "Να επιτρέπεται η απομακρυσμένη πρόσβαση στον CEF debugger",
"desc": "Να επιτρέπεται η ανεξέλεγκτη πρόσβαση στον CEF debugger σε οποιονδήποτε στο τοπικό δίκτυο"
}
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Έκδοση Decky",
"header": "Σχετικά"
},
"developer_mode": {
"desc": "Ενεργοποιεί το μενού προγραμματιστή του Decky.",
"label": "Λειτουργία προγραμματιστή"
},
"other": {
"header": "Άλλα"
},
"updates": {
"header": "Ενημερώσεις"
},
"beta": {
"header": "Συμμετοχή στη Beta"
}
},
"SettingsIndex": {
"plugins_title": "Επεκτάσεις",
"developer_title": "Προγραμματιστής",
"general_title": "Γενικά",
"navbar_settings": "Ρυθμίσεις Decky"
},
"Store": {
"store_contrib": {
"label": "Συνεισφέροντας",
"desc": "Αν θέλετε να συνεισφέρετε στο κατάστημα επεκτάσεων του Decky, τσεκάρετε το SteamDeckHomebrew/decky-plugin-template repository στο GitHub. Πληροφοριές σχετικά με τη δημιουργία και τη διανομή επεκτάσεων είναι διαθέσιμες στο README."
},
"store_filter": {
"label": "Φίλτρο",
"label_def": "Όλα"
},
"store_search": {
"label": "Αναζήτηση"
},
"store_sort": {
"label": "Ταξινόμηση",
"label_def": "Τελευταία ενημέρωση (Νεότερα)"
},
"store_source": {
"desc": "Ο πηγαίος κώδικας όλων των επεκτάσεων είναι διαθέσιμος στο SteamDeckHomebrew/decky-plugin-database repository στο GitHub.",
"label": "Πηγαίος κώδικας"
},
"store_tabs": {
"about": "Σχετικά",
"alph_asce": "Αλφαβητικά (Ζ σε Α)",
"alph_desc": "Αλφαβητικά (Α σε Ζ)",
"title": "Περιήγηση"
},
"store_testing_cta": "Παρακαλώ σκεφτείτε να τεστάρετε νέες επεκτάσεις για να βοηθήσετε την ομάδα του Decky Loader!"
},
"StoreSelect": {
"custom_store": {
"label": "Προσαρμοσμένο κατάστημα",
"url_label": "URL"
},
"store_channel": {
"custom": "Προσαρμοσμένο",
"default": "Προεπιλεγμένο",
"label": "Κανάλι καταστήματος",
"testing": "Δοκιμαστικό"
}
},
"Updater": {
"no_patch_notes_desc": "Κανένα ενημερωτικό σημείωμα για αυτή την έκδοση",
"patch_notes_desc": "Σημειώσεις ενημέρωσης",
"updates": {
"check_button": "Έλεγχος για ενημερώσεις",
"checking": "Γίνεται έλεγχος",
"cur_version": "Τρέχουσα έκδοση: {{ver}}",
"install_button": "Εγκατάσταση ενημέρωσης",
"label": "Ενημερώσεις",
"updating": "Γίνεται ενημέρωση",
"lat_version": "Ενημερωμένο: τρέχουσα έκδοση {{ver}}",
"reloading": "Γίνεται επαναφόρτωση"
},
"decky_updates": "Ενημερώσεις Decky"
},
"FilePickerIndex": {
"folder": {
"select": "Χρησιμοποιήστε αυτό το φάκελο"
}
}
}
+207
View File
@@ -0,0 +1,207 @@
{
"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"
},
"FilePickerIndex": {
"folder": {
"select": "Use this folder"
}
},
"PluginCard": {
"plugin_full_access": "This plugin has full access to your Steam Deck.",
"plugin_install": "Install",
"plugin_no_desc": "No description provided.",
"plugin_version_label": "Plugin Version"
},
"PluginInstallModal": {
"install": {
"button_idle": "Install",
"button_processing": "Installing",
"desc": "Are you sure you want to install {{artifact}} {{version}}?",
"title": "Install {{artifact}}"
},
"no_hash": "This plugin does not have a hash, you are installing it at your own risk.",
"reinstall": {
"button_idle": "Reinstall",
"button_processing": "Reinstalling",
"desc": "Are you sure you want to reinstall {{artifact}} {{version}}?",
"title": "Reinstall {{artifact}}"
},
"update": {
"button_idle": "Update",
"button_processing": "Updating",
"desc": "Are you sure you want to update {{artifact}} {{version}}?",
"title": "Update {{artifact}}"
}
},
"MultiplePluginsInstallModal": {
"title": {
"mixed_one": "Modify 1 plugin",
"mixed_other": "Modify {{count}} plugins",
"update_one": "Update 1 plugin",
"update_other": "Update {{count}} plugins",
"reinstall_one": "Reinstall 1 plugin",
"reinstall_other": "Reinstall {{count}} plugins",
"install_one": "Install 1 plugin",
"install_other": "Install {{count}} plugins"
},
"ok_button": {
"idle": "Confirm",
"loading": "Working"
},
"confirm": "Are you sure you want to make the following modifications?",
"description": {
"install": "Install {{name}} {{version}}",
"update": "Update {{name}} to {{version}}",
"reinstall": "Reinstall {{name}} {{version}}"
}
},
"PluginListIndex": {
"no_plugin": "No plugins installed!",
"plugin_actions": "Plugin Actions",
"reinstall": "Reinstall",
"reload": "Reload",
"uninstall": "Uninstall",
"update_to": "Update to {{name}}",
"update_all_one": "Update 1 plugin",
"update_all_other": "Update {{count}} plugins"
},
"PluginLoader": {
"decky_title": "Decky",
"decky_update_available": "Update to {{tag_name}} available!",
"error": "Error",
"plugin_error_uninstall": "Loading {{name}} caused an exception as shown above. This usually means that the plugin requires an update for the new version of SteamUI. Check if an update is present or evaluate its removal in the Decky settings, in the Plugins section.",
"plugin_load_error": {
"message": "Error loading plugin {{name}}",
"toast": "Error loading {{name}}"
},
"plugin_uninstall": {
"button": "Uninstall",
"desc": "Are you sure you want to uninstall {{name}}?",
"title": "Uninstall {{name}}"
},
"plugin_update_one": "Updates available for 1 plugin!",
"plugin_update_other": "Updates available for {{count}} plugins!"
},
"RemoteDebugging": {
"remote_cef": {
"desc": "Allow unauthenticated access to the CEF debugger to anyone in your network",
"label": "Allow Remote CEF Debugging"
}
},
"SettingsDeveloperIndex": {
"header": "Other",
"react_devtools": {
"desc": "Enables connection to a computer running React DevTools. Changing this setting will reload Steam. Set the IP address before enabling.",
"ip_label": "IP",
"label": "Enable React DevTools"
},
"third_party_plugins": {
"button_install": "Install",
"button_zip": "Browse",
"header": "Third-Party Plugins",
"label_desc": "URL",
"label_url": "Install Plugin from URL",
"label_zip": "Install Plugin from ZIP File"
},
"toast_zip": {
"body": "Installation failed! Only ZIP files are supported.",
"title": "Decky"
},
"valve_internal": {
"desc1": "Enables the Valve internal developer menu.",
"desc2": "Do not touch anything in this menu unless you know what it does.",
"label": "Enable Valve Internal"
}
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Decky Version",
"header": "About"
},
"beta": {
"header": "Beta participation"
},
"developer_mode": {
"desc": "Enables Decky's developer settings.",
"label": "Developer mode"
},
"other": {
"header": "Other"
},
"updates": {
"header": "Updates"
}
},
"SettingsIndex": {
"developer_title": "Developer",
"general_title": "General",
"navbar_settings": "Decky Settings",
"plugins_title": "Plugins"
},
"Store": {
"store_contrib": {
"desc": "If you would like to contribute to the Decky Plugin Store, check the SteamDeckHomebrew/decky-plugin-template repository on GitHub. Information on development and distribution is available in the README.",
"label": "Contributing"
},
"store_filter": {
"label": "Filter",
"label_def": "All"
},
"store_search": {
"label": "Search"
},
"store_sort": {
"label": "Sort",
"label_def": "Last Updated (Newest)"
},
"store_source": {
"desc": "All plugin source code is available on SteamDeckHomebrew/decky-plugin-database repository on GitHub.",
"label": "Source Code"
},
"store_tabs": {
"about": "About",
"alph_asce": "Alphabetical (Z to A)",
"alph_desc": "Alphabetical (A to Z)",
"title": "Browse"
},
"store_testing_cta": "Please consider testing new plugins to help the Decky Loader team!"
},
"StoreSelect": {
"custom_store": {
"label": "Custom Store",
"url_label": "URL"
},
"store_channel": {
"custom": "Custom",
"default": "Default",
"label": "Store Channel",
"testing": "Testing"
}
},
"Updater": {
"decky_updates": "Decky Updates",
"no_patch_notes_desc": "no patch notes for this version",
"patch_notes_desc": "Patch Notes",
"updates": {
"check_button": "Check For Updates",
"checking": "Checking",
"cur_version": "Current version: {{ver}}",
"install_button": "Install Update",
"label": "Updates",
"lat_version": "Up to date: running {{ver}}",
"reloading": "Reloading",
"updating": "Updating"
}
}
}
+113
View File
@@ -0,0 +1,113 @@
{
"SettingsDeveloperIndex": {
"third_party_plugins": {
"button_install": "Instalar",
"button_zip": "Navegar"
},
"valve_internal": {
"desc2": "No toque nada en este menú a menos que sepa lo que hace."
},
"toast_zip": {
"title": "Decky"
}
},
"PluginInstallModal": {
"install": {
"button_idle": "Instalar",
"button_processing": "Instalando"
},
"reinstall": {
"button_idle": "Reinstalar",
"button_processing": "Reinstalando"
},
"update": {
"button_processing": "Actualizando",
"button_idle": "Actualizar"
}
},
"Developer": {
"disabling": "Desactivando",
"enabling": "Activando",
"5secreload": "Recargando en 5 segundos"
},
"BranchSelect": {
"update_channel": {
"prerelease": "Prelanzamiento",
"stable": "Estable",
"label": "Canal de actualización"
}
},
"PluginCard": {
"plugin_full_access": "Este plugin tiene acceso completo a su Steam Deck.",
"plugin_install": "Instalar",
"plugin_version_label": "Versión de Plugin"
},
"FilePickerIndex": {
"folder": {
"select": "Usar esta carpeta"
}
},
"PluginListIndex": {
"uninstall": "Desinstalar",
"reinstall": "Reinstalar",
"reload": "Recargar",
"plugin_actions": "Acciónes de Plugin",
"no_plugin": "¡No hay plugins instalados!"
},
"PluginLoader": {
"error": "Error",
"plugin_uninstall": {
"button": "Desinstalar"
},
"decky_title": "Decky"
},
"RemoteDebugging": {
"remote_cef": {
"desc": "Permitir acceso no autenticado al CEF debugger a cualquier persona en su red"
}
},
"SettingsGeneralIndex": {
"updates": {
"header": "Actualizaciones"
},
"about": {
"header": "Información"
},
"developer_mode": {
"label": "Modo Desarrollador"
}
},
"SettingsIndex": {
"developer_title": "Desarrollador",
"general_title": "General",
"navbar_settings": "Ajustes de Decky",
"plugins_title": "Plugins"
},
"Store": {
"store_search": {
"label": "Buscar"
},
"store_sort": {
"label": "Ordenar"
},
"store_contrib": {
"desc": "Si desea contribuir a la Tienda de Decky Plugin, revise el repositorio SteamDeckHomebrew/decky-plugin-template en GitHub. Información acerca del desarrollo y distribución está disponible en al archivo README.",
"label": "Contribuyendo"
},
"store_tabs": {
"about": "Información",
"title": "Navegar"
},
"store_testing_cta": "¡Por favor considera probando plugins nuevos para ayudar al equipo de Decky Loader!"
},
"Updater": {
"updates": {
"reloading": "Recargando",
"updating": "Actualizando",
"checking": "Buscando",
"check_button": "Buscar Actualizaciones",
"install_button": "Instalar Actualización",
"label": "Actualizaciones"
}
}
}
+183
View File
@@ -0,0 +1,183 @@
{
"SettingsDeveloperIndex": {
"react_devtools": {
"desc": "Permet la connexion à un ordinateur exécutant React DevTools. Changer ce paramètre rechargera Steam. Définissez l'adresse IP avant l'activation.",
"ip_label": "IP",
"label": "Activer React DevTools"
},
"third_party_plugins": {
"button_install": "Installer",
"button_zip": "Parcourir",
"header": "Plugins tiers",
"label_desc": "URL",
"label_url": "Installer le plugin à partir d'un URL",
"label_zip": "Installer le plugin à partir d'un fichier ZIP"
},
"toast_zip": {
"body": "Échec de l'installation! Seuls les fichiers ZIP sont pris en charge.",
"title": "Decky"
},
"valve_internal": {
"desc1": "Active le menu développeur interne de Valve.",
"desc2": "Ne touchez à rien dans ce menu à moins que vous ne sachiez ce qu'il fait.",
"label": "Activer Valve Internal"
}
},
"BranchSelect": {
"update_channel": {
"prerelease": "Avant-première",
"label": "Canal de mise à jour",
"stable": "Stable",
"testing": "Test"
}
},
"StoreSelect": {
"store_channel": {
"label": "Canal du Plugin Store",
"testing": "Test",
"custom": "Personnalisé",
"default": "Par défaut"
},
"custom_store": {
"label": "Plugin Store personnalisé",
"url_label": "URL"
}
},
"Updater": {
"decky_updates": "Mises à jour de Decky",
"no_patch_notes_desc": "pas de notes de mise à jour pour cette version",
"patch_notes_desc": "Notes de mise à jour",
"updates": {
"check_button": "Chercher les mises à jour",
"checking": "Recherche",
"cur_version": "Version actuelle: {{ver}}",
"install_button": "Installer la mise à jour",
"label": "Mises à jour",
"lat_version": "À jour: version {{ver}}",
"reloading": "Rechargement",
"updating": "Mise à jour en cours"
}
},
"Developer": {
"5secreload": "Rechargement dans 5 secondes",
"disabling": "Désactivation",
"enabling": "Activation"
},
"FilePickerIndex": {
"folder": {
"select": "Utiliser ce dossier"
}
},
"PluginCard": {
"plugin_full_access": "Ce plugin a un accès complet à votre Steam Deck.",
"plugin_install": "Installer",
"plugin_no_desc": "Aucune description fournie.",
"plugin_version_label": "Version du plugin"
},
"PluginInstallModal": {
"install": {
"button_idle": "Installer",
"button_processing": "Installation en cours",
"title": "Installer {{artifact}}",
"desc": "Êtes-vous sûr de vouloir installer {{artifact}} {{version}}?"
},
"no_hash": "Ce plugin n'a pas de somme de contrôle, vous l'installez à vos risques et périls.",
"reinstall": {
"button_idle": "Réinstaller",
"button_processing": "Réinstallation en cours",
"desc": "Êtes-vous sûr de vouloir réinstaller {{artifact}} {{version}}?",
"title": "Réinstaller {{artifact}}"
},
"update": {
"button_idle": "Mettre à jour",
"button_processing": "Mise à jour",
"title": "Mettre à jour {{artifact}}",
"desc": "Êtes-vous sûr de vouloir mettre à jour {{artifact}} {{version}}?"
}
},
"PluginListIndex": {
"plugin_actions": "Plugin Actions",
"reinstall": "Réinstaller",
"reload": "Recharger",
"uninstall": "Désinstaller",
"update_to": "Mettre à jour vers {{name}}",
"no_plugin": "Aucun plugin installé !"
},
"PluginLoader": {
"decky_title": "Decky",
"error": "Erreur",
"plugin_error_uninstall": "Allez sur <0></0> dans le menu de Decky si vous voulez désinstaller ce plugin.",
"plugin_load_error": {
"message": "Erreur lors du chargement du plugin {{name}}",
"toast": "Erreur lors du chargement de {{name}}"
},
"decky_update_available": "Mise à jour vers {{tag_name}} disponible !",
"plugin_uninstall": {
"button": "Désinstaller",
"title": "Désinstaller {{name}}",
"desc": "Êtes-vous sûr.e de vouloir désinstaller {{name}} ?"
},
"plugin_update_one": "",
"plugin_update_many": "",
"plugin_update_other": ""
},
"RemoteDebugging": {
"remote_cef": {
"desc": "Autoriser l'accès non authentifié au débogueur CEF à toute personne de votre réseau",
"label": "Autoriser le débogage CEF à distance"
}
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Version de Decky",
"header": "À propos"
},
"beta": {
"header": "Participation à la Bêta"
},
"developer_mode": {
"desc": "Active les paramètres de développeur de Decky.",
"label": "Mode développeur"
},
"other": {
"header": "Autre"
},
"updates": {
"header": "Mises à jour"
}
},
"SettingsIndex": {
"developer_title": "Développeur",
"general_title": "Général",
"navbar_settings": "Paramètres de Decky",
"plugins_title": "Plugins"
},
"Store": {
"store_contrib": {
"desc": "Si vous souhaitez contribuer au Decky Plugin Store, consultez le dépôt SteamDeckHomebrew/decky-plugin-template sur GitHub. Des informations sur le développement et la distribution sont disponibles dans le fichier README.",
"label": "Contributions"
},
"store_filter": {
"label": "Filtrer",
"label_def": "Tous"
},
"store_search": {
"label": "Rechercher"
},
"store_sort": {
"label": "Trier",
"label_def": "Mises à jour (Plus récentes)"
},
"store_source": {
"desc": "Tout le code source des plugins est disponible sur le dépôt SteamDeckHomebrew/decky-plugin-database sur GitHub.",
"label": "Code Source"
},
"store_tabs": {
"about": "À propos",
"alph_asce": "Alphabétique (Z à A)",
"alph_desc": "Alphabétique (A à Z)",
"title": "Explorer"
},
"store_testing_cta": "Pensez à tester de nouveaux plugins pour aider l'équipe Decky Loader !"
}
}
+184
View File
@@ -0,0 +1,184 @@
{
"BranchSelect": {
"update_channel": {
"label": "Canale di aggiornamento",
"prerelease": "Prerilascio",
"stable": "Stabile",
"testing": "In prova"
}
},
"Developer": {
"5secreload": "Ricarico in 5 secondi",
"disabling": "Disabilito i tools di React",
"enabling": "Abilito i tools di React"
},
"FilePickerIndex": {
"folder": {
"select": "Usa questa cartella"
}
},
"PluginCard": {
"plugin_full_access": "Questo plugin ha accesso completo al tuo Steam Deck.",
"plugin_install": "Installa",
"plugin_no_desc": "Nessuna descrizione fornita.",
"plugin_version_label": "Versione Plugin"
},
"PluginInstallModal": {
"install": {
"button_idle": "Installa",
"button_processing": "Installando",
"desc": "Sei sicuro di voler installare {{artifact}} {{version}}?",
"title": "Installa {{artifact}}"
},
"no_hash": "Questo plugin non ha un hash associato, lo stai installando a tuo rischio e pericolo.",
"reinstall": {
"button_idle": "Reinstalla",
"button_processing": "Reinstallando",
"desc": "Sei sicuro di voler reinstallare {{artifact}} {{version}}?",
"title": "Reinstalla {{artifact}}"
},
"update": {
"button_idle": "Aggiorna",
"button_processing": "Aggiornando",
"desc": "Sei sicuro di voler aggiornare {{artifact}} {{version}}?",
"title": "Aggiorna {{artifact}}"
}
},
"PluginListIndex": {
"no_plugin": "Nessun plugin installato!",
"plugin_actions": "Operazioni sui plugins",
"reinstall": "Reinstalla",
"reload": "Ricarica",
"uninstall": "Rimuovi",
"update_to": "Aggiorna a {{name}}"
},
"PluginLoader": {
"decky_title": "Decky",
"decky_update_available": "Disponibile aggiornamento a {{tag_name}}!",
"error": "Errore",
"plugin_error_uninstall": "Il plugin {{name}} ha causato un'eccezione che è descritta sopra. Questo tipicamente significa che il plugin deve essere aggiornato per funzionare sulla nuova versione di SteamUI. Controlla se è disponibile un aggiornamento o valutane la rimozione andando nelle impostazioni di Decky nella sezione Plugins.",
"plugin_load_error": {
"message": "Errore caricando il plugin {{name}}",
"toast": "Errore caricando {{name}}"
},
"plugin_uninstall": {
"button": "Rimuovi",
"desc": "Sei sicuro di voler rimuovere {{name}}?",
"title": "Rimuovi {{name}}"
},
"plugin_update_one": "Aggiornamento disponibile per 1 plugin!",
"plugin_update_many": "Aggiornamenti disponibili per {{count}} plugins!",
"plugin_update_other": "Aggiornamenti disponibili per {{count}} plugins!"
},
"RemoteDebugging": {
"remote_cef": {
"desc": "Permetti l'accesso non autenticato al debugger di CEF da tutti gli indirizzi sulla tua rete locale",
"label": "Permetti il debug remoto di CEF"
}
},
"SettingsDeveloperIndex": {
"header": "Altro",
"react_devtools": {
"desc": "Abilita la connessione ad un computer che esegue i DevTools di React. Cambiando questa impostazione ricaricherà Steam. Imposta l'indirizzo IP prima di abilitarlo.",
"ip_label": "IP",
"label": "Abilita i DevTools di React"
},
"third_party_plugins": {
"button_install": "Installa",
"button_zip": "Seleziona",
"header": "Plugin di terze parti",
"label_desc": "URL",
"label_url": "Installa plugin da un'indirizzo web",
"label_zip": "Installa plugin da un file ZIP"
},
"toast_zip": {
"body": "Installazione non riuscita! Solo supportati solo file ZIP.",
"title": "Decky"
},
"valve_internal": {
"desc1": "Abilita il menu di sviluppo interno di Valve.",
"desc2": "Non toccare nulla in questo menu se non sai quello che fa.",
"label": "Abilita Menu Sviluppatore"
}
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Versione di Decky",
"header": "Riguardo a"
},
"beta": {
"header": "Partecipazione alla beta"
},
"developer_mode": {
"desc": "Abilità le impostazioni di sviluppo di Decky.",
"label": "Modalità sviluppatore"
},
"other": {
"header": "Altro"
},
"updates": {
"header": "Aggiornamenti"
}
},
"SettingsIndex": {
"developer_title": "Sviluppatore",
"general_title": "Generali",
"navbar_settings": "Impostazioni Decky",
"plugins_title": "Plugins"
},
"Store": {
"store_contrib": {
"desc": "Se desideri contribuire allo store di Decky, puoi trovare un template caricato su GitHub all'indirizzo SteamDeckHomebrew/decky-plugin-template. Informazioni riguardo sviluppo e distribuzione sono disponibili nel README.",
"label": "Contribuisci"
},
"store_filter": {
"label": "Filtra",
"label_def": "Tutto"
},
"store_search": {
"label": "Cerca"
},
"store_sort": {
"label": "Ordina",
"label_def": "Ultimo aggiornato (Più recente)"
},
"store_source": {
"desc": "Tutto il codice sorgente dei plugin è disponibile su GitHub all'indirizzo SteamDeckHomebrew/decky-plugin-database.",
"label": "Codice Sorgente"
},
"store_tabs": {
"about": "Riguardo a",
"alph_asce": "Alfabetico (Z a A)",
"alph_desc": "Alfabetico (A a Z)",
"title": "Sfoglia"
},
"store_testing_cta": "Valuta la possibilità di testare nuovi plugin per aiutare il team di Decky Loader!"
},
"StoreSelect": {
"custom_store": {
"label": "Negozio custom",
"url_label": "URL"
},
"store_channel": {
"custom": "Personalizzato",
"default": "Default",
"label": "Canale del negozio",
"testing": "In prova"
}
},
"Updater": {
"decky_updates": "Aggiornamento di Decky",
"no_patch_notes_desc": "nessuna patch notes per questa versione",
"patch_notes_desc": "Cambiamenti",
"updates": {
"check_button": "Cerca aggiornamenti",
"checking": "Controllando",
"cur_version": "Versione attuale: {{ver}}",
"install_button": "Installa aggiornamento",
"label": "Aggiornamenti",
"lat_version": "Aggiornato. Eseguendo {{ver}}",
"reloading": "Ricaricando",
"updating": "Aggiornando"
}
}
}
+115
View File
@@ -0,0 +1,115 @@
{
"SettingsDeveloperIndex": {
"react_devtools": {
"ip_label": "IP",
"label": "Aktivizo React DevTools"
},
"third_party_plugins": {
"button_zip": "Kërko",
"header": "Shtesa të Huaj",
"button_install": "Instalo",
"label_desc": "URL",
"label_url": "Instalo Shtes Nga URL",
"label_zip": "Instalo Shtes Nga ZIP"
},
"toast_zip": {
"title": "Decky"
}
},
"BranchSelect": {
"update_channel": {
"stable": "Fiksuar",
"label": "Kanali Përditësimet"
}
},
"FilePickerIndex": {
"folder": {
"select": "Përdore këtë folder"
}
},
"PluginCard": {
"plugin_install": "Instalo",
"plugin_version_label": "Versioni Shteses"
},
"PluginInstallModal": {
"install": {
"button_idle": "Instalo",
"button_processing": "Instalohet",
"desc": "Je i sigurt që don ta instalojsh {{artifact}} {{version}}?",
"title": "Instalo {{artifact}}"
},
"no_hash": "Ky shtesë nuk ka hash, ti e instalon me rrezikun tuaj.",
"reinstall": {
"button_idle": "Riinstalo",
"button_processing": "Riinstalohet",
"desc": "Je i sigurt a don ta riinstalojsh {{artifact}} {{version}}?",
"title": "Riinstalo {{artifact}}"
},
"update": {
"button_processing": "Përditësohet",
"desc": "Je i sigurt a don ta përditësojsh {{artifact}} {{version}}?",
"title": "Përditëso {{artifact}}"
}
},
"PluginLoader": {
"decky_title": "Decky",
"plugin_uninstall": {
"title": "Çinstalo {{name}}",
"button": "Çinstalo",
"desc": "Je i sigurt që don ta çinstalojsh {{name}}?"
},
"error": "Gabim",
"plugin_error_uninstall": "Ju lutem shko nga <0></0> në Decky menu nëse don ta çinstalojsh këtë shtese."
},
"PluginListIndex": {
"no_plugin": "Nuk ka shtesa të instaluar!",
"uninstall": "Çinstalo"
},
"SettingsGeneralIndex": {
"other": {
"header": "Të Tjera"
},
"about": {
"decky_version": "Versioni Decky"
},
"updates": {
"header": "Përmirësimet"
}
},
"SettingsIndex": {
"developer_title": "Zhvillues",
"general_title": "Gjeneral",
"navbar_settings": "Cilësimet Decky"
},
"Store": {
"store_sort": {
"label": "Rendit"
},
"store_tabs": {
"title": "Kërko"
},
"store_contrib": {
"label": "Kontributi"
},
"store_filter": {
"label": "Filtro",
"label_def": "Të Gjitha"
},
"store_search": {
"label": "Kërko"
},
"store_source": {
"label": "Kodin Burimor"
}
},
"StoreSelect": {
"store_channel": {
"label": "Kanali Dyqanit"
}
},
"Updater": {
"updates": {
"cur_version": "Versioni e tanishëme: {{ver}}"
}
}
}
+181
View File
@@ -0,0 +1,181 @@
{
"BranchSelect": {
"update_channel": {
"prerelease": "发布候选",
"stable": "稳定",
"testing": "测试",
"label": "更新通道"
}
},
"Developer": {
"5secreload": "5 秒钟后重新加载",
"disabling": "正在禁用",
"enabling": "正在启用"
},
"FilePickerIndex": {
"folder": {
"select": "使用这个文件夹"
}
},
"PluginCard": {
"plugin_install": "安装",
"plugin_no_desc": "无描述提供。",
"plugin_version_label": "插件版本",
"plugin_full_access": "此插件可以完全访问你的 Steam Deck"
},
"PluginInstallModal": {
"install": {
"button_idle": "安装",
"button_processing": "安装中",
"desc": "你确定要安装 {{artifact}} {{version}} 吗?",
"title": "安装 {{artifact}}"
},
"reinstall": {
"button_idle": "重新安装",
"button_processing": "正在重新安装",
"desc": "你确定要重新安装 {{artifact}} {{version}} 吗?",
"title": "重新安装 {{artifact}}"
},
"update": {
"button_idle": "更新",
"button_processing": "正在更新",
"desc": "你确定要更新 {{artifact}} {{version}} 吗?",
"title": "更新 {{artifact}}"
},
"no_hash": "此插件没有哈希校验值,你需要自行承担安装风险"
},
"PluginListIndex": {
"no_plugin": "没有安装插件!",
"plugin_actions": "插件操作",
"reinstall": "重新安装",
"reload": "重新加载",
"uninstall": "卸载",
"update_to": "更新 {{name}}"
},
"PluginLoader": {
"decky_title": "Decky",
"error": "错误",
"plugin_error_uninstall": "如果你想卸载插件请点击 Decky 菜单中的 <0></0> 图标",
"plugin_load_error": {
"message": "加载插件 {{name}} 错误",
"toast": "加载插件 {{name}} 发生了错误"
},
"plugin_uninstall": {
"button": "卸载",
"title": "卸载 {{name}}",
"desc": "你确定要卸载 {{name}} 吗?"
},
"decky_update_available": "新版本 {{tag_name}} 可用!",
"plugin_update_other": "{{count}} 个插件有更新!"
},
"RemoteDebugging": {
"remote_cef": {
"desc": "允许你网络中的任何人无需身份验证即可访问CEF调试器",
"label": "允许远程访问CEF调试"
}
},
"SettingsDeveloperIndex": {
"react_devtools": {
"ip_label": "IP",
"label": "启用 React DevTools",
"desc": "允许连接到运行着 React DevTools 的计算机,更改此设置将重新加载Steam,请在启用前设置IP地址"
},
"third_party_plugins": {
"button_install": "安装",
"button_zip": "浏览文件",
"header": "第三方插件",
"label_desc": "URL",
"label_url": "从 URL 安装插件",
"label_zip": "从 ZIP 压缩文件安装插件"
},
"toast_zip": {
"title": "Decky",
"body": "安装失败!只有 ZIP 格式的插件被支持"
},
"valve_internal": {
"desc1": "启用 Valve 内部开发者菜单",
"desc2": "除非你知道你在干什么,否则请不要修改此菜单中的任何内容",
"label": "启用 Valve 内部开发者"
}
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Decky 版本",
"header": "关于"
},
"beta": {
"header": "参与测试"
},
"developer_mode": {
"label": "开发者模式",
"desc": "启用 Decky 的开发者测试"
},
"other": {
"header": "其他"
},
"updates": {
"header": "更新"
}
},
"SettingsIndex": {
"developer_title": "开发者",
"general_title": "通用",
"navbar_settings": "Decky 设置",
"plugins_title": "插件"
},
"Store": {
"store_contrib": {
"label": "贡献",
"desc": "如果你想要提交你的插件到 Decky 插件商店,请访问 GitHub 上的 SteamDeckHomebrew/decky-plugin-template 存储库,关于开发和分发的相关信息,请查看 README 文件"
},
"store_filter": {
"label": "过滤器",
"label_def": "全部"
},
"store_search": {
"label": "搜索"
},
"store_sort": {
"label": "排序",
"label_def": "最后更新 (最新)"
},
"store_source": {
"label": "源代码",
"desc": "所有插件的源代码都可以在 GitHub 上的 SteamDeckHomebrew/decky-plugin-database 存储库中获得"
},
"store_tabs": {
"about": "关于",
"alph_asce": "字母排序 (Z 到 A)",
"alph_desc": "字母排序 (A 到 Z)",
"title": "浏览"
},
"store_testing_cta": "请考虑测试新插件以帮助 Decky Loader 团队!"
},
"StoreSelect": {
"store_channel": {
"default": "默认",
"label": "商店通道",
"testing": "测试",
"custom": "自定义"
},
"custom_store": {
"label": "自定义商店",
"url_label": "URL"
}
},
"Updater": {
"decky_updates": "Decky 更新",
"no_patch_notes_desc": "此版本没有补丁说明",
"patch_notes_desc": "补丁说明",
"updates": {
"check_button": "检查更新",
"checking": "检查中",
"cur_version": "当前版本: {{ver}}",
"install_button": "安装更新",
"label": "更新",
"lat_version": "已是最新版本: {{ver}} 运行中",
"reloading": "重新加载中",
"updating": "更新中"
}
}
}
+180
View File
@@ -0,0 +1,180 @@
{
"BranchSelect": {
"update_channel": {
"testing": "測試版",
"label": "更新頻道",
"prerelease": "預發佈",
"stable": "穩定版"
}
},
"Developer": {
"5secreload": "5 秒後重新載入",
"disabling": "正在停用",
"enabling": "正在啟用"
},
"FilePickerIndex": {
"folder": {
"select": "使用此資料夾"
}
},
"PluginCard": {
"plugin_install": "安裝",
"plugin_no_desc": "未提示描述。",
"plugin_version_label": "外掛程式版本",
"plugin_full_access": "此外掛程式擁有您的 Steam Deck 的完整存取權。"
},
"PluginInstallModal": {
"install": {
"button_idle": "安裝",
"button_processing": "正在安裝",
"title": "安裝 {{artifact}}",
"desc": "您確定要安裝 {{artifact}} {{version}} 嗎?"
},
"reinstall": {
"button_idle": "重新安裝",
"button_processing": "正在重新安裝",
"desc": "您確定要重新安裝 {{artifact}} {{version}} 嗎?",
"title": "重新安裝 {{artifact}}"
},
"update": {
"button_idle": "更新",
"button_processing": "正在更新",
"desc": "您確定要更新 {{artifact}} {{version}} 嗎?",
"title": "更新 {{artifact}}"
},
"no_hash": "此外掛程式沒有提供 hash 驗證,安裝可能有風險。"
},
"PluginListIndex": {
"no_plugin": "未安裝外掛程式!",
"plugin_actions": "外掛程式操作",
"uninstall": "解除安裝",
"update_to": "更新到 {{name}}",
"reinstall": "重新安裝",
"reload": "重新載入"
},
"PluginLoader": {
"decky_title": "Decky",
"error": "錯誤",
"plugin_error_uninstall": "如果您要解除安裝外掛程式,請在 Decky 選單,按下 <0></0>。",
"plugin_load_error": {
"message": "載入外掛程式 {{name}} 發生錯誤",
"toast": "{{name}} 載入出錯"
},
"plugin_uninstall": {
"button": "解除安裝",
"title": "解除安裝 {{name}}",
"desc": "您確定要解除安裝 {{name}} 嗎?"
},
"decky_update_available": "可更新至版本 {{tag_name}}"
},
"RemoteDebugging": {
"remote_cef": {
"desc": "允許您的網路中的任何人未經認證地存取 CEF 偵錯器",
"label": "允許 CEF 遠端偵錯"
}
},
"SettingsDeveloperIndex": {
"third_party_plugins": {
"button_zip": "開啟",
"label_desc": "網址",
"label_url": "從網址安裝外掛程式",
"label_zip": "從 ZIP 檔案安裝外掛程式",
"button_install": "安裝",
"header": "第三方外掛程式"
},
"toast_zip": {
"body": "安裝失敗!只支援 ZIP 檔案。",
"title": "Decky"
},
"valve_internal": {
"desc2": "除非您知道它的作用,否則不要碰這個選單中的任何東西。",
"desc1": "啟用 Valve 內建開發人員選單。",
"label": "啟用 Valve 內建"
},
"react_devtools": {
"desc": "啟用與執行 React DevTools 的電腦的連接。改變這個設定將重新載入 Steam。啟用前必須設定 IP 位址。",
"ip_label": "IP",
"label": "啟用 React DevTools"
}
},
"SettingsGeneralIndex": {
"about": {
"header": "關於",
"decky_version": "Decky 版本"
},
"beta": {
"header": "參與測試"
},
"developer_mode": {
"label": "開發人員模式",
"desc": "啟用 Decky 的開發人員模式。"
},
"other": {
"header": "其他"
},
"updates": {
"header": "更新"
}
},
"SettingsIndex": {
"developer_title": "開發人員",
"general_title": "一般",
"navbar_settings": "Decky 設定",
"plugins_title": "外掛程式"
},
"Store": {
"store_contrib": {
"label": "貢獻",
"desc": "如果您想為 Decky 外掛程式商店做貢獻,請查看 GitHub 上的 SteamDeckHomebrew/decky-plugin-template 儲存庫。README 中提供了有關開發和發佈的資訊。"
},
"store_filter": {
"label": "過濾",
"label_def": "全部"
},
"store_search": {
"label": "搜尋"
},
"store_sort": {
"label": "排序",
"label_def": "最後更新 (最新)"
},
"store_source": {
"label": "原始碼",
"desc": "所有外掛程式原始碼可以在 GitHub 的 SteamDeckHomebrew/decky-plugin-database 儲存庫查看。"
},
"store_tabs": {
"about": "關於",
"alph_asce": "依字母排序 (Z 到 A)",
"alph_desc": "依字母排序 (A 到 Z)",
"title": "瀏覽"
},
"store_testing_cta": "請考慮測試新的外掛程式來幫助 Decky Loader 團隊!"
},
"StoreSelect": {
"custom_store": {
"label": "自訂商店",
"url_label": "網址"
},
"store_channel": {
"custom": "自訂",
"default": "預設",
"label": "商店頻道",
"testing": "測試"
}
},
"Updater": {
"decky_updates": "Decky 更新",
"no_patch_notes_desc": "這個版本沒有更新日誌",
"patch_notes_desc": "更新日誌",
"updates": {
"checking": "正在檢查",
"install_button": "安裝更新",
"label": "更新",
"lat_version": "已是最新:執行 {{ver}}",
"reloading": "正在重新載入",
"check_button": "檢查更新",
"cur_version": "目前版本:{{ver}}",
"updating": "正在更新"
}
}
}
+43
View File
@@ -0,0 +1,43 @@
import platform, os
ON_WINDOWS = platform.system() == "Windows"
ON_LINUX = not ON_WINDOWS
if ON_WINDOWS:
from localplatformwin import *
import localplatformwin as localplatform
else:
from localplatformlinux import *
import localplatformlinux as localplatform
def get_privileged_path() -> str:
'''Get path accessible by elevated user. Holds plugins, decky loader and decky loader configs'''
return localplatform.get_privileged_path()
def get_unprivileged_path() -> str:
'''Get path accessible by non-elevated user. Holds plugin configuration, plugin data and plugin logs. Externally referred to as the 'Homebrew' directory'''
return localplatform.get_unprivileged_path()
def get_unprivileged_user() -> str:
'''Get user that should own files made in unprivileged path'''
return localplatform.get_unprivileged_user()
def get_chown_plugin_path() -> bool:
return os.getenv("CHOWN_PLUGIN_PATH", "1") == "1"
def get_server_host() -> str:
return os.getenv("SERVER_HOST", "127.0.0.1")
def get_server_port() -> int:
return int(os.getenv("SERVER_PORT", "1337"))
def get_live_reload() -> bool:
return os.getenv("LIVE_RELOAD", "1") == "1"
def get_keep_systemd_service() -> bool:
return os.getenv("KEEP_SYSTEMD_SERVICE", "0") == "1"
def get_log_level() -> int:
return {"CRITICAL": 50, "ERROR": 40, "WARNING": 30, "INFO": 20, "DEBUG": 10}[
os.getenv("LOG_LEVEL", "INFO")
]
+194
View File
@@ -0,0 +1,194 @@
import os, pwd, grp, sys, logging
from subprocess import call, run, DEVNULL, PIPE, STDOUT
from customtypes import UserType
logger = logging.getLogger("localplatform")
# Get the user id hosting the plugin loader
def _get_user_id() -> int:
return pwd.getpwnam(_get_user()).pw_uid
# Get the user hosting the plugin loader
def _get_user() -> str:
return get_unprivileged_user()
# Get the effective user id of the running process
def _get_effective_user_id() -> int:
return os.geteuid()
# Get the effective user of the running process
def _get_effective_user() -> str:
return pwd.getpwuid(_get_effective_user_id()).pw_name
# Get the effective user group id of the running process
def _get_effective_user_group_id() -> int:
return os.getegid()
# Get the effective user group of the running process
def _get_effective_user_group() -> str:
return grp.getgrgid(_get_effective_user_group_id()).gr_name
# Get the user owner of the given file path.
def _get_user_owner(file_path) -> str:
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 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 = ""
if user == UserType.HOST_USER:
user_str = _get_user()+":"+_get_user_group()
elif user == UserType.EFFECTIVE_USER:
user_str = _get_effective_user()+":"+_get_effective_user_group()
elif user == UserType.ROOT:
user_str = "root:root"
else:
raise Exception("Unknown User Type")
result = call(["chown", "-R", user_str, path] if recursive else ["chown", user_str, path])
return result == 0
def chmod(path : str, permissions : int, recursive : bool = True) -> bool:
result = call(["chmod", "-R", str(permissions), path] if recursive else ["chmod", str(permissions), path])
return result == 0
def folder_owner(path : str) -> UserType|None:
user_owner = _get_user_owner(path)
if (user_owner == _get_user()):
return UserType.HOST_USER
elif (user_owner == _get_effective_user()):
return UserType.EFFECTIVE_USER
else:
return None
def get_home_path(user : UserType = UserType.HOST_USER) -> str:
user_name = "root"
if user == UserType.HOST_USER:
user_name = _get_user()
elif user == UserType.EFFECTIVE_USER:
user_name = _get_effective_user()
elif user == UserType.ROOT:
pass
else:
raise Exception("Unknown User Type")
return pwd.getpwnam(user_name).pw_dir
def get_username() -> str:
return _get_user()
def setgid(user : UserType = UserType.HOST_USER):
user_id = 0
if user == UserType.HOST_USER:
user_id = _get_user_group_id()
elif user == UserType.ROOT:
pass
else:
raise Exception("Unknown user type")
os.setgid(user_id)
def setuid(user : UserType = UserType.HOST_USER):
user_id = 0
if user == UserType.HOST_USER:
user_id = _get_user_id()
elif user == UserType.ROOT:
pass
else:
raise Exception("Unknown user type")
os.setuid(user_id)
async def service_active(service_name : str) -> bool:
res = run(["systemctl", "is-active", service_name], stdout=DEVNULL, stderr=DEVNULL)
return res.returncode == 0
async def service_restart(service_name : str) -> bool:
call(["systemctl", "daemon-reload"])
cmd = ["systemctl", "restart", service_name]
res = run(cmd, stdout=PIPE, stderr=STDOUT)
return res.returncode == 0
async def service_stop(service_name : str) -> bool:
cmd = ["systemctl", "stop", service_name]
res = run(cmd, stdout=PIPE, stderr=STDOUT)
return res.returncode == 0
async def service_start(service_name : str) -> bool:
cmd = ["systemctl", "start", service_name]
res = run(cmd, stdout=PIPE, stderr=STDOUT)
return res.returncode == 0
def get_privileged_path() -> str:
path = os.getenv("PRIVILEGED_PATH")
if path == None:
path = get_unprivileged_path()
return path
def _parent_dir(path : str) -> str:
if path == None:
return None
if path.endswith('/'):
path = path[:-1]
return os.path.dirname(path)
def get_unprivileged_path() -> str:
path = os.getenv("UNPRIVILEGED_PATH")
if path == None:
path = _parent_dir(os.getenv("PLUGIN_PATH"))
if path == None:
logger.debug("Unprivileged path is not properly configured. Making something up!")
# Expected path of loader binary is /home/deck/homebrew/service/PluginLoader
path = _parent_dir(_parent_dir(os.path.realpath(sys.argv[0])))
if not os.path.exists(path):
path = None
if path == None:
logger.warn("Unprivileged path is not properly configured. Defaulting to /home/deck/homebrew")
path = "/home/deck/homebrew" # We give up
return path
def get_unprivileged_user() -> str:
user = os.getenv("UNPRIVILEGED_USER")
if user == None:
# Lets hope we can extract it from the unprivileged dir
dir = os.path.realpath(get_unprivileged_path())
pws = sorted(pwd.getpwall(), reverse=True, key=lambda pw: len(pw.pw_dir))
for pw in pws:
if dir.startswith(os.path.realpath(pw.pw_dir)):
user = pw.pw_name
break
if user == None:
logger.warn("Unprivileged user is not properly configured. Defaulting to 'deck'")
user = 'deck'
return user
+53
View File
@@ -0,0 +1,53 @@
from customtypes import UserType
import os, sys
def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool = True) -> bool:
return True # Stubbed
def chmod(path : str, permissions : int, recursive : bool = True) -> bool:
return True # Stubbed
def folder_owner(path : str) -> UserType|None:
return UserType.HOST_USER # Stubbed
def get_home_path(user : UserType = UserType.HOST_USER) -> str:
return os.path.expanduser("~") # Mostly stubbed
def setgid(user : UserType = UserType.HOST_USER):
pass # Stubbed
def setuid(user : UserType = UserType.HOST_USER):
pass # Stubbed
async def service_active(service_name : str) -> bool:
return True # Stubbed
async def service_stop(service_name : str) -> bool:
return True # Stubbed
async def service_start(service_name : str) -> bool:
return True # Stubbed
async def service_restart(service_name : str) -> bool:
if service_name == "plugin_loader":
sys.exit(42)
return True # Stubbed
def get_username() -> str:
return os.getlogin()
def get_privileged_path() -> str:
'''On windows, privileged_path is equal to unprivileged_path'''
return get_unprivileged_path()
def get_unprivileged_path() -> str:
path = os.getenv("UNPRIVILEGED_PATH")
if path == None:
path = os.getenv("PRIVILEGED_PATH", os.path.join(os.path.expanduser("~"), "homebrew"))
return path
def get_unprivileged_user() -> str:
return os.getenv("UNPRIVILEGED_USER", os.getlogin())
+132
View File
@@ -0,0 +1,132 @@
import asyncio, time, random
from localplatform import ON_WINDOWS
BUFFER_LIMIT = 2 ** 20 # 1 MiB
class UnixSocket:
def __init__(self, on_new_message):
'''
on_new_message takes 1 string argument.
It's return value gets used, if not None, to write data to the socket.
Method should be async
'''
self.socket_addr = f"/tmp/plugin_socket_{time.time()}"
self.on_new_message = on_new_message
self.socket = None
self.reader = None
self.writer = None
async def setup_server(self):
self.socket = await asyncio.start_unix_server(self._listen_for_method_call, path=self.socket_addr, limit=BUFFER_LIMIT)
async def _open_socket_if_not_exists(self):
if not self.reader:
retries = 0
while retries < 10:
try:
self.reader, self.writer = await asyncio.open_unix_connection(self.socket_addr, limit=BUFFER_LIMIT)
return True
except:
await asyncio.sleep(2)
retries += 1
return False
else:
return True
async def get_socket_connection(self):
if not await self._open_socket_if_not_exists():
return None, None
return self.reader, self.writer
async def close_socket_connection(self):
if self.writer != None:
self.writer.close()
self.reader = None
async def read_single_line(self) -> str|None:
reader, writer = await self.get_socket_connection()
if self.reader == None:
return None
return await self._read_single_line(reader)
async def write_single_line(self, message : str):
reader, writer = await self.get_socket_connection()
if self.writer == None:
return;
await self._write_single_line(writer, message)
async def _read_single_line(self, reader) -> str:
line = bytearray()
while True:
try:
line.extend(await reader.readuntil())
except asyncio.LimitOverrunError:
line.extend(await reader.read(reader._limit))
continue
except asyncio.IncompleteReadError as err:
line.extend(err.partial)
break
else:
break
return line.decode("utf-8")
async def _write_single_line(self, writer, message : str):
if not message.endswith("\n"):
message += "\n"
writer.write(message.encode("utf-8"))
await writer.drain()
async def _listen_for_method_call(self, reader, writer):
while True:
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)
class PortSocket (UnixSocket):
def __init__(self, on_new_message):
'''
on_new_message takes 1 string argument.
It's return value gets used, if not None, to write data to the socket.
Method should be async
'''
super().__init__(on_new_message)
self.host = "127.0.0.1"
self.port = random.sample(range(40000, 60000), 1)[0]
async def setup_server(self):
self.socket = await asyncio.start_server(self._listen_for_method_call, host=self.host, port=self.port, limit=BUFFER_LIMIT)
async def _open_socket_if_not_exists(self):
if not self.reader:
retries = 0
while retries < 10:
try:
self.reader, self.writer = await asyncio.open_connection(host=self.host, port=self.port, limit=BUFFER_LIMIT)
return True
except:
await asyncio.sleep(2)
retries += 1
return False
else:
return True
if ON_WINDOWS:
class LocalSocket (PortSocket):
pass
else:
class LocalSocket (UnixSocket):
pass
+190
View File
@@ -0,0 +1,190 @@
# 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)
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()
+156
View File
@@ -0,0 +1,156 @@
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(environ["DECKY_PLUGIN_SETTINGS_DIR"])
environ["DECKY_PLUGIN_RUNTIME_DIR"] = path.join(environ["DECKY_HOME"], "data", self.plugin_directory)
helpers.mkdir_as_user(environ["DECKY_PLUGIN_RUNTIME_DIR"])
environ["DECKY_PLUGIN_LOG_DIR"] = path.join(environ["DECKY_HOME"], "logs", self.plugin_directory)
helpers.mkdir_as_user(environ["DECKY_PLUGIN_LOG_DIR"])
environ["DECKY_PLUGIN_DIR"] = path.join(self.plugin_path, self.plugin_directory)
environ["DECKY_PLUGIN_NAME"] = self.name
environ["DECKY_PLUGIN_VERSION"] = self.version
environ["DECKY_PLUGIN_AUTHOR"] = self.author
# 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"]
+59
View File
@@ -0,0 +1,59 @@
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 helpers import get_homebrew_path
class SettingsManager:
def __init__(self, name, settings_directory = None) -> None:
wrong_dir = get_homebrew_path()
if settings_directory == None:
settings_directory = path.join(wrong_dir, "settings")
self.path = path.join(settings_directory, name + ".json")
#Create the folder with the correct permission
if not path.exists(settings_directory):
mkdir(settings_directory)
#Copy all old settings file in the root directory to the correct folder
for file in listdir(wrong_dir):
if file.endswith(".json"):
rename(path.join(wrong_dir,file),
path.join(settings_directory, file))
self.path = path.join(settings_directory, name + ".json")
#If the owner of the settings directory is not the user, then set it as the user:
expected_user = UserType.HOST_USER if get_chown_plugin_path() else UserType.ROOT
if folder_owner(settings_directory) != expected_user:
chown(settings_directory, expected_user, False)
self.settings = {}
try:
open(self.path, "x", encoding="utf-8")
except FileExistsError as e:
self.read()
pass
def read(self):
try:
with open(self.path, "r", encoding="utf-8") as file:
self.settings = load(file)
except Exception as e:
print(e)
pass
def commit(self):
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):
return self.settings.get(key, default)
def setSetting(self, key, value):
self.settings[key] = value
self.commit()
+216
View File
@@ -0,0 +1,216 @@
import os
import shutil
import uuid
from asyncio import sleep
from ensurepip import version
from json.decoder import JSONDecodeError
from logging import getLogger
from os import getcwd, path, remove
from localplatform import chmod, service_restart, ON_LINUX, get_keep_systemd_service
from aiohttp import ClientSession, web
import helpers
from injector import get_gamepadui_tab, inject_to_tab
from settings import SettingsManager
logger = getLogger("Updater")
class Updater:
def __init__(self, context) -> None:
self.context = context
self.settings = self.context.settings
# Exposes updater methods to frontend
self.updater_methods = {
"get_branch": self._get_branch,
"get_version": self.get_version,
"do_update": self.do_update,
"do_restart": self.do_restart,
"check_for_updates": self.check_for_updates
}
self.remoteVer = None
self.allRemoteVers = None
self.localVer = helpers.get_loader_version()
try:
self.currentBranch = self.get_branch(self.context.settings)
except:
self.currentBranch = 0
logger.error("Current branch could not be determined, defaulting to \"Stable\"")
if context:
context.web_app.add_routes([
web.post("/updater/{method_name}", self._handle_server_method_call)
])
context.loop.create_task(self.version_reloader())
async def _handle_server_method_call(self, request):
method_name = request.match_info["method_name"]
try:
args = await request.json()
except JSONDecodeError:
args = {}
res = {}
try:
r = await self.updater_methods[method_name](**args)
res["result"] = r
res["success"] = True
except Exception as e:
res["result"] = str(e)
res["success"] = False
return web.json_response(res)
def get_branch(self, manager: SettingsManager):
ver = manager.getSetting("branch", -1)
logger.debug("current branch: %i" % ver)
if ver == -1:
logger.info("Current branch is not set, determining branch from version...")
if self.localVer.startswith("v") and "-pre" in self.localVer:
logger.info("Current version determined to be pre-release")
return 1
else:
logger.info("Current version determined to be stable")
return 0
return ver
async def _get_branch(self, manager: SettingsManager):
return self.get_branch(manager)
# retrieve relevant service file's url for each branch
def get_service_url(self):
logger.debug("Getting service URL")
branch = self.get_branch(self.context.settings)
match branch:
case 0:
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-release.service"
case 1 | 2:
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service"
case _:
logger.error("You have an invalid branch set... Defaulting to prerelease service, please send the logs to the devs!")
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service"
return str(url)
async def get_version(self):
return {
"current": self.localVer,
"remote": self.remoteVer,
"all": self.allRemoteVers,
"updatable": self.localVer != "unknown"
}
async def check_for_updates(self):
logger.debug("checking for updates")
selectedBranch = self.get_branch(self.context.settings)
async with ClientSession() as web:
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases", ssl=helpers.get_ssl_context()) as res:
remoteVersions = await res.json()
if selectedBranch == 0:
logger.debug("release type: release")
remoteVersions = list(filter(lambda ver: ver["tag_name"].startswith("v") and not ver["prerelease"] and not ver["tag_name"].find("-pre") > 0 and ver["tag_name"], remoteVersions))
elif selectedBranch == 1:
logger.debug("release type: pre-release")
remoteVersions = list(filter(lambda ver:ver["tag_name"].startswith("v"), remoteVersions))
else:
logger.error("release type: NOT FOUND")
raise ValueError("no valid branch found")
self.allRemoteVers = remoteVersions
logger.debug("determining release type to find, branch is %i" % selectedBranch)
if selectedBranch == 0:
logger.debug("release type: release")
self.remoteVer = next(filter(lambda ver: ver["tag_name"].startswith("v") and not ver["prerelease"] and not ver["tag_name"].find("-pre") > 0 and ver["tag_name"], remoteVersions), None)
elif selectedBranch == 1:
logger.debug("release type: pre-release")
self.remoteVer = next(filter(lambda ver:ver["tag_name"].startswith("v"), remoteVersions), None)
else:
logger.error("release type: NOT FOUND")
raise ValueError("no valid branch found")
logger.info("Updated remote version information")
tab = await get_gamepadui_tab()
await tab.evaluate_js(f"window.DeckyPluginLoader.notifyUpdates()", False, True, False)
return await self.get_version()
async def version_reloader(self):
await sleep(30)
while True:
try:
await self.check_for_updates()
except:
pass
await sleep(60 * 60 * 6) # 6 hours
async def do_update(self):
logger.debug("Starting update.")
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:
download_url = x["browser_download_url"]
break
if download_url == None:
raise Exception("Download url not found")
service_url = self.get_service_url()
logger.debug("Retrieved service URL")
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")
# download the relevant systemd service depending upon branch
async with web.request("GET", service_url, ssl=helpers.get_ssl_context(), allow_redirects=True) as res:
logger.debug("Downloading service file")
data = await res.content.read()
logger.debug(str(data))
service_file_path = path.join(getcwd(), "plugin_loader.service")
try:
with open(path.join(getcwd(), "plugin_loader.service"), "wb") as out:
out.write(data)
except Exception as e:
logger.error(f"Error at %s", exc_info=e)
with open(path.join(getcwd(), "plugin_loader.service"), "r", encoding="utf-8") as service_file:
service_data = service_file.read()
service_data = service_data.replace("${HOMEBREW_FOLDER}", helpers.get_homebrew_path())
with open(path.join(getcwd(), "plugin_loader.service"), "w", encoding="utf-8") as service_file:
service_file.write(service_data)
logger.debug("Saved service file")
logger.debug("Copying service file over current file.")
shutil.copy(service_file_path, "/etc/systemd/system/plugin_loader.service")
if not os.path.exists(path.join(getcwd(), ".systemd")):
os.mkdir(path.join(getcwd(), ".systemd"))
shutil.move(service_file_path, path.join(getcwd(), ".systemd")+"/plugin_loader.service")
logger.debug("Downloading binary")
async with web.request("GET", download_url, ssl=helpers.get_ssl_context(), allow_redirects=True) as res:
total = int(res.headers.get('content-length', 0))
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()
async def do_restart(self):
await service_restart("plugin_loader")
+289
View File
@@ -0,0 +1,289 @@
import uuid
import os
from json.decoder import JSONDecodeError
from traceback import format_exc
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
import helpers
import subprocess
from localplatform import service_stop, service_start
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
}
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, include_files=True):
# def sorter(file): # Modification time
# if os.path.isdir(os.path.join(path, file)) or os.path.isfile(os.path.join(path, file)):
# return os.path.getmtime(os.path.join(path, file))
# return 0
# file_names = sorted(os.listdir(path), key=sorter, reverse=True) # TODO provide more sort options
file_names = sorted(os.listdir(path)) # Alphabetical
files = []
for file in file_names:
full_path = os.path.join(path, file)
is_dir = os.path.isdir(full_path)
if is_dir or include_files:
files.append({
"isdir": is_dir,
"name": file,
"realpath": os.path.realpath(full_path)
})
return {
"realpath": os.path.realpath(path),
"files": files
}
# Based on https://stackoverflow.com/a/46422554/13174603
def start_rdt_proxy(self, ip, port):
async def pipe(reader, writer):
try:
while not reader.at_eof():
writer.write(await reader.read(2048))
finally:
writer.close()
async def handle_client(local_reader, local_writer):
try:
remote_reader, remote_writer = await open_connection(
ip, port)
pipe1 = pipe(local_reader, remote_writer)
pipe2 = pipe(remote_reader, local_writer)
await gather(pipe1, pipe2)
finally:
local_writer.close()
self.rdt_proxy_server = start_server(handle_client, "127.0.0.1", port)
self.rdt_proxy_task = self.context.loop.create_task(self.rdt_proxy_server)
def stop_rdt_proxy(self):
if self.rdt_proxy_server:
self.rdt_proxy_server.close()
self.rdt_proxy_task.cancel()
async def _enable_rdt(self):
# TODO un-hardcode port
try:
self.stop_rdt_proxy()
ip = self.context.settings.getSetting("developer.rdt.ip", None)
if ip != 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")
+335
View File
@@ -0,0 +1,335 @@
#!/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
@@ -0,0 +1,168 @@
#!/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"
-50
View File
@@ -1,50 +0,0 @@
#!/bin/sh
[ "$UID" -eq 0 ] || exec sudo "$0" "$@"
echo "Installing Steam Deck Plugin Loader nightly..."
HOMEBREW_FOLDER=/home/deck/homebrew
# Create folder structure
rm -rf ${HOMEBREW_FOLDER}/services
sudo -u deck mkdir -p ${HOMEBREW_FOLDER}/services
sudo -u deck mkdir -p ${HOMEBREW_FOLDER}/plugins
# Download latest nightly build and install it
rm -rf /tmp/plugin_loader
mkdir -p /tmp/plugin_loader
curl -L https://nightly.link/SteamDeckHomebrew/PluginLoader/workflows/build/main/Plugin%20Loader.zip --output /tmp/plugin_loader/PluginLoader.zip
unzip /tmp/plugin_loader/PluginLoader.zip -d /tmp/plugin_loader
cp /tmp/plugin_loader/PluginLoader ${HOMEBREW_FOLDER}/services/PluginLoader
rm -rf /tmp/plugin_loader
chmod +x ${HOMEBREW_FOLDER}/services/PluginLoader
systemctl --user stop plugin_loader 2> /dev/null
systemctl --user disable plugin_loader 2> /dev/null
rm -f /home/deck/.config/systemd/user/plugin_loader.service
systemctl stop plugin_loader 2> /dev/null
systemctl disable plugin_loader 2> /dev/null
rm -f /etc/systemd/system/plugin_loader.service
cat > /etc/systemd/system/plugin_loader.service <<- EOM
[Unit]
Description=SteamDeck Plugin Loader
[Service]
Type=simple
User=root
Restart=always
ExecStart=/home/deck/homebrew/services/PluginLoader
WorkingDirectory=/home/deck/homebrew/services
Environment=PLUGIN_PATH=/home/deck/homebrew/plugins
[Install]
WantedBy=multi-user.target
EOM
systemctl daemon-reload
systemctl start plugin_loader
systemctl enable plugin_loader
+69
View File
@@ -0,0 +1,69 @@
#!/bin/sh
[ "$UID" -eq 0 ] || exec sudo "$0" "$@"
echo "Installing Steam Deck Plugin Loader pre-release..."
USER_DIR="$(getent passwd $SUDO_USER | cut -d: -f6)"
HOMEBREW_FOLDER="${USER_DIR}/homebrew"
# Create folder structure
rm -rf "${HOMEBREW_FOLDER}/services"
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/services"
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/plugins"
touch "${USER_DIR}/.steam/steam/.cef-enable-remote-debugging"
# Download latest release and install it
RELEASE=$(curl -s 'https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases' | jq -r "first(.[] | select(.prerelease == "true"))")
VERSION=$(jq -r '.tag_name' <<< ${RELEASE} )
DOWNLOADURL=$(jq -r '.assets[].browser_download_url | select(endswith("PluginLoader"))' <<< ${RELEASE})
printf "Installing version %s...\n" "${VERSION}"
curl -L $DOWNLOADURL --output ${HOMEBREW_FOLDER}/services/PluginLoader
chmod +x ${HOMEBREW_FOLDER}/services/PluginLoader
echo $VERSION > ${HOMEBREW_FOLDER}/services/.loader.version
systemctl --user stop plugin_loader 2> /dev/null
systemctl --user disable plugin_loader 2> /dev/null
systemctl stop plugin_loader 2> /dev/null
systemctl disable plugin_loader 2> /dev/null
curl -L https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service --output ${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service
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=LOG_LEVEL=DEBUG
[Install]
WantedBy=multi-user.target
EOM
if [[ -f "${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service" ]]; then
printf "Grabbed latest prerelease service.\n"
sed -i -e "s|\${HOMEBREW_FOLDER}|${HOMEBREW_FOLDER}|" "${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service"
cp -f "${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service" "/etc/systemd/system/plugin_loader.service"
else
printf "Could not curl latest prerelease systemd service, using built-in service as a backup!\n"
rm -f "/etc/systemd/system/plugin_loader.service"
cp "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" "/etc/systemd/system/plugin_loader.service"
fi
mkdir -p ${HOMEBREW_FOLDER}/services/.systemd
cp ${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service ${HOMEBREW_FOLDER}/services/.systemd/plugin_loader-prerelease.service
cp ${HOMEBREW_FOLDER}/services/plugin_loader-backup.service ${HOMEBREW_FOLDER}/services/.systemd/plugin_loader-backup.service
rm ${HOMEBREW_FOLDER}/services/plugin_loader-backup.service ${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service
systemctl daemon-reload
systemctl start plugin_loader
systemctl enable plugin_loader
+40 -10
View File
@@ -4,36 +4,66 @@
echo "Installing Steam Deck Plugin Loader release..."
HOMEBREW_FOLDER=/home/deck/homebrew
USER_DIR="$(getent passwd $SUDO_USER | cut -d: -f6)"
HOMEBREW_FOLDER="${USER_DIR}/homebrew"
# Create folder structure
rm -rf ${HOMEBREW_FOLDER}/services
sudo -u deck mkdir -p ${HOMEBREW_FOLDER}/services
sudo -u deck mkdir -p ${HOMEBREW_FOLDER}/plugins
rm -rf "${HOMEBREW_FOLDER}/services"
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/services"
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/plugins"
touch "${USER_DIR}/.steam/steam/.cef-enable-remote-debugging"
# Download latest release and install it
curl -L https://github.com/SteamDeckHomebrew/PluginLoader/releases/latest/download/PluginLoader --output ${HOMEBREW_FOLDER}/services/PluginLoader
RELEASE=$(curl -s 'https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases' | jq -r "first(.[] | select(.prerelease == "false"))")
VERSION=$(jq -r '.tag_name' <<< ${RELEASE} )
DOWNLOADURL=$(jq -r '.assets[].browser_download_url | select(endswith("PluginLoader"))' <<< ${RELEASE})
printf "Installing version %s...\n" "${VERSION}"
curl -L $DOWNLOADURL --output ${HOMEBREW_FOLDER}/services/PluginLoader
chmod +x ${HOMEBREW_FOLDER}/services/PluginLoader
echo $VERSION > ${HOMEBREW_FOLDER}/services/.loader.version
systemctl --user stop plugin_loader 2> /dev/null
systemctl --user disable plugin_loader 2> /dev/null
systemctl stop plugin_loader 2> /dev/null
systemctl disable plugin_loader 2> /dev/null
rm -f /etc/systemd/system/plugin_loader.service
cat > /etc/systemd/system/plugin_loader.service <<- EOM
curl -L https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-release.service --output ${HOMEBREW_FOLDER}/services/plugin_loader-release.service
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=/home/deck/homebrew/services/PluginLoader
WorkingDirectory=/home/deck/homebrew/services
Environment=PLUGIN_PATH=/home/deck/homebrew/plugins
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
WorkingDirectory=${HOMEBREW_FOLDER}/services
KillSignal=SIGKILL
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
Environment=LOG_LEVEL=INFO
[Install]
WantedBy=multi-user.target
EOM
if [[ -f "${HOMEBREW_FOLDER}/services/plugin_loader-release.service" ]]; then
printf "Grabbed latest release service.\n"
sed -i -e "s|\${HOMEBREW_FOLDER}|${HOMEBREW_FOLDER}|" "${HOMEBREW_FOLDER}/services/plugin_loader-release.service"
cp -f "${HOMEBREW_FOLDER}/services/plugin_loader-release.service" "/etc/systemd/system/plugin_loader.service"
else
printf "Could not curl latest release systemd service, using built-in service as a backup!\n"
rm -f "/etc/systemd/system/plugin_loader.service"
cp "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" "/etc/systemd/system/plugin_loader.service"
fi
mkdir -p ${HOMEBREW_FOLDER}/services/.systemd
cp ${HOMEBREW_FOLDER}/services/plugin_loader-release.service ${HOMEBREW_FOLDER}/services/.systemd/plugin_loader-release.service
cp ${HOMEBREW_FOLDER}/services/plugin_loader-backup.service ${HOMEBREW_FOLDER}/services/.systemd/plugin_loader-backup.service
rm ${HOMEBREW_FOLDER}/services/plugin_loader-backup.service ${HOMEBREW_FOLDER}/services/plugin_loader-release.service
systemctl daemon-reload
systemctl start plugin_loader
systemctl enable plugin_loader
+16
View File
@@ -0,0 +1,16 @@
[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
[Install]
WantedBy=multi-user.target
+16
View File
@@ -0,0 +1,16 @@
[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
[Install]
WantedBy=multi-user.target
+8 -5
View File
@@ -1,17 +1,20 @@
#!/bin/sh
[ "$UID" -eq 0 ] || exec sudo "$0" "$@"
echo "Uninstalling Steam Deck Plugin Loader..."
HOMEBREW_FOLDER=/home/deck/homebrew
USER_DIR="$(getent passwd $SUDO_USER | cut -d: -f6)"
HOMEBREW_FOLDER="${USER_DIR}/homebrew"
# Disable and remove services
sudo systemctl disable --now plugin_loader.service > /dev/null
sudo rm -f /home/deck/.config/systemd/user/plugin_loader.service
sudo rm -f /etc/systemd/system/plugin_loader.service
sudo rm -f "${USER_DIR}/.config/systemd/user/plugin_loader.service"
sudo rm -f "/etc/systemd/system/plugin_loader.service"
# Remove temporary folder if it exists from the install process
rm -rf /tmp/plugin_loader
rm -rf "/tmp/plugin_loader"
# Cleanup services folder
sudo rm ${HOMEBREW_FOLDER}/services/PluginLoader
sudo rm "${HOMEBREW_FOLDER}/services/PluginLoader"
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="#000" d="M495.9 166.6c3.2 8.7 .5 18.4-6.4 24.6l-43.3 39.4c1.1 8.3 1.7 16.8 1.7 25.4s-.6 17.1-1.7 25.4l43.3 39.4c6.9 6.2 9.6 15.9 6.4 24.6c-4.4 11.9-9.7 23.3-15.8 34.3l-4.7 8.1c-6.6 11-14 21.4-22.1 31.2c-5.9 7.2-15.7 9.6-24.5 6.8l-55.7-17.7c-13.4 10.3-28.2 18.9-44 25.4l-12.5 57.1c-2 9.1-9 16.3-18.2 17.8c-13.8 2.3-28 3.5-42.5 3.5s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-12.5-57.1c-15.8-6.5-30.6-15.1-44-25.4L83.1 425.9c-8.8 2.8-18.6 .3-24.5-6.8c-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1c-6.1-11-11.4-22.4-15.8-34.3c-3.2-8.7-.5-18.4 6.4-24.6l43.3-39.4C64.6 273.1 64 264.6 64 256s.6-17.1 1.7-25.4L22.4 191.2c-6.9-6.2-9.6-15.9-6.4-24.6c4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2c5.9-7.2 15.7-9.6 24.5-6.8l55.7 17.7c13.4-10.3 28.2-18.9 44-25.4l12.5-57.1c2-9.1 9-16.3 18.2-17.8C227.3 1.2 241.5 0 256 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l12.5 57.1c15.8 6.5 30.6 15.1 44 25.4l55.7-17.7c8.8-2.8 18.6-.3 24.5 6.8c8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3zM256 336c44.2 0 80-35.8 80-80s-35.8-80-80-80s-80 35.8-80 80s35.8 80 80 80z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="#000" d="M96 0C78.3 0 64 14.3 64 32v96h64V32c0-17.7-14.3-32-32-32zM288 0c-17.7 0-32 14.3-32 32v96h64V32c0-17.7-14.3-32-32-32zM32 160c-17.7 0-32 14.3-32 32s14.3 32 32 32v32c0 77.4 55 142 128 156.8V480c0 17.7 14.3 32 32 32s32-14.3 32-32V412.8C297 398 352 333.4 352 256V224c17.7 0 32-14.3 32-32s-14.3-32-32-32H32z"/></svg>

After

Width:  |  Height:  |  Size: 561 B

+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="32" height="14" viewBox="0.395 9 31.21 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.39502 16C0.39502 12.134 3.57877 9 7.50613 9H24.4938C28.4211 9 31.6049 12.134 31.6049 16C31.6049 19.866 28.4211 23 24.4938 23H7.50613C3.57877 23 0.39502 19.866 0.39502 16Z" fill="#000"/>
<ellipse cx="8.88886" cy="16" rx="1.77778" ry="1.75" fill="#fff"/>
<ellipse cx="15.9999" cy="16" rx="1.77778" ry="1.75" fill="#fff"/>
<ellipse cx="23.111" cy="16" rx="1.77778" ry="1.75" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 554 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 36" fill="none" class="footericons_SizeMedium_3-w0R footericons_Light_2e0Fq"><path class="footericons_Background_I3P4e" fill="#000" d="M0 18C0 8.05888 8.05888 0 18 0H82C91.9411 0 100 8.05888 100 18C100 27.9411 91.9411 36 82 36H18C8.05888 36 0 27.9411 0 18Z"></path><path class="footericons_Foreground_39K5g" fill="#fff" d="M21.8011 11.5C22.6531 11.5 23.4391 11.62 24.1591 11.86C24.8791 12.1 25.4851 12.394 25.9771 12.742L24.8611 14.722C24.4171 14.41 23.9191 14.158 23.3671 13.966C22.8271 13.774 22.3111 13.678 21.8191 13.678C21.2191 13.678 20.7511 13.804 20.4151 14.056C20.0791 14.296 19.9111 14.632 19.9111 15.064C19.9111 15.496 20.1091 15.838 20.5051 16.09C20.9011 16.33 21.5071 16.594 22.3231 16.882C23.1631 17.182 23.8351 17.458 24.3391 17.71C24.8431 17.962 25.2811 18.334 25.6531 18.826C26.0371 19.306 26.2291 19.924 26.2291 20.68C26.2291 21.484 26.0191 22.18 25.5991 22.768C25.1911 23.356 24.6151 23.812 23.8711 24.136C23.1271 24.448 22.2751 24.604 21.3151 24.604C20.5351 24.604 19.7371 24.502 18.9211 24.298C18.1171 24.082 17.4091 23.794 16.7971 23.434L17.6251 21.238C18.2011 21.55 18.8071 21.802 19.4431 21.994C20.0911 22.174 20.7271 22.264 21.3511 22.264C22.0351 22.264 22.5451 22.132 22.8811 21.868C23.2291 21.604 23.4031 21.256 23.4031 20.824C23.4031 20.392 23.2171 20.056 22.8451 19.816C22.4731 19.576 21.9031 19.33 21.1351 19.078C20.2711 18.802 19.5751 18.538 19.0471 18.286C18.5191 18.022 18.0631 17.644 17.6791 17.152C17.3071 16.648 17.1211 15.994 17.1211 15.19C17.1211 14.446 17.3131 13.798 17.6971 13.246C18.0931 12.682 18.6451 12.25 19.3531 11.95C20.0611 11.65 20.8771 11.5 21.8011 11.5Z"></path><path class="footericons_Foreground_39K5g" fill="#fff" d="M35.2486 24.388H32.6026V14.056H28.7866V11.788H39.0646V14.056H35.2486V24.388Z"></path><path class="footericons_Foreground_39K5g" fill="#fff" d="M42.3148 11.788H50.8108V14.038H44.9608V16.882H50.0008V19.15H44.9608V22.102H50.8108V24.388H42.3148V11.788Z"></path><path class="footericons_Foreground_39K5g" fill="#fff" d="M65.8582 24.388H62.9962L62.1322 21.94H57.2002L56.3722 24.388H53.6182L58.3342 11.788H60.9982L65.8582 24.388ZM59.6482 14.794L57.9202 19.834H61.4122L59.6482 14.794Z"></path><path class="footericons_Foreground_39K5g" fill="#fff" d="M75.8489 20.734L79.7729 11.788H82.4549V24.388H79.9169V16.378L76.5329 24.028H74.9309L71.4749 16.468V24.388H69.0629V11.788H71.6009L75.8489 20.734Z"></path></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="#000" d="M547.6 103.8L490.3 13.1C485.2 5 476.1 0 466.4 0H109.6C99.9 0 90.8 5 85.7 13.1L28.3 103.8c-29.6 46.8-3.4 111.9 51.9 119.4c4 .5 8.1 .8 12.1 .8c26.1 0 49.3-11.4 65.2-29c15.9 17.6 39.1 29 65.2 29c26.1 0 49.3-11.4 65.2-29c15.9 17.6 39.1 29 65.2 29c26.2 0 49.3-11.4 65.2-29c16 17.6 39.1 29 65.2 29c4.1 0 8.1-.3 12.1-.8c55.5-7.4 81.8-72.5 52.1-119.4zM499.7 254.9l-.1 0c-5.3 .7-10.7 1.1-16.2 1.1c-12.4 0-24.3-1.9-35.4-5.3V384H128V250.6c-11.2 3.5-23.2 5.4-35.6 5.4c-5.5 0-11-.4-16.3-1.1l-.1 0c-4.1-.6-8.1-1.3-12-2.3V384v64c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V384 252.6c-4 1-8 1.8-12.3 2.3z"/></svg>

After

Width:  |  Height:  |  Size: 850 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 30C23.732 30 30 23.732 30 16C30 8.26801 23.732 2 16 2C8.26801 2 2 8.26801 2 16C2 23.732 8.26801 30 16 30ZM22.0775 9H18.5059L15.8368 13.3393L13.1677 9H9.69202L14.2814 15.5186L9.5 22.5H12.8796L15.8944 17.5821L19.0436 22.5H22.5L17.3538 15.4221L22.0775 9Z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 423 B

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

After

Width:  |  Height:  |  Size: 8.9 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="#fff" d="M495.9 166.6c3.2 8.7 .5 18.4-6.4 24.6l-43.3 39.4c1.1 8.3 1.7 16.8 1.7 25.4s-.6 17.1-1.7 25.4l43.3 39.4c6.9 6.2 9.6 15.9 6.4 24.6c-4.4 11.9-9.7 23.3-15.8 34.3l-4.7 8.1c-6.6 11-14 21.4-22.1 31.2c-5.9 7.2-15.7 9.6-24.5 6.8l-55.7-17.7c-13.4 10.3-28.2 18.9-44 25.4l-12.5 57.1c-2 9.1-9 16.3-18.2 17.8c-13.8 2.3-28 3.5-42.5 3.5s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-12.5-57.1c-15.8-6.5-30.6-15.1-44-25.4L83.1 425.9c-8.8 2.8-18.6 .3-24.5-6.8c-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1c-6.1-11-11.4-22.4-15.8-34.3c-3.2-8.7-.5-18.4 6.4-24.6l43.3-39.4C64.6 273.1 64 264.6 64 256s.6-17.1 1.7-25.4L22.4 191.2c-6.9-6.2-9.6-15.9-6.4-24.6c4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2c5.9-7.2 15.7-9.6 24.5-6.8l55.7 17.7c13.4-10.3 28.2-18.9 44-25.4l12.5-57.1c2-9.1 9-16.3 18.2-17.8C227.3 1.2 241.5 0 256 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l12.5 57.1c15.8 6.5 30.6 15.1 44 25.4l55.7-17.7c8.8-2.8 18.6-.3 24.5 6.8c8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3zM256 336c44.2 0 80-35.8 80-80s-35.8-80-80-80s-80 35.8-80 80s35.8 80 80 80z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="#fff" d="M96 0C78.3 0 64 14.3 64 32v96h64V32c0-17.7-14.3-32-32-32zM288 0c-17.7 0-32 14.3-32 32v96h64V32c0-17.7-14.3-32-32-32zM32 160c-17.7 0-32 14.3-32 32s14.3 32 32 32v32c0 77.4 55 142 128 156.8V480c0 17.7 14.3 32 32 32s32-14.3 32-32V412.8C297 398 352 333.4 352 256V224c17.7 0 32-14.3 32-32s-14.3-32-32-32H32z"/></svg>

After

Width:  |  Height:  |  Size: 561 B

+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="32" height="14" viewBox="0.395 9 31.21 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.39502 16C0.39502 12.134 3.57877 9 7.50613 9H24.4938C28.4211 9 31.6049 12.134 31.6049 16C31.6049 19.866 28.4211 23 24.4938 23H7.50613C3.57877 23 0.39502 19.866 0.39502 16Z" fill="white"/>
<ellipse cx="8.88886" cy="16" rx="1.77778" ry="1.75" style="fill: rgb(0, 0, 0);"/>
<ellipse cx="15.9999" cy="16" rx="1.77778" ry="1.75" style="fill: rgb(0, 0, 0);"/>
<ellipse cx="23.111" cy="16" rx="1.77778" ry="1.75" style="fill: rgb(0, 0, 0);"/>
</svg>

After

Width:  |  Height:  |  Size: 603 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 36" fill="none" class="footericons_SizeMedium_3-w0R footericons_Light_2e0Fq"><path class="footericons_Background_I3P4e" fill="#fff" d="M0 18C0 8.05888 8.05888 0 18 0H82C91.9411 0 100 8.05888 100 18C100 27.9411 91.9411 36 82 36H18C8.05888 36 0 27.9411 0 18Z"></path><path class="footericons_Foreground_39K5g" fill="#000" d="M21.8011 11.5C22.6531 11.5 23.4391 11.62 24.1591 11.86C24.8791 12.1 25.4851 12.394 25.9771 12.742L24.8611 14.722C24.4171 14.41 23.9191 14.158 23.3671 13.966C22.8271 13.774 22.3111 13.678 21.8191 13.678C21.2191 13.678 20.7511 13.804 20.4151 14.056C20.0791 14.296 19.9111 14.632 19.9111 15.064C19.9111 15.496 20.1091 15.838 20.5051 16.09C20.9011 16.33 21.5071 16.594 22.3231 16.882C23.1631 17.182 23.8351 17.458 24.3391 17.71C24.8431 17.962 25.2811 18.334 25.6531 18.826C26.0371 19.306 26.2291 19.924 26.2291 20.68C26.2291 21.484 26.0191 22.18 25.5991 22.768C25.1911 23.356 24.6151 23.812 23.8711 24.136C23.1271 24.448 22.2751 24.604 21.3151 24.604C20.5351 24.604 19.7371 24.502 18.9211 24.298C18.1171 24.082 17.4091 23.794 16.7971 23.434L17.6251 21.238C18.2011 21.55 18.8071 21.802 19.4431 21.994C20.0911 22.174 20.7271 22.264 21.3511 22.264C22.0351 22.264 22.5451 22.132 22.8811 21.868C23.2291 21.604 23.4031 21.256 23.4031 20.824C23.4031 20.392 23.2171 20.056 22.8451 19.816C22.4731 19.576 21.9031 19.33 21.1351 19.078C20.2711 18.802 19.5751 18.538 19.0471 18.286C18.5191 18.022 18.0631 17.644 17.6791 17.152C17.3071 16.648 17.1211 15.994 17.1211 15.19C17.1211 14.446 17.3131 13.798 17.6971 13.246C18.0931 12.682 18.6451 12.25 19.3531 11.95C20.0611 11.65 20.8771 11.5 21.8011 11.5Z"></path><path class="footericons_Foreground_39K5g" fill="#000" d="M35.2486 24.388H32.6026V14.056H28.7866V11.788H39.0646V14.056H35.2486V24.388Z"></path><path class="footericons_Foreground_39K5g" fill="#000" d="M42.3148 11.788H50.8108V14.038H44.9608V16.882H50.0008V19.15H44.9608V22.102H50.8108V24.388H42.3148V11.788Z"></path><path class="footericons_Foreground_39K5g" fill="#000" d="M65.8582 24.388H62.9962L62.1322 21.94H57.2002L56.3722 24.388H53.6182L58.3342 11.788H60.9982L65.8582 24.388ZM59.6482 14.794L57.9202 19.834H61.4122L59.6482 14.794Z"></path><path class="footericons_Foreground_39K5g" fill="#000" d="M75.8489 20.734L79.7729 11.788H82.4549V24.388H79.9169V16.378L76.5329 24.028H74.9309L71.4749 16.468V24.388H69.0629V11.788H71.6009L75.8489 20.734Z"></path></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="#fff" d="M547.6 103.8L490.3 13.1C485.2 5 476.1 0 466.4 0H109.6C99.9 0 90.8 5 85.7 13.1L28.3 103.8c-29.6 46.8-3.4 111.9 51.9 119.4c4 .5 8.1 .8 12.1 .8c26.1 0 49.3-11.4 65.2-29c15.9 17.6 39.1 29 65.2 29c26.1 0 49.3-11.4 65.2-29c15.9 17.6 39.1 29 65.2 29c26.2 0 49.3-11.4 65.2-29c16 17.6 39.1 29 65.2 29c4.1 0 8.1-.3 12.1-.8c55.5-7.4 81.8-72.5 52.1-119.4zM499.7 254.9l-.1 0c-5.3 .7-10.7 1.1-16.2 1.1c-12.4 0-24.3-1.9-35.4-5.3V384H128V250.6c-11.2 3.5-23.2 5.4-35.6 5.4c-5.5 0-11-.4-16.3-1.1l-.1 0c-4.1-.6-8.1-1.3-12-2.3V384v64c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V384 252.6c-4 1-8 1.8-12.3 2.3z"/></svg>

After

Width:  |  Height:  |  Size: 850 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 30C23.732 30 30 23.732 30 16C30 8.26801 23.732 2 16 2C8.26801 2 2 8.26801 2 16C2 23.732 8.26801 30 16 30ZM22.0775 9H18.5059L15.8368 13.3393L13.1677 9H9.69202L14.2814 15.5186L9.5 22.5H12.8796L15.8944 17.5821L19.0436 22.5H22.5L17.3538 15.4221L22.0775 9Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 424 B

+4
View File
@@ -0,0 +1,4 @@
node_modules/
.yalc
yalc.lock
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
cd frontend && npm run lint
+9
View File
@@ -0,0 +1,9 @@
module.exports = {
semi: true,
trailingComma: 'all',
singleQuote: true,
printWidth: 120,
tabWidth: 2,
endOfLine: 'auto',
plugins: [require('prettier-plugin-import-sort')],
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

+100
View File
@@ -0,0 +1,100 @@
export default {
contextSeparator: '_',
// Key separator used in your translation keys
createOldCatalogs: false,
// Save the \_old files
defaultNamespace: 'translation',
// Default namespace used in your i18next config
defaultValue: '',
// Default value to give to keys with no value
// You may also specify a function accepting the locale, namespace, key, and value as arguments
indentation: 2,
// Indentation of the catalog files
keepRemoved: true,
// Keep keys from the catalog that are no longer in code
keySeparator: '.',
// Key separator used in your translation keys
// If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance.
// see below for more details
lexers: {
mjs: ['JavascriptLexer'],
js: ['JavascriptLexer'], // if you're writing jsx inside .js files, change this to JsxLexer
ts: ['JavascriptLexer'],
jsx: ['JsxLexer'],
tsx: ['JsxLexer'],
default: ['JavascriptLexer'],
},
lineEnding: 'auto',
// Control the line ending. See options at https://github.com/ryanve/eol
locales: ['en-US', 'it-IT'],
// An array of the locales in your applications
namespaceSeparator: false,
// Namespace separator used in your translation keys
// If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance.
output: '../backend/locales/$LOCALE.json',
// Supports $LOCALE and $NAMESPACE injection
// Supports JSON (.json) and YAML (.yml) file formats
// Where to write the locale files relative to process.cwd()
pluralSeparator: '_',
// Plural separator used in your translation keys
// If you want to use plain english keys, separators such as `_` might conflict. You might want to set `pluralSeparator` to a different string that does not occur in your keys.
input: './src/**/*.{ts,tsx}',
// An array of globs that describe where to look for source files
// relative to the location of the configuration file
sort: true,
// Whether or not to sort the catalog. Can also be a [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#parameters)
verbose: false,
// Display info about the parsing including some stats
failOnWarnings: false,
// Exit with an exit code of 1 on warnings
failOnUpdate: false,
// Exit with an exit code of 1 when translations are updated (for CI purpose)
customValueTemplate: null,
// If you wish to customize the value output the value as an object, you can set your own format.
// ${defaultValue} is the default value you set in your translation function.
// Any other custom property will be automatically extracted.
//
// Example:
// {
// message: "${defaultValue}",
// description: "${maxLength}", // t('my-key', {maxLength: 150})
// }
resetDefaultValueLocale: null,
// The locale to compare with default values to determine whether a default value has been changed.
// If this is set and a default value differs from a translation in the specified locale, all entries
// for that key across locales are reset to the default value, and existing translations are moved to
// the `_old` file.
i18nextOptions: null,
// If you wish to customize options in internally used i18next instance, you can define an object with any
// configuration property supported by i18next (https://www.i18next.com/overview/configuration-options).
// { compatibilityJSON: 'v3' } can be used to generate v3 compatible plurals.
yamlOptions: null,
// If you wish to customize options for yaml output, you can define an object here.
// Configuration options are here (https://github.com/nodeca/js-yaml#dump-object---options-).
// Example:
// {
// lineWidth: -1,
// }
}
+2
View File
@@ -0,0 +1,2 @@
declare module '*.png';
declare module '*.jpg';
+55
View File
@@ -0,0 +1,55 @@
{
"name": "decky_frontend",
"version": "2.1.1",
"private": true,
"license": "GPLV2",
"scripts": {
"prepare": "cd .. && husky install frontend/.husky",
"build": "rollup -c",
"watch": "rollup -c -w",
"lint": "prettier -c src",
"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": "^7.9.0",
"import-sort-style-module": "^6.0.0",
"inquirer": "^8.2.5",
"prettier": "^2.8.8",
"prettier-plugin-import-sort": "^0.0.7",
"react": "16.14.0",
"react-dom": "16.14.0",
"rollup": "^2.79.1",
"rollup-plugin-delete": "^2.0.0",
"rollup-plugin-external-globals": "^0.6.1",
"rollup-plugin-polyfill-node": "^0.10.2",
"tslib": "^2.5.2",
"typescript": "^4.9.5"
},
"importSort": {
".js, .jsx, .ts, .tsx": {
"style": "module",
"parser": "typescript"
}
},
"dependencies": {
"decky-frontend-lib": "3.20.7",
"i18next": "^22.5.0",
"i18next-http-backend": "^2.2.1",
"react-file-icon": "^1.3.0",
"react-i18next": "^12.3.1",
"react-icons": "^4.8.0",
"react-markdown": "^8.0.7",
"remark-gfm": "^3.0.1"
}
}
+3967
View File
File diff suppressed because it is too large Load Diff
+49
View File
@@ -0,0 +1,49 @@
import commonjs from '@rollup/plugin-commonjs';
import image from '@rollup/plugin-image';
import json from '@rollup/plugin-json';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import typescript from '@rollup/plugin-typescript';
import { defineConfig } from 'rollup';
import del from 'rollup-plugin-delete';
import externalGlobals from 'rollup-plugin-external-globals';
const hiddenWarnings = ['THIS_IS_UNDEFINED', 'EVAL'];
export default defineConfig({
input: 'src/index.ts',
plugins: [
del({ targets: '../backend/static/*', force: true }),
commonjs(),
nodeResolve({
browser: true
}),
externalGlobals({
react: 'SP_REACT',
'react-dom': 'SP_REACTDOM',
// hack to shut up react-markdown
process: '{cwd: () => {}}',
path: '{dirname: () => {}, join: () => {}, basename: () => {}, extname: () => {}}',
url: '{fileURLToPath: (f) => f}',
}),
typescript(),
json(),
replace({
preventAssignment: false,
'process.env.NODE_ENV': JSON.stringify('production'),
}),
image(),
],
preserveEntrySignatures: false,
output: {
dir: '../backend/static',
format: 'esm',
chunkFileNames: (chunkInfo) => {
return 'chunk-[hash].js';
},
},
onwarn: function (message, handleWarning) {
if (hiddenWarnings.some((warning) => message.code === warning)) return;
handleWarning(message);
},
});
@@ -0,0 +1,74 @@
import { FC, createContext, useContext, useEffect, useState } from 'react';
interface PublicDeckyGlobalComponentsState {
components: Map<string, FC>;
}
export class DeckyGlobalComponentsState {
// TODO a set would be better
private _components = new Map<string, FC>();
public eventBus = new EventTarget();
publicState(): PublicDeckyGlobalComponentsState {
return { components: this._components };
}
addComponent(path: string, component: FC) {
this._components.set(path, component);
this.notifyUpdate();
}
removeComponent(path: string) {
this._components.delete(path);
this.notifyUpdate();
}
private notifyUpdate() {
this.eventBus.dispatchEvent(new Event('update'));
}
}
interface DeckyGlobalComponentsContext extends PublicDeckyGlobalComponentsState {
addComponent(path: string, component: FC): void;
removeComponent(path: string): void;
}
const DeckyGlobalComponentsContext = createContext<DeckyGlobalComponentsContext>(null as any);
export const useDeckyGlobalComponentsState = () => useContext(DeckyGlobalComponentsContext);
interface Props {
deckyGlobalComponentsState: DeckyGlobalComponentsState;
}
export const DeckyGlobalComponentsStateContextProvider: FC<Props> = ({
children,
deckyGlobalComponentsState: deckyGlobalComponentsState,
}) => {
const [publicDeckyGlobalComponentsState, setPublicDeckyGlobalComponentsState] =
useState<PublicDeckyGlobalComponentsState>({
...deckyGlobalComponentsState.publicState(),
});
useEffect(() => {
function onUpdate() {
setPublicDeckyGlobalComponentsState({ ...deckyGlobalComponentsState.publicState() });
}
deckyGlobalComponentsState.eventBus.addEventListener('update', onUpdate);
return () => deckyGlobalComponentsState.eventBus.removeEventListener('update', onUpdate);
}, []);
const addComponent = deckyGlobalComponentsState.addComponent.bind(deckyGlobalComponentsState);
const removeComponent = deckyGlobalComponentsState.removeComponent.bind(deckyGlobalComponentsState);
return (
<DeckyGlobalComponentsContext.Provider
value={{ ...publicDeckyGlobalComponentsState, addComponent, removeComponent }}
>
{children}
</DeckyGlobalComponentsContext.Provider>
);
};
+37
View File
@@ -0,0 +1,37 @@
export default function DeckyIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 456" width="512" height="456">
<g>
<path
style={{ fill: 'none' }}
d="M154.33,72.51v49.79c11.78-0.17,23.48,2,34.42,6.39c10.93,4.39,20.89,10.91,29.28,19.18
c8.39,8.27,15.06,18.13,19.61,29c4.55,10.87,6.89,22.54,6.89,34.32c0,11.78-2.34,23.45-6.89,34.32
c-4.55,10.87-11.21,20.73-19.61,29c-8.39,8.27-18.35,14.79-29.28,19.18c-10.94,4.39-22.63,6.56-34.42,6.39v49.77
c36.78,0,72.05-14.61,98.05-40.62c26-26.01,40.61-61.28,40.61-98.05c0-36.78-14.61-72.05-40.61-98.05
C226.38,87.12,191.11,72.51,154.33,72.51z"
/>
<ellipse
transform="matrix(0.982 -0.1891 0.1891 0.982 -37.1795 32.9988)"
style={{ fill: 'none' }}
cx="154.33"
cy="211.33"
rx="69.33"
ry="69.33"
/>
<path style={{ fill: 'none' }} d="M430,97h-52v187h52c7.18,0,13-5.82,13-13V110C443,102.82,437.18,97,430,97z" />
<path
style={{ fill: 'currentColor' }}
d="M432,27h-54V0H0v361c0,52.47,42.53,95,95,95h188c52.47,0,95-42.53,95-95v-7h54c44.18,0,80-35.82,80-80V107
C512,62.82,476.18,27,432,27z M85,211.33c0-38.29,31.04-69.33,69.33-69.33c38.29,0,69.33,31.04,69.33,69.33
c0,38.29-31.04,69.33-69.33,69.33C116.04,280.67,85,249.62,85,211.33z M252.39,309.23c-26.01,26-61.28,40.62-98.05,40.62v-49.77
c11.78,0.17,23.48-2,34.42-6.39c10.93-4.39,20.89-10.91,29.28-19.18c8.39-8.27,15.06-18.13,19.61-29
c4.55-10.87,6.89-22.53,6.89-34.32c0-11.78-2.34-23.45-6.89-34.32c-4.55-10.87-11.21-20.73-19.61-29
c-8.39-8.27-18.35-14.79-29.28-19.18c-10.94-4.39-22.63-6.56-34.42-6.39V72.51c36.78,0,72.05,14.61,98.05,40.61
c26,26.01,40.61,61.28,40.61,98.05C293,247.96,278.39,283.23,252.39,309.23z M443,271c0,7.18-5.82,13-13,13h-52V97h52
c7.18,0,13,5.82,13,13V271z"
/>
</g>
</svg>
);
}
@@ -0,0 +1,103 @@
import { ComponentType, FC, createContext, useContext, useEffect, useState } from 'react';
import type { RouteProps } from 'react-router';
export interface RouterEntry {
props: Omit<RouteProps, 'path' | 'children'>;
component: ComponentType;
}
export type RoutePatch = (route: RouteProps) => RouteProps;
interface PublicDeckyRouterState {
routes: Map<string, RouterEntry>;
routePatches: Map<string, Set<RoutePatch>>;
}
export class DeckyRouterState {
private _routes = new Map<string, RouterEntry>();
private _routePatches = new Map<string, Set<RoutePatch>>();
public eventBus = new EventTarget();
publicState(): PublicDeckyRouterState {
return { routes: this._routes, routePatches: this._routePatches };
}
addRoute(path: string, component: RouterEntry['component'], props: RouterEntry['props'] = {}) {
this._routes.set(path, { props, component });
this.notifyUpdate();
}
addPatch(path: string, patch: RoutePatch) {
let patchList = this._routePatches.get(path);
if (!patchList) {
patchList = new Set();
this._routePatches.set(path, patchList);
}
patchList.add(patch);
this.notifyUpdate();
return patch;
}
removePatch(path: string, patch: RoutePatch) {
const patchList = this._routePatches.get(path);
patchList?.delete(patch);
if (patchList?.size == 0) {
this._routePatches.delete(path);
}
this.notifyUpdate();
}
removeRoute(path: string) {
this._routes.delete(path);
this.notifyUpdate();
}
private notifyUpdate() {
this.eventBus.dispatchEvent(new Event('update'));
}
}
interface DeckyRouterStateContext extends PublicDeckyRouterState {
addRoute(path: string, component: RouterEntry['component'], props: RouterEntry['props']): void;
addPatch(path: string, patch: RoutePatch): RoutePatch;
removePatch(path: string, patch: RoutePatch): void;
removeRoute(path: string): void;
}
const DeckyRouterStateContext = createContext<DeckyRouterStateContext>(null as any);
export const useDeckyRouterState = () => useContext(DeckyRouterStateContext);
interface Props {
deckyRouterState: DeckyRouterState;
}
export const DeckyRouterStateContextProvider: FC<Props> = ({ children, deckyRouterState }) => {
const [publicDeckyRouterState, setPublicDeckyRouterState] = useState<PublicDeckyRouterState>({
...deckyRouterState.publicState(),
});
useEffect(() => {
function onUpdate() {
setPublicDeckyRouterState({ ...deckyRouterState.publicState() });
}
deckyRouterState.eventBus.addEventListener('update', onUpdate);
return () => deckyRouterState.eventBus.removeEventListener('update', onUpdate);
}, []);
const addRoute = deckyRouterState.addRoute.bind(deckyRouterState);
const addPatch = deckyRouterState.addPatch.bind(deckyRouterState);
const removePatch = deckyRouterState.removePatch.bind(deckyRouterState);
const removeRoute = deckyRouterState.removeRoute.bind(deckyRouterState);
return (
<DeckyRouterStateContext.Provider
value={{ ...publicDeckyRouterState, addRoute, addPatch, removePatch, removeRoute }}
>
{children}
</DeckyRouterStateContext.Provider>
);
};
+134
View File
@@ -0,0 +1,134 @@
import { FC, createContext, useContext, useEffect, useState } from 'react';
import { Plugin } from '../plugin';
import { PluginUpdateMapping } from '../store';
import { VerInfo } from '../updater';
interface PublicDeckyState {
plugins: Plugin[];
pluginOrder: string[];
activePlugin: Plugin | null;
updates: PluginUpdateMapping | null;
hasLoaderUpdate?: boolean;
isLoaderUpdating: boolean;
versionInfo: VerInfo | null;
}
export class DeckyState {
private _plugins: Plugin[] = [];
private _pluginOrder: string[] = [];
private _activePlugin: Plugin | null = null;
private _updates: PluginUpdateMapping | null = null;
private _hasLoaderUpdate: boolean = false;
private _isLoaderUpdating: boolean = false;
private _versionInfo: VerInfo | null = null;
public eventBus = new EventTarget();
publicState(): PublicDeckyState {
return {
plugins: this._plugins,
pluginOrder: this._pluginOrder,
activePlugin: this._activePlugin,
updates: this._updates,
hasLoaderUpdate: this._hasLoaderUpdate,
isLoaderUpdating: this._isLoaderUpdating,
versionInfo: this._versionInfo,
};
}
setVersionInfo(versionInfo: VerInfo) {
this._versionInfo = versionInfo;
this.notifyUpdate();
}
setPlugins(plugins: Plugin[]) {
this._plugins = plugins;
this.notifyUpdate();
}
setPluginOrder(pluginOrder: string[]) {
this._pluginOrder = pluginOrder;
this.notifyUpdate();
}
setActivePlugin(name: string) {
this._activePlugin = this._plugins.find((plugin) => plugin.name === name) ?? null;
this.notifyUpdate();
}
closeActivePlugin() {
this._activePlugin = null;
this.notifyUpdate();
}
setUpdates(updates: PluginUpdateMapping) {
this._updates = updates;
this.notifyUpdate();
}
setHasLoaderUpdate(hasUpdate: boolean) {
this._hasLoaderUpdate = hasUpdate;
this.notifyUpdate();
}
setIsLoaderUpdating(isUpdating: boolean) {
this._isLoaderUpdating = isUpdating;
this.notifyUpdate();
}
private notifyUpdate() {
this.eventBus.dispatchEvent(new Event('update'));
}
}
interface DeckyStateContext extends PublicDeckyState {
setVersionInfo(versionInfo: VerInfo): void;
setIsLoaderUpdating(hasUpdate: boolean): void;
setActivePlugin(name: string): void;
setPluginOrder(pluginOrder: string[]): void;
closeActivePlugin(): void;
}
const DeckyStateContext = createContext<DeckyStateContext>(null as any);
export const useDeckyState = () => useContext(DeckyStateContext);
interface Props {
deckyState: DeckyState;
}
export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) => {
const [publicDeckyState, setPublicDeckyState] = useState<PublicDeckyState>({ ...deckyState.publicState() });
useEffect(() => {
function onUpdate() {
setPublicDeckyState({ ...deckyState.publicState() });
}
deckyState.eventBus.addEventListener('update', onUpdate);
return () => deckyState.eventBus.removeEventListener('update', onUpdate);
}, []);
const setIsLoaderUpdating = (hasUpdate: boolean) => deckyState.setIsLoaderUpdating(hasUpdate);
const setVersionInfo = (versionInfo: VerInfo) => deckyState.setVersionInfo(versionInfo);
const setActivePlugin = (name: string) => deckyState.setActivePlugin(name);
const closeActivePlugin = () => deckyState.closeActivePlugin();
const setPluginOrder = (pluginOrder: string[]) => deckyState.setPluginOrder(pluginOrder);
return (
<DeckyStateContext.Provider
value={{
...publicDeckyState,
setIsLoaderUpdating,
setVersionInfo,
setActivePlugin,
closeActivePlugin,
setPluginOrder,
}}
>
{children}
</DeckyStateContext.Provider>
);
};
+54
View File
@@ -0,0 +1,54 @@
import { ToastData, joinClassNames } from 'decky-frontend-lib';
import { FC, useEffect, useState } from 'react';
import { ReactElement } from 'react-markdown/lib/react-markdown';
import { useDeckyToasterState } from './DeckyToasterState';
import Toast, { toastClasses } from './Toast';
interface DeckyToasterProps {}
interface RenderedToast {
component: ReactElement;
data: ToastData;
}
const DeckyToaster: FC<DeckyToasterProps> = () => {
const { toasts, removeToast } = useDeckyToasterState();
const [renderedToast, setRenderedToast] = useState<RenderedToast | null>(null);
console.log(toasts);
if (toasts.size > 0) {
const [activeToast] = toasts;
if (!renderedToast || activeToast != renderedToast.data) {
// TODO play toast sound
console.log('rendering toast', activeToast);
setRenderedToast({ component: <Toast key={Math.random()} toast={activeToast} />, data: activeToast });
}
} else {
if (renderedToast) setRenderedToast(null);
}
useEffect(() => {
// not actually node but TS is shit
let interval: NodeJS.Timer | null;
if (renderedToast) {
interval = setTimeout(() => {
interval = null;
console.log('clear toast', renderedToast.data);
removeToast(renderedToast.data);
}, (renderedToast.data.duration || 5e3) + 1000);
console.log('set int', interval);
}
return () => {
if (interval) {
console.log('clearing int', interval);
clearTimeout(interval);
}
};
}, [renderedToast]);
return (
<div className={joinClassNames('deckyToaster', toastClasses.ToastPlaceholder)}>
{renderedToast && renderedToast.component}
</div>
);
};
export default DeckyToaster;
@@ -0,0 +1,69 @@
import { ToastData } from 'decky-frontend-lib';
import { FC, 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();
publicState(): PublicDeckyToasterState {
return { toasts: this._toasts };
}
addToast(toast: ToastData) {
this._toasts.add(toast);
this.notifyUpdate();
}
removeToast(toast: ToastData) {
this._toasts.delete(toast);
this.notifyUpdate();
}
private notifyUpdate() {
this.eventBus.dispatchEvent(new Event('update'));
}
}
interface DeckyToasterContext extends PublicDeckyToasterState {
addToast(toast: ToastData): void;
removeToast(toast: ToastData): void;
}
const DeckyToasterContext = createContext<DeckyToasterContext>(null as any);
export const useDeckyToasterState = () => useContext(DeckyToasterContext);
interface Props {
deckyToasterState: DeckyToasterState;
}
export const DeckyToasterStateContextProvider: FC<Props> = ({ children, deckyToasterState }) => {
const [publicDeckyToasterState, setPublicDeckyToasterState] = useState<PublicDeckyToasterState>({
...deckyToasterState.publicState(),
});
useEffect(() => {
function onUpdate() {
setPublicDeckyToasterState({ ...deckyToasterState.publicState() });
}
deckyToasterState.eventBus.addEventListener('update', onUpdate);
return () => deckyToasterState.eventBus.removeEventListener('update', onUpdate);
}, []);
const addToast = deckyToasterState.addToast.bind(deckyToasterState);
const removeToast = deckyToasterState.removeToast.bind(deckyToasterState);
return (
<DeckyToasterContext.Provider value={{ ...publicDeckyToasterState, addToast, removeToast }}>
{children}
</DeckyToasterContext.Provider>
);
};
+11
View File
@@ -0,0 +1,11 @@
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;
+42
View File
@@ -0,0 +1,42 @@
import { Focusable, Navigation } from 'decky-frontend-lib';
import { FunctionComponent, useRef } from 'react';
import ReactMarkdown, { Options as ReactMarkdownOptions } from 'react-markdown';
import remarkGfm from 'remark-gfm';
interface MarkdownProps extends ReactMarkdownOptions {
onDismiss?: () => void;
}
const Markdown: FunctionComponent<MarkdownProps> = (props) => {
return (
<Focusable>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
div: (nodeProps) => <Focusable {...nodeProps.node.properties}>{nodeProps.children}</Focusable>,
a: (nodeProps) => {
const aRef = useRef<HTMLAnchorElement>(null);
return (
// TODO fix focus ring
<Focusable
onActivate={() => {}}
onOKButton={() => {
props.onDismiss?.();
Navigation.NavigateToExternalWeb(aRef.current!.href);
}}
style={{ display: 'inline' }}
>
<a ref={aRef} {...nodeProps.node.properties}>
{nodeProps.children}
</a>
</Focusable>
);
},
}}
{...props}
/>
</Focusable>
);
};
export default Markdown;
@@ -0,0 +1,25 @@
import { CSSProperties, FunctionComponent } from 'react';
interface NotificationBadgeProps {
show?: boolean;
style?: CSSProperties;
}
const NotificationBadge: FunctionComponent<NotificationBadgeProps> = ({ show, style }) => {
return show ? (
<div
style={{
position: 'absolute',
top: '8px',
right: '8px',
height: '10px',
width: '10px',
background: 'orange',
borderRadius: '50%',
...style,
}}
/>
) : null;
};
export default NotificationBadge;
+68
View File
@@ -0,0 +1,68 @@
import {
ButtonItem,
Focusable,
PanelSection,
PanelSectionRow,
joinClassNames,
scrollClasses,
staticClasses,
} from 'decky-frontend-lib';
import { VFC, useEffect, useState } from 'react';
import { Plugin } from '../plugin';
import { useDeckyState } from './DeckyState';
import NotificationBadge from './NotificationBadge';
import { useQuickAccessVisible } from './QuickAccessVisibleState';
import TitleView from './TitleView';
const PluginView: VFC = () => {
const { plugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = useDeckyState();
const visible = useQuickAccessVisible();
const [pluginList, setPluginList] = useState<Plugin[]>(
plugins.sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name)),
);
useEffect(() => {
setPluginList(plugins.sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name)));
console.log('updating PluginView after changes');
}, [plugins, pluginOrder]);
if (activePlugin) {
return (
<Focusable onCancelButton={closeActivePlugin}>
<TitleView />
<div
className={joinClassNames(staticClasses.TabGroupPanel, scrollClasses.ScrollPanel, scrollClasses.ScrollY)}
style={{ height: '100%' }}
>
{(visible || activePlugin.alwaysRender) && activePlugin.content}
</div>
</Focusable>
);
}
return (
<>
<TitleView />
<div className={joinClassNames(staticClasses.TabGroupPanel, scrollClasses.ScrollPanel, scrollClasses.ScrollY)}>
<PanelSection>
{pluginList
.filter((p) => p.content)
.map(({ name, icon }) => (
<PanelSectionRow key={name}>
<ButtonItem layout="below" onClick={() => setActivePlugin(name)}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
{icon}
<div>{name}</div>
<NotificationBadge show={updates?.has(name)} style={{ top: '-5px', right: '-5px' }} />
</div>
</ButtonItem>
</PanelSectionRow>
))}
</PanelSection>
</div>
</>
);
};
export default PluginView;
@@ -0,0 +1,17 @@
import { FC, createContext, useContext, useState } from 'react';
const QuickAccessVisibleState = createContext<boolean>(true);
export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState);
export const QuickAccessVisibleStateProvider: FC<{ initial: boolean; tab: any }> = ({ children, initial, tab }) => {
const [visible, setVisible] = useState<boolean>(initial);
const [prev, setPrev] = useState<boolean>(initial);
// HACK but i can't think of a better way to do this
tab.qAMVisibilitySetter = setVisible;
if (initial != prev) {
setPrev(initial);
setVisible(initial);
}
return <QuickAccessVisibleState.Provider value={visible}>{children}</QuickAccessVisibleState.Provider>;
};
+60
View File
@@ -0,0 +1,60 @@
import { DialogButton, Focusable, Router, staticClasses } from 'decky-frontend-lib';
import { CSSProperties, VFC } from 'react';
import { BsGearFill } from 'react-icons/bs';
import { FaArrowLeft, FaStore } from 'react-icons/fa';
import { useDeckyState } from './DeckyState';
const titleStyles: CSSProperties = {
display: 'flex',
paddingTop: '3px',
paddingRight: '16px',
};
const TitleView: VFC = () => {
const { activePlugin, closeActivePlugin } = useDeckyState();
const onSettingsClick = () => {
Router.CloseSideMenus();
Router.Navigate('/decky/settings');
};
const onStoreClick = () => {
Router.CloseSideMenus();
Router.Navigate('/decky/store');
};
if (activePlugin === null) {
return (
<Focusable style={titleStyles} className={staticClasses.Title}>
<div style={{ marginRight: 'auto', flex: 0.9 }}>Decky</div>
<DialogButton
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
onClick={onStoreClick}
>
<FaStore style={{ marginTop: '-4px', display: 'block' }} />
</DialogButton>
<DialogButton
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
onClick={onSettingsClick}
>
<BsGearFill style={{ marginTop: '-4px', display: 'block' }} />
</DialogButton>
</Focusable>
);
}
return (
<div 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>
);
};
export default TitleView;
+47
View File
@@ -0,0 +1,47 @@
import { ToastData, findModule, joinClassNames } from 'decky-frontend-lib';
import { FunctionComponent } from 'react';
interface ToastProps {
toast: ToastData;
}
export const toastClasses = findModule((mod) => {
if (typeof mod !== 'object') return false;
if (mod.ToastPlaceholder) {
return true;
}
return false;
});
const templateClasses = findModule((mod) => {
if (typeof mod !== 'object') return false;
if (mod.ShortTemplate) {
return true;
}
return false;
});
const Toast: FunctionComponent<ToastProps> = ({ toast }) => {
return (
<div
style={{ '--toast-duration': `${toast.duration}ms` } as React.CSSProperties}
onClick={toast.onClick}
className={joinClassNames(templateClasses.ShortTemplate, toast.className || '')}
>
{toast.logo && <div className={templateClasses.StandardLogoDimensions}>{toast.logo}</div>}
<div className={joinClassNames(templateClasses.Content, toast.contentClassName || '')}>
<div className={templateClasses.Header}>
{toast.icon && <div className={templateClasses.Icon}>{toast.icon}</div>}
<div className={templateClasses.Title}>{toast.title}</div>
</div>
<div className={templateClasses.Body}>{toast.body}</div>
</div>
</div>
);
};
export default Toast;
+38
View File
@@ -0,0 +1,38 @@
import { Focusable, SteamSpinner } from 'decky-frontend-lib';
import { FunctionComponent, ReactElement, ReactNode, Suspense } from 'react';
interface WithSuspenseProps {
children: ReactNode;
route?: boolean;
}
// Nice little wrapper around Suspense so we don't have to duplicate the styles and code for the loading spinner
const WithSuspense: FunctionComponent<WithSuspenseProps> = (props) => {
const propsCopy = { ...props };
delete propsCopy.children;
(props.children as ReactElement)?.props && Object.assign((props.children as ReactElement).props, propsCopy); // There is probably a better way to do this but valve does it this way so ¯\_(ツ)_/¯
return (
<Suspense
fallback={
<Focusable
// needed to enable focus ring so that the focus properly resets on load
onActivate={() => {}}
style={{
overflowY: 'scroll',
backgroundColor: 'transparent',
...(props.route && {
marginTop: '40px',
height: 'calc( 100% - 40px )',
}),
}}
>
<SteamSpinner />
</Focusable>
}
>
{props.children}
</Suspense>
);
};
export default WithSuspense;
@@ -0,0 +1,82 @@
import { ConfirmModal, Navigation, QuickAccessTab } from 'decky-frontend-lib';
import { FC, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { InstallType } from '../../plugin';
interface MultiplePluginsInstallModalProps {
requests: { name: string; version: string; hash: string; install_type: InstallType }[];
onOK(): void | Promise<void>;
onCancel(): void | Promise<void>;
closeModal?(): void;
}
// values are the JSON keys used in the translation file
const InstallTypeTranslationMapping = {
[InstallType.INSTALL]: 'install',
[InstallType.REINSTALL]: 'reinstall',
[InstallType.UPDATE]: 'update',
} as const satisfies Record<InstallType, string>;
type TitleTranslationMapping = 'mixed' | (typeof InstallTypeTranslationMapping)[InstallType];
const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
requests,
onOK,
onCancel,
closeModal,
}) => {
const [loading, setLoading] = useState<boolean>(false);
const { t } = useTranslation();
// used as part of the title translation
// if we know all operations are of a specific type, we can show so in the title to make decision easier
const installTypeGrouped = useMemo((): TitleTranslationMapping => {
if (requests.every(({ install_type }) => install_type === InstallType.INSTALL)) return 'install';
if (requests.every(({ install_type }) => install_type === InstallType.REINSTALL)) return 'reinstall';
if (requests.every(({ install_type }) => install_type === InstallType.UPDATE)) return 'update';
return 'mixed';
}, [requests]);
return (
<ConfirmModal
bOKDisabled={loading}
closeModal={closeModal}
onOK={async () => {
setLoading(true);
await onOK();
setTimeout(() => Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky), 250);
setTimeout(() => window.DeckyPluginLoader.checkPluginUpdates(), 1000);
}}
onCancel={async () => {
await onCancel();
}}
strTitle={<div>{t(`MultiplePluginsInstallModal.title.${installTypeGrouped}`, { count: requests.length })}</div>}
strOKButtonText={t(`MultiplePluginsInstallModal.ok_button.${loading ? 'loading' : 'idle'}`)}
>
<div>
{t('MultiplePluginsInstallModal.confirm')}
<ul style={{ listStyle: 'none', display: 'flex', flexDirection: 'column', gap: '4px' }}>
{requests.map(({ name, version, install_type, hash }, i) => {
const installTypeStr = InstallTypeTranslationMapping[install_type];
const description = t(`MultiplePluginsInstallModal.description.${installTypeStr}`, {
name,
version,
});
return (
<li key={i} style={{ display: 'flex', flexDirection: 'column' }}>
<div>{description}</div>
{hash === 'False' && (
<div style={{ color: 'red', paddingLeft: '10px' }}>{t('PluginInstallModal.no_hash')}</div>
)}
</li>
);
})}
</ul>
</div>
</ConfirmModal>
);
};
export default MultiplePluginsInstallModal;
@@ -0,0 +1,88 @@
import { ConfirmModal, Navigation, QuickAccessTab } from 'decky-frontend-lib';
import { FC, useState } from 'react';
import { useTranslation } from 'react-i18next';
import TranslationHelper, { TranslationClass } from '../../utils/TranslationHelper';
interface PluginInstallModalProps {
artifact: string;
version: string;
hash: string;
installType: number;
onOK(): void;
onCancel(): void;
closeModal?(): void;
}
const PluginInstallModal: FC<PluginInstallModalProps> = ({
artifact,
version,
hash,
installType,
onOK,
onCancel,
closeModal,
}) => {
const [loading, setLoading] = useState<boolean>(false);
const { t } = useTranslation();
return (
<ConfirmModal
bOKDisabled={loading}
closeModal={closeModal}
onOK={async () => {
setLoading(true);
await onOK();
setTimeout(() => Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky), 250);
setTimeout(() => window.DeckyPluginLoader.checkPluginUpdates(), 1000);
}}
onCancel={async () => {
await onCancel();
}}
strTitle={
<div>
<TranslationHelper
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
trans_text="title"
i18n_args={{ artifact: artifact }}
install_type={installType}
/>
</div>
}
strOKButtonText={
loading ? (
<div>
<TranslationHelper
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
trans_text="button_processing"
install_type={installType}
/>
</div>
) : (
<div>
<TranslationHelper
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
trans_text="button_idle"
install_type={installType}
/>
</div>
)
}
>
<div>
<TranslationHelper
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
trans_text="desc"
i18n_args={{
artifact: artifact,
version: version,
}}
install_type={installType}
/>
</div>
{hash == 'False' && <span style={{ color: 'red' }}>{t('PluginInstallModal.no_hash')}</span>}
</ConfirmModal>
);
};
export default PluginInstallModal;
@@ -0,0 +1,170 @@
// https://codesandbox.io/s/react-file-icon-colored-tmwut?file=/src/App.js
import { FileIconProps } from 'react-file-icon';
type T_FileExtList = string[];
const styleDef: [FileIconProps, T_FileExtList][] = [];
// video ////////////////////////////////////
const videoStyle = {
color: '#f00f0f',
};
const videoExtList = [
'avi',
'3g2',
'3gp',
'aep',
'asf',
'flv',
'm4v',
'mkv',
'mov',
'mp4',
'mpeg',
'mpg',
'ogv',
'pr',
'swfw',
'webm',
'wmv',
'swf',
'rm',
];
styleDef.push([videoStyle, videoExtList]);
// image ////////////////////////////////////
const imageStyle = {
color: '#d18f00',
};
const imageExtList = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tif', 'tiff'];
styleDef.push([imageStyle, imageExtList]);
// zip ////////////////////////////////////
const zipStyle = {
color: '#f7b500',
labelTextColor: '#000',
// glyphColor: "#de9400"
};
const zipExtList = ['zip', 'zipx', '7zip', 'tar', 'sitx', 'gz', 'rar'];
styleDef.push([zipStyle, zipExtList]);
// audio ////////////////////////////////////
const audioStyle = {
color: '#f00f0f',
};
const audioExtList = ['aac', 'aif', 'aiff', 'flac', 'm4a', 'mid', 'mp3', 'ogg', 'wav'];
styleDef.push([audioStyle, audioExtList]);
// text ////////////////////////////////////
const textStyle = {
color: '#ffffff',
glyphColor: '#787878',
};
const textExtList = ['cue', 'odt', 'md', 'rtf', 'txt', 'tex', 'wpd', 'wps', 'xlr', 'fodt'];
styleDef.push([textStyle, textExtList]);
// system ////////////////////////////////////
const systemStyle = {
color: '#111',
};
const systemExtList = ['exe', 'ini', 'dll', 'plist', 'sys'];
styleDef.push([systemStyle, systemExtList]);
// srcCode ////////////////////////////////////
const srcCodeStyle = {
glyphColor: '#787878',
color: '#ffffff',
};
const srcCodeExtList = [
'asp',
'aspx',
'c',
'cpp',
'cs',
'css',
'scss',
'py',
'json',
'htm',
'html',
'java',
'yml',
'php',
'js',
'ts',
'rb',
'jsx',
'tsx',
];
styleDef.push([srcCodeStyle, srcCodeExtList]);
// vector ////////////////////////////////////
const vectorStyle = {
color: '#ffe600',
};
const vectorExtList = ['dwg', 'dxf', 'ps', 'svg', 'eps'];
styleDef.push([vectorStyle, vectorExtList]);
// font ////////////////////////////////////
const fontStyle = {
color: '#555',
};
const fontExtList = ['fnt', 'ttf', 'otf', 'fon', 'eot', 'woff'];
styleDef.push([fontStyle, fontExtList]);
// objectModel ////////////////////////////////////
const objectModelStyle = {
color: '#bf6a02',
glyphColor: '#bf6a02',
};
const objectModelExtList = ['3dm', '3ds', 'max', 'obj', 'pkg'];
styleDef.push([objectModelStyle, objectModelExtList]);
// sheet ////////////////////////////////////
const sheetStyle = {
color: '#2a6e00',
};
const sheetExtList = ['csv', 'fods', 'ods', 'xlr'];
styleDef.push([sheetStyle, sheetExtList]);
// const defaultStyle: Record<string, FileIconProps> = {
// pdf: {
// glyphColor: "white",
// color: "#D93831"
// }
// };
//////////////////////////////////////////////////
function createStyleObj(extList: T_FileExtList, styleObj: Partial<FileIconProps>) {
return Object.fromEntries(
extList.map((ext) => {
return [ext, { ...styleObj, glyphColor: 'white' }];
}),
);
}
export const styleDefObj = styleDef.reduce((acc, [fileStyle, fileExtList]) => {
return { ...acc, ...createStyleObj(fileExtList, fileStyle) };
});
@@ -0,0 +1,170 @@
import { DialogButton, Focusable, SteamSpinner, TextField } from 'decky-frontend-lib';
import { useEffect } from 'react';
import { FunctionComponent, useState } from 'react';
import { FileIcon, defaultStyles } from 'react-file-icon';
import { useTranslation } from 'react-i18next';
import { FaArrowUp, FaFolder } from 'react-icons/fa';
import Logger from '../../../logger';
import { styleDefObj } from './iconCustomizations';
const logger = new Logger('FilePicker');
export interface FilePickerProps {
startPath: string;
includeFiles?: boolean;
regex?: RegExp;
onSubmit: (val: { path: string; realpath: string }) => void;
closeModal?: () => void;
}
interface File {
isdir: boolean;
name: string;
realpath: string;
}
interface FileListing {
realpath: string;
files: File[];
}
function getList(
path: string,
includeFiles: boolean = true,
): Promise<{ result: FileListing | string; success: boolean }> {
return window.DeckyPluginLoader.callServerMethod('filepicker_ls', { path, include_files: includeFiles });
}
const iconStyles = {
paddingRight: '10px',
width: '1em',
};
const FilePicker: FunctionComponent<FilePickerProps> = ({
startPath,
includeFiles = true,
regex,
onSubmit,
closeModal,
}) => {
const { t } = useTranslation();
if (startPath.endsWith('/')) startPath = startPath.substring(0, startPath.length - 1); // remove trailing path
const [path, setPath] = useState<string>(startPath);
const [listing, setListing] = useState<FileListing>({ files: [], realpath: path });
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
(async () => {
if (error) setError(null);
setLoading(true);
const listing = await getList(path, includeFiles);
if (!listing.success) {
setListing({ files: [], realpath: path });
setLoading(false);
setError(listing.result as string);
logger.error(listing.result);
return;
}
setLoading(false);
setListing(listing.result as FileListing);
logger.log('reloaded', path, listing);
})();
}, [path]);
return (
<div className="deckyFilePicker">
<Focusable style={{ display: 'flex', flexDirection: 'row', paddingBottom: '10px' }}>
<DialogButton
style={{
minWidth: 'unset',
width: '40px',
flexGrow: '0',
borderRadius: 'unset',
margin: '0',
padding: '10px',
}}
onClick={() => {
const newPathArr = path.split('/');
newPathArr.pop();
let newPath = newPathArr.join('/');
if (newPath == '') newPath = '/';
setPath(newPath);
}}
>
<FaArrowUp />
</DialogButton>
<div style={{ flexGrow: '1', width: '100%' }}>
<TextField
value={path}
onChange={(e) => {
e.target.value && setPath(e.target.value);
}}
style={{ height: '100%' }}
/>
</div>
</Focusable>
<Focusable style={{ display: 'flex', flexDirection: 'column', height: '60vh', overflow: 'scroll' }}>
{loading && <SteamSpinner style={{ height: '100%' }} />}
{!loading &&
listing.files
.filter((file) => (includeFiles || file.isdir) && (!regex || regex.test(file.name)))
.map((file) => {
let extension = file.realpath.split('.').pop() as string;
return (
<DialogButton
style={{ borderRadius: 'unset', margin: '0', padding: '10px' }}
onClick={() => {
const fullPath = `${path}${path.endsWith('/') ? '' : '/'}${file.name}`;
if (file.isdir) setPath(fullPath);
else {
onSubmit({ path: fullPath, realpath: file.realpath });
closeModal?.();
}
}}
>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'flex-start' }}>
{file.isdir ? (
<FaFolder style={iconStyles} />
) : (
<div style={iconStyles}>
{file.realpath.includes('.') ? (
<FileIcon {...defaultStyles[extension]} {...styleDefObj[extension]} extension={''} />
) : (
<FileIcon />
)}
</div>
)}
<span
style={{
textOverflow: 'ellipsis',
overflow: 'hidden',
textAlign: 'left',
}}
>
{file.name}
</span>
</div>
</DialogButton>
);
})}
{error}
</Focusable>
{!loading && !error && !includeFiles && (
<DialogButton
className="Primary"
style={{ marginTop: '10px', alignSelf: 'flex-end' }}
onClick={() => {
onSubmit({ path, realpath: listing.realpath });
closeModal?.();
}}
>
{t('FilePickerIndex.folder.select')}
</DialogButton>
)}
</div>
);
};
export default FilePicker;
@@ -0,0 +1 @@
This directory contains patches that replace Valve's broken file picker with ours.
@@ -0,0 +1,10 @@
import library from './library';
let patches: Function[] = [];
export function deinitFilepickerPatches() {
patches.forEach((unpatch) => unpatch());
}
export async function initFilepickerPatches() {
patches.push(await library());
}
@@ -0,0 +1,64 @@
import { Patch, findModuleChild, replacePatch, sleep } from 'decky-frontend-lib';
import Logger from '../../../../logger';
const logger = new Logger('LibraryPatch');
let patch: Patch;
function rePatch() {
// If you patch anything on SteamClient within the first few seconds of the client having loaded it will get redefined for some reason, so repatch any of these changes that occur with History.listen or an interval
patch = replacePatch(window.SteamClient.Apps, 'PromptToChangeShortcut', async ([appid]: number[]) => {
try {
const details = window.appDetailsStore.GetAppDetails(appid);
logger.debug('game details', details);
// strShortcutStartDir
const file = await window.DeckyPluginLoader.openFilePicker(
details?.strShortcutStartDir.replaceAll('"', '') || '/',
);
logger.debug('user selected', file);
window.SteamClient.Apps.SetShortcutExe(appid, JSON.stringify(file.path));
const pathArr = file.path.split('/');
pathArr.pop();
const folder = pathArr.join('/');
window.SteamClient.Apps.SetShortcutStartDir(appid, JSON.stringify(folder));
} catch (e) {
logger.error(e);
}
});
}
export default async function libraryPatch() {
try {
rePatch();
// TODO type and add to frontend-lib
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;
}
});
if (!History) {
logger.debug('Waiting 5s for history to become available.');
await sleep(5000);
}
}
const unlisten = History.listen(() => {
if (window.SteamClient.Apps.PromptToChangeShortcut !== patch.patchedFunction) {
rePatch();
}
});
return () => {
unlisten();
patch.unpatch();
};
} catch (e) {
logger.error('Error patching library file picker', e);
}
return () => {};
}
@@ -0,0 +1,21 @@
import { Focusable, updaterFieldClasses } from 'decky-frontend-lib';
import { FunctionComponent, ReactNode } from 'react';
interface InlinePatchNotesProps {
date: ReactNode;
title: string;
children: ReactNode;
onClick?: () => void;
}
const InlinePatchNotes: FunctionComponent<InlinePatchNotesProps> = ({ date, title, children, onClick }) => {
return (
<Focusable className={updaterFieldClasses.PatchNotes} onActivate={onClick}>
<div className={updaterFieldClasses.PostedTime}>{date}</div>
<div className={updaterFieldClasses.EventDetailTitle}>{title}</div>
<div className={updaterFieldClasses.EventDetailsBody}>{children}</div>
</Focusable>
);
};
export default InlinePatchNotes;
@@ -0,0 +1,45 @@
import { SidebarNavigation } from 'decky-frontend-lib';
import { lazy } from 'react';
import { useTranslation } from 'react-i18next';
import { FaCode, FaPlug } from 'react-icons/fa';
import { useSetting } from '../../utils/hooks/useSetting';
import DeckyIcon from '../DeckyIcon';
import WithSuspense from '../WithSuspense';
import GeneralSettings from './pages/general';
import PluginList from './pages/plugin_list';
const DeveloperSettings = lazy(() => import('./pages/developer'));
export default function SettingsPage() {
const [isDeveloper, setIsDeveloper] = useSetting<boolean>('developer.enabled', false);
const { t } = useTranslation();
const pages = [
{
title: t('SettingsIndex.general_title'),
content: <GeneralSettings isDeveloper={isDeveloper} setIsDeveloper={setIsDeveloper} />,
route: '/decky/settings/general',
icon: <DeckyIcon />,
},
{
title: t('SettingsIndex.plugins_title'),
content: <PluginList />,
route: '/decky/settings/plugins',
icon: <FaPlug />,
},
{
title: t('SettingsIndex.developer_title'),
content: (
<WithSuspense>
<DeveloperSettings />
</WithSuspense>
),
route: '/decky/settings/developer',
icon: <FaCode />,
visible: isDeveloper,
},
];
return <SidebarNavigation pages={pages} />;
}
@@ -0,0 +1,127 @@
import {
DialogBody,
DialogButton,
DialogControlsSection,
DialogControlsSectionHeader,
Field,
TextField,
Toggle,
} from 'decky-frontend-lib';
import { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaFileArchive, FaLink, FaReact, FaSteamSymbol } from 'react-icons/fa';
import { setShouldConnectToReactDevTools, setShowValveInternal } from '../../../../developer';
import { installFromURL } from '../../../../store';
import { useSetting } from '../../../../utils/hooks/useSetting';
import RemoteDebuggingSettings from '../general/RemoteDebugging';
const installFromZip = () => {
window.DeckyPluginLoader.openFilePicker('/home/deck', true).then((val) => {
const url = `file://${val.path}`;
console.log(`Installing plugin locally from ${url}`);
if (url.endsWith('.zip')) {
installFromURL(url);
} else {
window.DeckyPluginLoader.toaster.toast({
//title: t('SettingsDeveloperIndex.toast_zip.title'),
title: 'Decky',
//body: t('SettingsDeveloperIndex.toast_zip.body'),
body: 'Installation failed! Only ZIP files are supported.',
onClick: installFromZip,
});
}
});
};
export default function DeveloperSettings() {
const [enableValveInternal, setEnableValveInternal] = useSetting<boolean>('developer.valve_internal', false);
const [reactDevtoolsEnabled, setReactDevtoolsEnabled] = useSetting<boolean>('developer.rdt.enabled', false);
const [reactDevtoolsIP, setReactDevtoolsIP] = useSetting<string>('developer.rdt.ip', '');
const [pluginURL, setPluginURL] = useState('');
const textRef = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
return (
<DialogBody>
<DialogControlsSection>
<DialogControlsSectionHeader>
{t('SettingsDeveloperIndex.third_party_plugins.header')}
</DialogControlsSectionHeader>
<Field
label={t('SettingsDeveloperIndex.third_party_plugins.label_zip')}
icon={<FaFileArchive style={{ display: 'block' }} />}
>
<DialogButton onClick={installFromZip}>
{t('SettingsDeveloperIndex.third_party_plugins.button_zip')}
</DialogButton>
</Field>
<Field
label={t('SettingsDeveloperIndex.third_party_plugins.label_url')}
description={
<TextField
label={t('SettingsDeveloperIndex.third_party_plugins.label_desc')}
value={pluginURL}
onChange={(e) => setPluginURL(e?.target.value)}
/>
}
icon={<FaLink style={{ display: 'block' }} />}
>
<DialogButton disabled={pluginURL.length == 0} onClick={() => installFromURL(pluginURL)}>
{t('SettingsDeveloperIndex.third_party_plugins.button_install')}
</DialogButton>
</Field>
</DialogControlsSection>
<DialogControlsSection>
<DialogControlsSectionHeader>{t('SettingsDeveloperIndex.header')}</DialogControlsSectionHeader>
<RemoteDebuggingSettings />
<Field
label={t('SettingsDeveloperIndex.valve_internal.label')}
description={
<span style={{ whiteSpace: 'pre-line' }}>
{t('SettingsDeveloperIndex.valve_internal.desc1')}{' '}
<span style={{ color: 'red' }}>{t('SettingsDeveloperIndex.valve_internal.desc2')}</span>
</span>
}
icon={<FaSteamSymbol style={{ display: 'block' }} />}
>
<Toggle
value={enableValveInternal}
onChange={(toggleValue) => {
setEnableValveInternal(toggleValue);
setShowValveInternal(toggleValue);
}}
/>
</Field>
<Field
label={t('SettingsDeveloperIndex.react_devtools.label')}
description={
<>
<span style={{ whiteSpace: 'pre-line' }}>{t('SettingsDeveloperIndex.react_devtools.desc')}</span>
<br />
<br />
<div ref={textRef}>
<TextField
label={t('SettingsDeveloperIndex.react_devtools.ip_label')}
value={reactDevtoolsIP}
onChange={(e) => setReactDevtoolsIP(e?.target.value)}
/>
</div>
</>
}
icon={<FaReact style={{ display: 'block' }} />}
>
<Toggle
value={reactDevtoolsEnabled}
// disabled={reactDevtoolsIP == ''}
onChange={(toggleValue) => {
setReactDevtoolsEnabled(toggleValue);
setShouldConnectToReactDevTools(toggleValue);
}}
/>
</Field>
</DialogControlsSection>
</DialogBody>
);
}
@@ -0,0 +1,48 @@
import { Dropdown, Field } from 'decky-frontend-lib';
import { FunctionComponent } from 'react';
import { useTranslation } from 'react-i18next';
import Logger from '../../../../logger';
import { callUpdaterMethod } from '../../../../updater';
import { useSetting } from '../../../../utils/hooks/useSetting';
const logger = new Logger('BranchSelect');
enum UpdateBranch {
Stable,
Prerelease,
// Testing,
}
const BranchSelect: FunctionComponent<{}> = () => {
const { t } = useTranslation();
const tBranches = [
t('BranchSelect.update_channel.stable'),
t('BranchSelect.update_channel.prerelease'),
t('BranchSelect.update_channel.testing'),
];
const [selectedBranch, setSelectedBranch] = useSetting<UpdateBranch>('branch', UpdateBranch.Prerelease);
return (
// Returns numerical values from 0 to 2 (with current branch setup as of 8/28/22)
// 0 being stable, 1 being pre-release and 2 being nightly
<Field label={t('BranchSelect.update_channel.label')} childrenContainerWidth={'fixed'}>
<Dropdown
rgOptions={Object.values(UpdateBranch)
.filter((branch) => typeof branch == 'string')
.map((branch) => ({
label: tBranches[UpdateBranch[branch]],
data: UpdateBranch[branch],
}))}
selectedOption={selectedBranch}
onChange={async (newVal) => {
await setSelectedBranch(newVal.data);
callUpdaterMethod('check_for_updates');
logger.log('switching branches!');
}}
/>
</Field>
);
};
export default BranchSelect;
@@ -0,0 +1,27 @@
import { Field, Toggle } from 'decky-frontend-lib';
import { useTranslation } from 'react-i18next';
import { FaChrome } from 'react-icons/fa';
import { useSetting } from '../../../../utils/hooks/useSetting';
export default function RemoteDebuggingSettings() {
const [allowRemoteDebugging, setAllowRemoteDebugging] = useSetting<boolean>('cef_forward', false);
const { t } = useTranslation();
return (
<Field
label={t('RemoteDebugging.remote_cef.label')}
description={<span style={{ whiteSpace: 'pre-line' }}>{t('RemoteDebugging.remote_cef.desc')}</span>}
icon={<FaChrome style={{ display: 'block' }} />}
>
<Toggle
value={allowRemoteDebugging || false}
onChange={(toggleValue) => {
setAllowRemoteDebugging(toggleValue);
if (toggleValue) window.DeckyPluginLoader.callServerMethod('allow_remote_debugging');
else window.DeckyPluginLoader.callServerMethod('disallow_remote_debugging');
}}
/>
</Field>
);
}
@@ -0,0 +1,59 @@
import { Dropdown, Field, TextField } from 'decky-frontend-lib';
import { FunctionComponent } from 'react';
import { useTranslation } from 'react-i18next';
import { FaShapes } from 'react-icons/fa';
import Logger from '../../../../logger';
import { Store } from '../../../../store';
import { useSetting } from '../../../../utils/hooks/useSetting';
const logger = new Logger('StoreSelect');
const StoreSelect: FunctionComponent<{}> = () => {
const [selectedStore, setSelectedStore] = useSetting<Store>('store', Store.Default);
const [selectedStoreURL, setSelectedStoreURL] = useSetting<string | null>('store-url', null);
const { t } = useTranslation();
const tStores = [
t('StoreSelect.store_channel.default'),
t('StoreSelect.store_channel.testing'),
t('StoreSelect.store_channel.custom'),
];
// Returns numerical values from 0 to 2 (with current branch setup as of 8/28/22)
// 0 being Default, 1 being Testing and 2 being Custom
return (
<>
<Field label={t('StoreSelect.store_channel.label')} childrenContainerWidth={'fixed'}>
<Dropdown
rgOptions={Object.values(Store)
.filter((store) => typeof store == 'string')
.map((store) => ({
label: tStores[Store[store]],
data: Store[store],
}))}
selectedOption={selectedStore}
onChange={async (newVal) => {
await setSelectedStore(newVal.data);
logger.log('switching stores!');
}}
/>
</Field>
{selectedStore == Store.Custom && (
<Field
label={t('StoreSelect.custom_store.label')}
indentLevel={1}
description={
<TextField
label={t('StoreSelect.custom_store.url_label')}
value={selectedStoreURL || undefined}
onChange={(e) => setSelectedStoreURL(e?.target.value || null)}
/>
}
icon={<FaShapes style={{ display: 'block' }} />}
></Field>
)}
</>
);
};
export default StoreSelect;
@@ -0,0 +1,169 @@
import {
Carousel,
DialogButton,
Field,
FocusRing,
Focusable,
ProgressBarWithInfo,
Spinner,
findSP,
showModal,
} from 'decky-frontend-lib';
import { useCallback } from 'react';
import { Suspense, lazy } from 'react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaExclamation } from 'react-icons/fa';
import { VerInfo, callUpdaterMethod, finishUpdate } from '../../../../updater';
import { useDeckyState } from '../../../DeckyState';
import InlinePatchNotes from '../../../patchnotes/InlinePatchNotes';
import WithSuspense from '../../../WithSuspense';
const MarkdownRenderer = lazy(() => import('../../../Markdown'));
function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | null; closeModal?: () => {} }) {
const SP = findSP();
const { t } = useTranslation();
return (
<Focusable onCancelButton={closeModal}>
<FocusRing>
<Carousel
fnItemRenderer={(id: number) => (
<Focusable
style={{
marginTop: '40px',
height: 'calc( 100% - 40px )',
overflowY: 'scroll',
display: 'flex',
justifyContent: 'center',
margin: '40px',
}}
>
<div>
<h1>{versionInfo?.all?.[id]?.name || 'Invalid Update Name'}</h1>
{versionInfo?.all?.[id]?.body ? (
<WithSuspense>
<MarkdownRenderer onDismiss={closeModal}>{versionInfo.all[id].body}</MarkdownRenderer>
</WithSuspense>
) : (
t('Updater.no_patch_notes_desc')
)}
</div>
</Focusable>
)}
fnGetId={(id) => id}
nNumItems={versionInfo?.all?.length}
nHeight={SP.innerHeight - 40}
nItemHeight={SP.innerHeight - 40}
nItemMarginX={0}
initialColumn={0}
autoFocus={true}
fnGetColumnWidth={() => SP.innerWidth}
name={t('Updater.decky_updates') as string}
/>
</FocusRing>
</Focusable>
);
}
export default function UpdaterSettings() {
const { isLoaderUpdating, setIsLoaderUpdating, versionInfo, setVersionInfo } = useDeckyState();
const [checkingForUpdates, setCheckingForUpdates] = useState<boolean>(false);
const [updateProgress, setUpdateProgress] = useState<number>(-1);
const [reloading, setReloading] = useState<boolean>(false);
const { t } = useTranslation();
useEffect(() => {
window.DeckyUpdater = {
updateProgress: (i) => {
setUpdateProgress(i);
setIsLoaderUpdating(true);
},
finish: async () => {
setUpdateProgress(0);
setReloading(true);
await finishUpdate();
},
};
}, []);
const showPatchNotes = useCallback(() => {
showModal(<PatchNotesModal versionInfo={versionInfo} />);
}, [versionInfo]);
return (
<>
<Field
onOptionsActionDescription={versionInfo?.all ? t('Updater.patch_notes_desc') : undefined}
onOptionsButton={versionInfo?.all ? showPatchNotes : undefined}
label={t('Updater.updates.label')}
description={
checkingForUpdates || versionInfo?.remote?.tag_name != versionInfo?.current || !versionInfo?.remote ? (
''
) : (
<span>{t('Updater.updates.lat_version', { ver: versionInfo?.current })} </span>
)
}
icon={
versionInfo?.remote &&
versionInfo?.remote?.tag_name != versionInfo?.current && (
<FaExclamation color="var(--gpColor-Yellow)" style={{ display: 'block' }} />
)
}
childrenContainerWidth={'fixed'}
>
{updateProgress == -1 && !isLoaderUpdating ? (
<DialogButton
disabled={!versionInfo?.updatable || checkingForUpdates}
onClick={
!versionInfo?.remote || versionInfo?.remote?.tag_name == versionInfo?.current
? async () => {
setCheckingForUpdates(true);
const res = (await callUpdaterMethod('check_for_updates')) as { result: VerInfo };
setVersionInfo(res.result);
setCheckingForUpdates(false);
}
: async () => {
setUpdateProgress(0);
callUpdaterMethod('do_update');
}
}
>
{checkingForUpdates
? t('Updater.updates.checking')
: !versionInfo?.remote || versionInfo?.remote?.tag_name == versionInfo?.current
? t('Updater.updates.check_button')
: t('Updater.updates.install_button')}
</DialogButton>
) : (
<ProgressBarWithInfo
layout="inline"
bottomSeparator="none"
nProgress={updateProgress}
indeterminate={reloading}
sOperationText={reloading ? t('Updater.updates.reloading') : t('Updater.updates.updating')}
/>
)}
</Field>
{versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current && (
<InlinePatchNotes
title={versionInfo?.remote.name}
date={new Intl.RelativeTimeFormat('en-US', {
numeric: 'auto',
}).format(
Math.ceil((new Date(versionInfo.remote.published_at).getTime() - new Date().getTime()) / 86400000),
'day',
)}
onClick={showPatchNotes}
>
<Suspense fallback={<Spinner style={{ width: '24', height: '24' }} />}>
<MarkdownRenderer>{versionInfo?.remote.body}</MarkdownRenderer>
</Suspense>
</InlinePatchNotes>
)}
</>
);
}
@@ -0,0 +1,49 @@
import { DialogBody, DialogControlsSection, DialogControlsSectionHeader, Field, Toggle } from 'decky-frontend-lib';
import { useTranslation } from 'react-i18next';
import { useDeckyState } from '../../../DeckyState';
import BranchSelect from './BranchSelect';
import StoreSelect from './StoreSelect';
import UpdaterSettings from './Updater';
export default function GeneralSettings({
isDeveloper,
setIsDeveloper,
}: {
isDeveloper: boolean;
setIsDeveloper: (val: boolean) => void;
}) {
const { versionInfo } = useDeckyState();
const { t } = useTranslation();
return (
<DialogBody>
<DialogControlsSection>
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.updates.header')}</DialogControlsSectionHeader>
<UpdaterSettings />
</DialogControlsSection>
<DialogControlsSection>
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.beta.header')}</DialogControlsSectionHeader>
<BranchSelect />
<StoreSelect />
</DialogControlsSection>
<DialogControlsSection>
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.other.header')}</DialogControlsSectionHeader>
<Field label={t('SettingsGeneralIndex.developer_mode.label')}>
<Toggle
value={isDeveloper}
onChange={(toggleValue) => {
setIsDeveloper(toggleValue);
}}
/>
</Field>
</DialogControlsSection>
<DialogControlsSection>
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.about.header')}</DialogControlsSectionHeader>
<Field label={t('SettingsGeneralIndex.about.decky_version')} focusable={true}>
<div style={{ color: 'var(--gpSystemLighterGrey)' }}>{versionInfo?.current}</div>
</Field>
</DialogControlsSection>
</DialogBody>
);
}

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